Что такое чистый, pythonic способ иметь несколько конструкторов в Python?

Я не могу найти окончательный ответ на это. Насколько я знаю, вы не можете иметь несколько функций __init__ в классе Python. Так как мне решить эту проблему?

Предположим, у меня есть класс с именем Cheese со свойством number_of_holes. Как у меня может быть два способа создания сырных объектов...

  1. Тот, который занимает несколько таких отверстий: parmesan = Cheese(num_holes = 15)
  2. И тот, который не принимает аргументов и просто рандомизирует свойство 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

Текст от Alex Martelli

(как упоминается комментарий от @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")