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

Я не могу найти окончательного ответа на этот вопрос. AFAIK, вы не можете иметь несколько __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

что скажешь? Есть ли другой способ?

11 ответов


на самом деле 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


используя 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()

все эти ответы превосходны, если вы хотите использовать дополнительные параметры, но другая возможность Pythonic-использовать classmethod для создания псевдо-конструктора в Заводском стиле:

def __init__(self, num_holes):

  # do stuff with the number

@classmethod
def fromRandom(cls):

  return cls( # some-random-number )

почему вы думаете, что ваше решение является "неуклюжим"? Лично я предпочел бы один конструктор со значениями по умолчанию над несколькими перегруженными конструкторами в таких ситуациях, как ваша (Python все равно не поддерживает перегрузку метода):

def __init__(self, num_holes=None):
    if num_holes is None:
        # Construct a gouda
    else:
        # custom cheese
    # common initialization

для действительно сложных случаев с большим количеством различных конструкторов может быть чище использовать вместо этого различные заводские функции:

@classmethod
def create_gouda(cls):
    c = Cheese()
    # ...
    return c

@classmethod
def create_cheddar(cls):
    # ...

в вашем примере с сыром вы можете использовать подкласс Gouda сыра хотя...


Это хорошие идеи для вашей реализации, но если вы представляете интерфейс для создания сыра пользователю. Им все равно, сколько дырок в сыре или какие внутренности идут на производство сыра. Пользователь вашего кода просто хочет "Гауда" или "пармезан", верно?

Так почему бы не сделать так:

# 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)

Это хороший метод инкапсуляции, и я думаю это скорее питон. Для меня этот способ делать вещи больше подходит в соответствии с утиной печатью. Вы просто просите объект гауды, и вам все равно, какой это класс.


лучший ответ-это тот, который выше о аргументах по умолчанию, но мне было весело писать это, и он, безусловно, подходит для "нескольких конструкторов". Используйте на свой страх и риск.

насчет новая метод.

" типичные реализации создают новый экземпляр класса, вызывая суперкласс новая() метод с использованием super (currentclass, cls).новая(cls[, ...]) с соответствующими аргументами, а затем изменение вновь созданного экземпляра по мере необходимости перед его возвратом."

таким образом, вы можете иметь новая метод измените определение класса, прикрепив соответствующий метод конструктора.

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)

определенно следует предпочесть уже опубликованные решения,но поскольку никто еще не упомянул это решение, я думаю, что стоит упомянуть для полноты.

на @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

использовать num_holes=None вместо этого по умолчанию. Затем проверьте, есть ли num_holes is None и если это так, выборочно. Во всяком случае, я обычно это вижу.

более радикально разные методы построения могут гарантировать classmethod, который возвращает экземпляр cls.


Я бы использовал наследование. Особенно, если будет больше различий, чем количество отверстий. Особенно, если Гауда нужно будет иметь другой набор членов, то пармезан.

class Gouda(Cheese):
    def __init__(self):
        super(Gouda).__init__(num_holes=10)


class Parmesan(Cheese):
    def __init__(self):
        super(Parmesan).__init__(num_holes=15) 

вот как я решил это для 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__ С несколькими сообщениями об ошибках, конечно. Я опустил их для этого примера.


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