Что такое чистый, pythonic способ иметь несколько конструкторов в Python?
Я не могу найти окончательный ответ на это. Насколько я знаю, вы не можете иметь несколько функций __init__
в классе Python. Так как мне решить эту проблему?
Предположим, у меня есть класс с именем Cheese
со свойством number_of_holes
. Как у меня может быть два способа создания сырных объектов...
- Тот, который занимает несколько таких отверстий:
parmesan = Cheese(num_holes = 15)
- И тот, который не принимает аргументов и просто рандомизирует свойство
number_of_holes
: gouda = Cheese()
Я могу придумать только один способ сделать это, но это кажется неуклюжим:
class Cheese():
def __init__(self, num_holes = 0):
if (num_holes == 0):
# randomize number_of_holes
else:
number_of_holes = num_holes
Что ты говоришь? Есть ли другой способ?
Ответы
Ответ 1
На самом деле None
намного лучше для "волшебных" значений:
class Cheese():
def __init__(self, num_holes = None):
if num_holes is None:
...
Теперь, если вам нужна полная свобода добавления дополнительных параметров:
class Cheese():
def __init__(self, *args, **kwargs):
#args -- tuple of anonymous arguments
#kwargs -- dictionary of named arguments
self.num_holes = kwargs.get('num_holes',random_holes())
Чтобы лучше объяснить концепцию *args
и **kwargs
(вы можете фактически изменить эти имена):
def f(*args, **kwargs):
print 'args: ', args, ' kwargs: ', kwargs
>>> f('a')
args: ('a',) kwargs: {}
>>> f(ar='a')
args: () kwargs: {'ar': 'a'}
>>> f(1,2,param=3)
args: (1, 2) kwargs: {'param': 3}
http://docs.python.org/reference/expressions.html#calls
Ответ 2
Использование num_holes=None
как значения по умолчанию хорошо, если вы собираетесь использовать только __init__
.
Если вам нужно несколько независимых "конструкторов", вы можете предоставить их как методы класса. Обычно они называются фабричными методами. В этом случае вы можете иметь значение по умолчанию для num_holes
0
.
class Cheese(object):
def __init__(self, num_holes=0):
"defaults to a solid cheese"
self.number_of_holes = num_holes
@classmethod
def random(cls):
return cls(randint(0, 100))
@classmethod
def slightly_holey(cls):
return cls(randint(0, 33))
@classmethod
def very_holey(cls):
return cls(randint(66, 100))
Теперь создайте объект так:
gouda = Cheese()
emmentaler = Cheese.random()
leerdammer = Cheese.slightly_holey()
Ответ 3
Все эти ответы превосходны, если вы хотите использовать необязательные параметры, но другая возможность Pythonic - использовать метод class для создания псевдоструктора factory:
def __init__(self, num_holes):
# do stuff with the number
@classmethod
def fromRandom(cls):
return cls( # some-random-number )
Ответ 4
Почему вы думаете, что ваше решение "неуклюже"? Лично я предпочел бы один конструктор со значениями по умолчанию над несколькими перегруженными конструкторами в таких ситуациях, как ваш (Python не поддерживает перегрузку метода в любом случае):
def __init__(self, num_holes=None):
if num_holes is None:
# Construct a gouda
else:
# custom cheese
# common initialization
Для действительно сложных случаев с множеством разных конструкторов может быть проще использовать разные функции factory:
@classmethod
def create_gouda(cls):
c = Cheese()
# ...
return c
@classmethod
def create_cheddar(cls):
# ...
В вашем примере с сыром вы, возможно, захотите использовать подкласс Gouda of Cheese, хотя...
Ответ 5
Это хорошие идеи для вашей реализации, но если вы представляете пользователю интерфейс для создания сыра. Им все равно, сколько отверстий у сыра или какие внутренние ингредиенты входят в сыр. Пользователь вашего кода просто хочет "гоуда" или "пармезан" правильно?
Так почему бы не сделать это:
# cheese_user.py
from cheeses import make_gouda, make_parmesean
gouda = make_gouda()
paremesean = make_parmesean()
И тогда вы можете использовать любой из приведенных выше методов для фактического выполнения функций:
# cheeses.py
class Cheese(object):
def __init__(self, *args, **kwargs):
#args -- tuple of anonymous arguments
#kwargs -- dictionary of named arguments
self.num_holes = kwargs.get('num_holes',random_holes())
def make_gouda():
return Cheese()
def make_paremesean():
return Cheese(num_holes=15)
Это хороший метод инкапсуляции, и я думаю, что он более Pythonic. Для меня этот способ делать что-то больше подходит больше, чем печатать на утке. Вы просто просите объект gouda, и вам все равно, какой он класс.
Ответ 6
Нужно отдать предпочтение уже опубликованным решениям, но поскольку никто не упомянул об этом решении, я думаю, что стоит упомянуть о полноте.
Подход @classmethod
можно изменить, чтобы предоставить альтернативный конструктор, который не вызывает конструктор по умолчанию (__init__
). Вместо этого экземпляр создается с помощью __new__
.
Это можно использовать, если тип инициализации не может быть выбран в зависимости от типа аргумента конструктора, а конструкторы не используют код.
Пример:
class MyClass(set):
def __init__(self, filename):
self._value = load_from_file(filename)
@classmethod
def from_somewhere(cls, somename):
obj = cls.__new__(cls) # Does not call __init__
obj._value = load_from_somewhere(somename)
return obj
Ответ 7
Лучше всего ответить на вопрос о аргументах по умолчанию, но мне было весело писать это, и это, безусловно, соответствует законопроекту для "нескольких конструкторов". Используйте на свой страх и риск.
Как насчет метода new.
"Типичные реализации создают новый экземпляр класса, вызывая метод new() суперкласса, используя super (currentclass, cls). new (cls [,...]) с соответствующими аргументами, а затем модифицируя вновь созданный экземпляр по мере необходимости, прежде чем возвращать его."
Таким образом, вы можете использовать метод new, чтобы изменить определение класса, добавив соответствующий метод конструктора.
class Cheese(object):
def __new__(cls, *args, **kwargs):
obj = super(Cheese, cls).__new__(cls)
num_holes = kwargs.get('num_holes', random_holes())
if num_holes == 0:
cls.__init__ = cls.foomethod
else:
cls.__init__ = cls.barmethod
return obj
def foomethod(self, *args, **kwargs):
print "foomethod called as __init__ for Cheese"
def barmethod(self, *args, **kwargs):
print "barmethod called as __init__ for Cheese"
if __name__ == "__main__":
parm = Cheese(num_holes=5)
Ответ 8
Используйте num_holes=None
по умолчанию. Затем проверьте, есть ли num_holes is None
, и если да, то рандомизируйте. Во всяком случае, это то, что я обычно вижу.
Более радикально разные методы построения могут гарантировать класс-метод, который возвращает экземпляр cls
.
Ответ 9
Я бы использовал наследование. Особенно, если будет больше различий, чем количество отверстий. Особенно, если у Гауды должен быть другой набор членов, а затем пармезан.
class Gouda(Cheese):
def __init__(self):
super(Gouda).__init__(num_holes=10)
class Parmesan(Cheese):
def __init__(self):
super(Parmesan).__init__(num_holes=15)
Ответ 10
(как упоминается комментарий от @ariddell)
ОК, я изучил и прочитал сообщения (и FAQ) о том, как имитировать несколько конструкторов. Алекс Мартелли дал самый надежный ответ проверяя количество * args и выполняя соответствующий раздел кода. Однако в другом посте он говорит
Это "перегружает" конструкторы, исходя из того, сколько аргументов данный - как изящный (и как Pythonic...!) это, конечно, спорно. Перегрузка по типам была бы менее изящной и менее Pythonic, хотя вы могли бы легко расширить эту идею, чтобы сделать это - я бы препятствовать ему еще сильнее.
Я думаю, что эта часть моего ответа - это то, что делает его "надежным": -).
Иными словами, лучший ответ на подобные запросы - это
часто: да, вы можете это сделать (и вот как), но есть лучше
(и вот они). Я не смог полностью войти в
"вот как" и "вот они", по общему признанию.
Однако, что мне нужно сделать, это именно то, что обескураживает, то есть создавая 3 конструктора с двумя аргументами, где второй аргумент каждый из них является другим типом. Настоящий кикер состоит в том, что в одном из конструкторы, мне нужно проверить класс объекта, чтобы убедиться, что метод получает соответствующий объект. У меня нет проблем кодировать это, если это так оно и должно быть, но если есть более приемлемые (и Pythonic) пути для этого я был бы признателен за некоторые указатели.
Почему, по вашему мнению, вам НЕОБХОДИМО различать вашу обработку на основе
тип аргумента или класс? Скорее всего, то, что вы хотите знать
о аргументе (для определения различной обработки в разных
случаев) - это то, как это БУДЕТ - что вы не можете сделать, проверяя типы,
или классы; скорее, вы можете использовать hasattr или try/except, чтобы узнать.
Сосредоточив внимание на поведении, а не на типе-идентичности, вы делаете
для программиста клиент-код проще: он или она могут затем
сделать полиморфное использование ваших компонентов с любым экземпляром, который
реализует необходимое поведение, которое необходимо для всего вашего кода.
Идея "перегрузки" - с возможностью вызова с одним заданным
имя, которое сопоставляется с несколькими внутренними вызовами в зависимости от различных
условия - также связаны с полиморфизмом; единственная веская причина
для подачи единственного вызываемого, который сопоставляется с несколькими, должен позволить
клиентский код использует, если нужно, полиморфно, если это необходимо.
Как правило, хорошая идея ТАКЖЕ выставлять множественные вызываемые вызовы
напрямую - не заставляйте программистов с клиентским кодом проходить через странные
чтобы убедиться, что "правильная" перегрузка вызывается в
конец; когда их потребность не полиморфна, пусть они явно
как можно проще. Это не сидит в
ну с "конструкторами" - вот почему функции factory имеют тенденцию
предпочтительнее, когда любое приложение нуждается в некотором богатстве
и сложность (factory callables, которые не являются
функции также в порядке, но встречаются более редкие, но все же связанные с ними потребности).
В Python (как в VB, так и на других языках с концепцией
явно именованных и дефолтных аргументов) у вас есть другой
стилистическая альтернатива "множественным вызовам": один вызываемый может
быть явно использованы для нескольких связанных целей, поставляя
разные именованные аргументы. Это можно легко переусердствовать (и VB
поставляет много примеров этого стиля, которые злоупотребляют!), но,
используется со вкусом и умеренностью, также может быть очень полезно.
Попробуйте увидеть один типичный пример. Мы хотим выставить класс
Munger, чьи экземпляры должны быть инициализированы "большим количеством
данные, которые нужно переманить ";" множество данных" может быть файлом, строкой,
или экземпляр нашего собственного класса DataBuffer, который обеспечивает
функции доступа к данным. Необходимы экземпляры Munger - фактически, когда мы
даны файл или строка, мы сами создаем DataBuffer
и пусть это все равно.
Стиль "перегрузки" может быть:
class Munger:
def __init__(self, data):
name = type(data).__name__
if name=='instance':
name = data.__class__.__name__
method = getattr(self, '_init_'+name)
method(data)
def _init_string(self, data):
self.data = DataBuffer(data)
def _init_file(self, data):
self.data = DataBuffer(data)
def _init_DataBuffer(self, data):
self.data = data
Теперь это предназначено как "плохой пример", и, возможно, я переусердствовал
Плохо, но я надеюсь, что, по крайней мере, ясно, зачем это делать
путь был бы существенно неоптимальным. Это не используется ни в одном
способ полиморфизма собственного конструктора DataBuffer, AND
он серьезно тормозит возможности полиморфизма клиентского кода
(за исключением таких трюков, как именование класса, например, "строка"...!).
Ясно, что это проще, поскольку "Мангер должен быть
передал DataBuffer или что-нибудь, что DataBuffer может быть построен из ':
class Munger:
def __init__(self, data):
if not isinstance(data, DataBuffer):
data = DataBuffer(data)
self.data = data
по крайней мере, здесь мы имеем некоторую простоту. Полиморфизм по-прежнему
не оптимальный; если клиентский код хочет имитировать данные
буфера, он должен наследовать от нашего класса DataBuffer, даже
если он не использует какую-либо его реализацию, просто чтобы удовлетворить
наш проверка состояния. По крайней мере, можно было бы "разделить",
из DataBuffer интерфейс и части реализации:
class IDataBuffer:
def rewind(self):
raise TypeError, "must override .rewind method"
def nextBytes(self, N):
raise TypeError, "must override .nextBytes method"
def pushBack(self, bytes):
raise TypeError, "must override .pushBack method"
и т.д., с классом DataBuffer, наследующим от этого (и обеспечивая
необходимые переопределения, конечно) и проверка состояния
сделано против IDataBuffer. Не очень Pythonic, но работоспособный
если есть много методов DataBuffer, которые нам нужны - проверка
для каждого из них отдельно может возникнуть больше проблем, чем
это стоит.
DataBuffer имеет собственную "перегрузку" ("Я инициализирован из
файл или из строки? ") необходимо обработать. Еще раз,
было бы неправильно ввести код:
class DataBuffer(IDataBuffer):
def __init__(self, data):
name = type(data).__name__
if name=='instance':
name = data.__class__.__name__
method = getattr(self, '_init_'+name)
method(data)
def _init_string(self, data):
self.data = data
self.index = 0
def _init_file(self, data):
self.data = data.read()
self.index = 0
# etc etc
потому что это ужасно тормозит полиморфизм клиентского кода.
Здесь все, что нам нужно от "файлового объекта", - это метод .read
мы можем позвонить без аргументов для предоставления наших данных - так
почему бы не код, который напрямую...:
class DataBuffer(IDataBuffer):
def __init__(self, data):
try: self.data = data.read()
except AttributeError: self.data=data
self.index = 0
# etc etc
Это намного проще, конечно. Можно добавить некоторые
тесты при инициализации для обеспечения результирующих данных
могут использоваться для наших целей, но обычно нет
большая проблема, если ошибка (если таковая имеется)
а не при инициализации.
Также стоит рассмотреть альтернативную архитектуру. ДЕЛАЕТ
код клиента ДЕЙСТВИТЕЛЬНО НУЖЕН полиморфизм, подразумеваемый при прохождении
конструктор Munger, либо файл (например), либо
string (-like), с очень разной подразумеваемой семантикой
как получить данные от указанного объекта? Библиотеки Python
дайте нам контрпримеры этого - файловые объекты и
подобные строкам, как правило, передаются через отдельные
методы; там нет реальной возможности полиморфизма!
Итак...
class DataBuffer(IDataBuffer):
def __init__(self, data):
self.data = data
self.index = 0
# etc etc
class Munger:
def __init__(self, data):
self.data = data
# etc etc
def FileMunger(afile):
return Munger(DataBuffer(afile.read()))
def StringMunger(astring):
return Munger(DataBuffer(astring))
Там, не так ли? Два неперегруженных factory
функции, максимальная простота в самих конструкторах.
Клиентский код знает, что он использует для создания
Munger и не нуждается в полиморфизме - он будет
быть более ясными и более явными и читаемыми, если он вызывает
FileMunger или StringMunger, и только
использует Munger ctor непосредственно для тех случаев, когда он
необходимо повторно использовать какой-либо существующий экземпляр IDataBuffer.
Если очень часто полиморфное использование может принести пользу
автор клиентского кода, мы можем добавить еще factory
только для этой цели:
def AnyMunger(mystery):
if isinstance(mystery, IDataBuffer):
return Munger(mystery)
else:
try: return FileMunger(mystery)
except AttributeError: return StringMunger(mystery)
Однако, не стоит просто добавлять такие материалы
если его уместность явно
конкретный вариант использования/сценарий - "вам это не понадобится"
это БОЛЬШОЙ принцип дизайна:-) [Правила XP...! -)].
Теперь, это, конечно, игрушечный пример, но я надеюсь
что только из-за этого он может выявить проблемы больше
ясно - и, возможно, убедите вас переосмыслить свои
дизайн проще и удобнее.
Алекс
Ответ 11
Вот как я решил это для класса YearQuarter
, который я должен был создать. Я создал __init__
с единственным параметром value
. Код для __init__
определяет только тип value
и обрабатывает данные соответствующим образом. Если вам нужны несколько входных параметров, вы просто упаковываете их в один кортеж и проверяете, что value
является кортежем.
Вы используете его следующим образом:
>>> temp = YearQuarter(datetime.date(2017, 1, 18))
>>> print temp
2017-Q1
>>> temp = YearQuarter((2017, 1))
>>> print temp
2017-Q1
И вот как выглядит __init__
и остальная часть класса:
import datetime
class YearQuarter:
def __init__(self, value):
if type(value) is datetime.date:
self._year = value.year
self._quarter = (value.month + 2) / 3
elif type(value) is tuple:
self._year = int(value[0])
self._quarter = int(value[1])
def __str__(self):
return '{0}-Q{1}'.format(self._year, self._quarter)
Вы можете развернуть __init__
с несколькими сообщениями об ошибках, конечно. Я пропустил их для этого примера.
Ответ 12
Это довольно чистый способ, я думаю и хитрый
class A(object):
def __init__(self,e,f,g):
self.__dict__.update({k: v for k,v in locals().items() if k!='self'})
def bc(self):
print(self.f)
k=A(e=5,f=6,g=12)
k.bc() # >>>6
Ответ 13
class Cheese:
def __init__(self, *args, **kwargs):
"""A user-friendly initialiser for the general-purpose constructor.
"""
...
def _init_parmesan(self, *args, **kwargs):
"""A special initialiser for Parmesan cheese.
"""
...
def _init_gauda(self, *args, **kwargs):
"""A special initialiser for Gauda cheese.
"""
...
@classmethod
def make_parmesan(cls, *args, **kwargs):
new = cls.__new__(cls)
new._init_parmesan(*args, **kwargs)
return new
@classmethod
def make_gauda(cls, *args, **kwargs):
new = cls.__new__(cls)
new._init_gauda(*args, **kwargs)
return new
Ответ 14
Поскольку мой первоначальный ответ был подвергнут критике на том основании, что мои конструкторы специального назначения не вызывали (уникальный) конструктор по умолчанию, я публикую здесь измененную версию, которая учитывает пожелания всех конструкторов должен вызвать по умолчанию:
class Cheese:
def __init__(self, *args, _initialiser="_default_init", **kwargs):
"""A multi-initialiser.
"""
getattr(self, _initialiser)(*args, **kwargs)
def _default_init(self, ...):
"""A user-friendly smart or general-purpose initialiser.
"""
...
def _init_parmesan(self, ...):
"""A special initialiser for Parmesan cheese.
"""
...
def _init_gouda(self, ...):
"""A special initialiser for Gouda cheese.
"""
...
@classmethod
def make_parmesan(cls, *args, **kwargs):
return cls(*args, **kwargs, _initialiser="_init_parmesan")
@classmethod
def make_gouda(cls, *args, **kwargs):
return cls(*args, **kwargs, _initialiser="_init_gouda")