У меня есть сценарий использования, в котором мульти-наследование кажется правильным. Но это подразумевает совместное использование атрибутов между «родными» классами, атрибутов, которые инициализируются в других классах (так или иначе неизвестных для них).
Я спрашиваю, является ли это ниже «правильной» и «питонической» моделью, или мне лучше пойти с моделью деривативных классов.
Допустим, мы хотим разработать разных поставщиков, которые будут принимать некоторые исходные данные, применять какой-то формат к ним и отправлять их через некоторый канал. И эти три части (data - format - send) можно настраивать для каждого случая.
Для начала работайте с кодом, чтобы приведенные ниже примеры работали:
import sys
PY3 = not sys.version_info < (3,)
from string import Template
import csv, io, smtplib, requests, os
def read_test_movies(year_from, year_to, genre= None):
TEST_MOVIES= [
{'year': 1971, 'release': '01/01/1971', 'genre': 'thriller', 'title': 'Play Misty for Me'},
{'year': 1973, 'release': '02/02/1973', 'genre': 'romantic', 'title': 'Breezy'},
{'year': 1976, 'release': '03/03/1976', 'genre': 'western', 'title': 'The Outlaw'},
{'year': 1986, 'release': '04/04/1986', 'genre': 'war', 'title': 'Heartbreak'},
{'year': 1988, 'release': '05/05/1988', 'genre': 'music', 'title': 'Bird'},
{'year': 1992, 'release': '06/06/1992', 'genre': 'western', 'title': 'Unforgiven'},
{'year': 1995, 'release': '07/07/1995', 'genre': 'romantic', 'title': 'The Bridges of Madison County'},
{'year': 2000, 'release': '08/08/2000', 'genre': 'space', 'title': 'Space Cowboys'},
{'year': 2003, 'release': '09/09/2003', 'genre': 'trhiller', 'title': 'Mystic River'},
{'year': 2004, 'release': '10/10/2004', 'genre': 'sports', 'title': 'Million Dollar Baby'},
{'year': 2006, 'release': '11/11/2006', 'genre': 'war', 'title': 'Flags of Our Fathers'},
{'year': 2006, 'release': '12/12/2006', 'genre': 'war', 'title': 'Letters from Iwo Jima'},
{'year': 2008, 'release': '13/11/2008', 'genre': 'drama', 'title': 'Changeling'},
{'year': 2008, 'release': '14/10/2008', 'genre': 'drama', 'title': 'Gran Torino'},
{'year': 2009, 'release': '15/09/2009', 'genre': 'sports', 'title': 'Invictus'},
{'year': 2010, 'release': '16/08/2010', 'genre': 'drama', 'title': 'Hereafter'},
{'year': 2011, 'release': '17/07/2011', 'genre': 'drama', 'title': 'J. Edgar'},
{'year': 2014, 'release': '18/06/2014', 'genre': 'war', 'title': 'American Sniper'},
{'year': 2016, 'release': '19/05/2016', 'genre': 'drama', 'title': 'Sully'}
]
out= []
for m in TEST_MOVIES:
if year_from <= m['year'] and m['year'] <= year_to:
if genre is None or (genre is not None and genre == m['genre']):
out.append(m)
return out
Поскольку эти три части (data - format - send) настолько различимы, мы бы начали с этих интерфейсоподобных классов (я думаю, abc тоже можно использовать):
class ITheData(object):
def __init__(self, year_from, year_to, genre= None):
self.year_from= year_from
self.year_to = year_to
self.genre = genre
def readMovies(self):
raise NotImplementedError('%s.readMovies() must be implemented' % self.__class__.__name__)
class ITheFormat(object):
def filename(self):
raise NotImplementedError('%s.filename() must be implemented' % self.__class__.__name__)
def make(self):
raise NotImplementedError('%s.make() must be implemented' % self.__class__.__name__)
class ITheSend(object):
def send(self):
raise NotImplementedError('%s.send() must be implemented' % self.__class__.__name__)
Для каждой пользовательской доставки мы подклассируем три из них и объединяем их в класс, подобный:
class ITheDeliverer(ITheData, ITheFormat, ITheSend):
def deliver(self):
raise NotImplementedError('%s.deliver() must be implemented' % self.__class__.__name__)
Итак, у нас может быть два разных источника данных. Помимо источника, они могут отличаться по действиям пост-обработки. Хотя для простоты я просто делаю self.readMovies()
повсеместно, это может быть какой-то другой пользовательский метод в подклассе.
class TheIMDBData(ITheData):
def readMovies(self):
# movies = some_read_from_IMDB(self.genre, self.year_from, self.year_to)
movies= read_test_movies(self.year_from, self.year_to, self.genre)
return movies
class TheTMDbData(ITheData):
def readMovies(self):
# movies = some_read_from_TMDb(self.genre, self.year_from, self.year_to)
movies= read_test_movies(self.year_from, self.year_to, self.genre)
return movies
Мы могли бы также использовать два разных формата:
class TheTXTFormat(ITheFormat):
def filename(self):
# Here `genre`, `year_from` and `year_to` are unknown
params= {'genre': self.genre, 'year_from': self.year_from, 'year_to': self.year_to}
return Template('movies_of_${genre}_from_${year_from}_to_${year_to}.txt').substitute(**params)
def make(self):
# Here `readMovies()` is unknown
strio = PY3 and io.StringIO() or io.BytesIO()
for movie in self.readMovies():
line= Template('$title, released on $release').substitute(**movie)
line+= '\n'
strio.write(line)
strio.seek(0)
return strio.read()
class TheCSVFormat(ITheFormat):
def filename(self):
# Here `genre`, `year_from` and `year_to` are unknown
params= {'genre': self.genre, 'year_from': self.year_from, 'year_to': self.year_to}
return Template('movies_of_${genre}_from_${year_from}_to_${year_to}.csv').substitute(**params)
def make(self):
# Here `readMovies()` is unknown
strio = PY3 and io.StringIO() or io.BytesIO()
writer = csv.writer(strio, delimiter=';', quotechar='"', quoting=csv.QUOTE_MINIMAL)
header = ('Title', 'Release')
writer.writerow(header)
for movie in self.readMovies():
writer.writerow((movie['title'], movie['release']))
strio.seek(0)
return strio.read()
и два разных канала отправки:
class TheMailSend(ITheSend):
host = 'localhost'
sender = 'movie@spammer.com'
receivers = ['movie@spammed.com']
def send(self):
# Here `make()` is unknown
print('TheMailSend.send() Sending to %s' % str(self.receivers))
try:
message = self.make() # Format agnostic
smtpObj = smtplib.SMTP(self.host)
smtpObj.sendmail(self.sender, self.receivers, message)
return True, 'ok'
except Exception as ss:
return False, str(ss)
class TheWSSend(ITheSend):
url = 'spammed.com/movies/send'
def send(self):
# Here `make()` is unknown
print('TheWSSend.send() Sending to %s' % str(self.url))
try:
content = self.make() # Format agnostic
s= requests.Session()
response= s.post(url= self.url, data= {'content': content})
s.close()
if response.status_code == 200:
return True, 'ok'
else:
return False, response.status_code
except Exception as ss:
return False, str(ss)
Итак, мы могли бы покончить с такими избавителями:
class TheIMDBToTXTFile(ITheDeliverer, TheIMDBData, TheTXTFormat):
def __init__(self, year_from, year_to, genre= None):
TheIMDBData.__init__(self, year_from, year_to, genre)
def deliver(self):
filepath= os.path.join('/tmp', self.filename())
f= open(filepath, 'w')
f.write(self.make())
f.close()
print('TheIMDBToTXTFile.deliver() => Successfully delivered to %s' % str(filepath))
class TheIMDBToWS(ITheDeliverer, TheIMDBData, TheTXTFormat, TheWSSend):
def __init__(self, year_from, year_to, genre=None):
TheIMDBData.__init__(self, year_from, year_to, genre)
def deliver(self):
ok, msg = self.send()
if ok:
print('TheIMDBToWS.deliver() => Successfully delivered!')
else:
print('TheIMDBToWS.deliver() => Error delivering: %s' % str(msg))
class TheTMDbToMail(ITheDeliverer, TheTMDbData, TheCSVFormat, TheMailSend):
def __init__(self, year_from, year_to, genre=None):
TheTMDbData.__init__(self, year_from, year_to, genre)
def deliver(self):
ok, msg= self.send()
if ok:
print('TheTMDbToMail.deliver() => Successfully delivered!')
else:
print('TheTMDbToMail.deliver() => Error delivering: %s' % str(msg))
И они работают нормально - с очевидными ошибками соединения -:
>>> imdbToTxt = TheIMDBToTXTFile(year_from= 2000, year_to= 2010)
>>> imdbToTxt.deliver()
TheIMDBToTXTFile.deliver() => Successfully delivered to /tmp/movies_of_None_from_200_to_2010.txt
>>>
>>> imdbToWs = TheIMDBToWS(year_from= 2000, year_to= 2010)
>>> imdbToWs.deliver()
TheWSSend.send() Sending to http://spammed.com/movies/send?
TheIMDBToWS.deliver() => Error delivering: 405
>>>
>>> tmdbToMail = TheTMDbToMail(year_from= 1980, year_to= 2019, genre= 'war')
>>> tmdbToMail.deliver()
TheMailSend.send() Sending to ['movie@spammed.com']
TheTMDbToMail.deliver() => Error delivering: [Errno 111] Connection refused
Но, как прокомментировано, некоторые атрибуты неизвестны для некоторых классов, и линтер, очевидно, жалуется на это:
Instance of 'TheTXTFormat' has no 'genre' member
Instance of 'TheTXTFormat' has no 'year_from' member
Instance of 'TheTXTFormat' has no 'year_to' member
Instance of 'TheTXTFormat' has no 'readMovies' member
Instance of 'TheCSVFormat' has no 'genre' member
Instance of 'TheCSVFormat' has no 'year_from' member
Instance of 'TheCSVFormat' has no 'year_to' member
Instance of 'TheCSVFormat' has no 'readMovies' member
Instance of 'TheMailSend' has no 'make' member
Instance of 'TheWSSend' has no 'make' member
Итак, остается вопрос: является ли мульти-наследование хорошей моделью?
Альтернативами могут быть: модель производных классов или просто независимые классы и передача параметров типа data
или formatter
. Но ни один из них не кажется таким простым, как множественное наследование (хотя они решали бы проблемы с линтером и, возможно, концептуально).