Как красиво и "питонично" реализовать несколько конструкторов?
Я не могу найти однозначный ответ на этот вопрос. Насколько я знаю, в классе Python не может быть нескольких функций __init__
. Как мне решить эту проблему?
Предположим, у меня есть класс под названием 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:
# Случайным образом задаём number_of_holes
else:
number_of_holes = num_holes
Что вы на это скажете? Есть ли другой способ?
5 ответ(ов)
На самом деле использование 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.
Следует определенно отдавать предпочтение уже опубликованным решениям, однако, поскольку никто пока не упомянул данный способ, я думаю, что стоит его упомянуть для полноты картины.
Подход с использованием @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
Все эти ответы отличные, если вы хотите использовать опциональные параметры, но еще одной "питонической" возможностью является использование метода класса для создания фактически псевдоконструктора в стиле фабрики:
def __init__(self, num_holes):
# выполнение действий с количеством
@classmethod
def fromRandom(cls):
return cls( # случайное число )
Таким образом, вы можете создать объект вашего класса, используя метод fromRandom
, что делает код более читаемым и удобным для использования.
Почему вы считаете, что ваше решение «неуклюжее»? Лично я предпочел бы один конструктор с значениями по умолчанию, а не несколько перегруженных конструкторов в подобных ситуациях (в 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...
Это хорошие идеи для вашей реализации, но если вы представляете интерфейс для производства сыра пользователю, ему не важно, сколько дырок в сыре или какие внутренности используются для его приготовления. Пользователь вашего кода просто хочет "гуду" или "пармезан", верно?
Почему бы не сделать так:
# 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: вы просто запрашиваете объект гуды и не особо волнуетесь, к какому классу он принадлежит.
Понимание Python super() с методами __init__()
Почему классы в Python наследуют от object?
Разница между старыми и новыми классами в Python?
Как изменить порядок столбцов в DataFrame?
Изменение типа столбца в pandas