Что такое чистый, питонский способ иметь несколько конструкторов в Python? - PullRequest
621 голосов
/ 25 марта 2009

Не могу найти однозначного ответа на это. 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

Что вы говорите? Есть ли другой способ?

Ответы [ 13 ]

743 голосов
/ 25 марта 2009

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

584 голосов
/ 25 марта 2009

Использование 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()
21 голосов
/ 25 марта 2009

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

def __init__(self, num_holes):

  # do stuff with the number

@classmethod
def fromRandom(cls):

  return cls( # some-random-number )
17 голосов
/ 25 октября 2011

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

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

# 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. Для меня этот способ ведения дел больше соответствует линии утки. Вы просто запрашиваете объект Гауда, и вам все равно, какой это класс.

17 голосов
/ 25 марта 2009

Почему вы думаете, что ваше решение "неуклюжее"? Лично я предпочел бы один конструктор со значениями по умолчанию над несколькими перегруженными конструкторами в ситуациях, подобных вашей (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):
    # ...

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

14 голосов
/ 11 августа 2016

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

Подход @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
10 голосов
/ 25 марта 2009

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

А как насчет нового метода.

"Типичные реализации создают новый экземпляр класса, вызывая метод new () суперкласса, используя super (currentclass, cls). new (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)
8 голосов
/ 25 марта 2009

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

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

2 голосов
/ 28 апреля 2015

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

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


class Parmesan(Cheese):
    def __init__(self):
        super(Parmesan).__init__(num_holes=15) 
0 голосов
/ 14 февраля 2019

Поскольку мой первоначальный ответ подвергся критике на основании , что мои специальные конструкторы не вызывали (уникальный) конструктор по умолчанию, я публикую здесь измененную версию, которая учитывает пожелания что все конструкторы должны вызывать значение по умолчанию:

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")
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...