9

Как красиво и "питонично" реализовать несколько конструкторов?

13

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

Предположим, у меня есть класс под названием 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:
            # Случайным образом задаём number_of_holes
        else:
            number_of_holes = num_holes

Что вы на это скажете? Есть ли другой способ?

5 ответ(ов)

9

На самом деле использование None гораздо более предпочтительно для "магических" значений:

class Cheese:
    def __init__(self, num_holes=None):
        if num_holes is None:
            ...

Теперь, если вы хотите полностью свободно добавлять дополнительные параметры, вы можете использовать следующее:

class Cheese:
    def __init__(self, *args, **kwargs):
        # args -- кортеж анонимных аргументов
        # kwargs -- словарь именованных аргументов
        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}

Дополнительную информацию можно найти в документации Python.

0

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

Подход с использованием @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)  # Не вызывает __init__
        super(MyClass, obj).__init__()  # Не забудьте вызвать инициализаторы любых полиморфных базовых классов
        obj._value = load_from_somewhere(somename)
        return obj
0

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

def __init__(self, num_holes):
    # выполнение действий с количеством

@classmethod
def fromRandom(cls):
    return cls( # случайное число )

Таким образом, вы можете создать объект вашего класса, используя метод fromRandom, что делает код более читаемым и удобным для использования.

0

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

def __init__(self, num_holes=None):
    if num_holes is None:
        # Создаем гауду
    else:
        # кастомный сыр
    # Общее инициализация

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

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

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

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

0

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

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

# cheese_user.py
from cheeses import make_gouda, make_parmesean

gouda = make_gouda()
parmesan = make_paremesean()

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

# cheeses.py
class Cheese(object):
    def __init__(self, *args, **kwargs):
        # args -- кортеж анонимных аргументов
        # kwargs -- словарь именованных аргументов
        self.num_holes = kwargs.get('num_holes', random_holes())

def make_gouda():
    return Cheese()

def make_parmesan():
    return Cheese(num_holes=15)

Это хороший метод инкапсуляции, и я считаю, что он более "пайтонский". На мой взгляд, такой способ соответствует принципу duck typing: вы просто запрашиваете объект гуды и не особо волнуетесь, к какому классу он принадлежит.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь