Наилучшая практика наследования: * args, ** kwargs или явно задающие параметры
Я часто нахожу себя переписыванием методов родительского класса и никогда не могу решить, должен ли я явно перечислять данные параметры или просто использовать конструкцию скрытого *args, **kwargs
. Является ли одна версия лучше, чем другая? Есть ли наилучшая практика? Какие (недостатки) мне не хватает?
class Parent(object):
def save(self, commit=True):
# ...
class Explicit(Parent):
def save(self, commit=True):
super(Explicit, self).save(commit=commit)
# more logic
class Blanket(Parent):
def save(self, *args, **kwargs):
super(Blanket, self).save(*args, **kwargs)
# more logic
Воспринимаемые преимущества явного варианта
- Более явный (Zen of Python)
- легче понять
- параметры функции легко доступны
Воспринимаемые преимущества защитного варианта
- больше DRY
- родительский класс легко взаимозаменяем.
- изменение значений по умолчанию в родительском методе распространяется без касания другого кода.
Ответы
Ответ 1
Принцип замены Лискова
Как правило, вы не хотите, чтобы ваша подпись метода изменялась в производных типах. Это может вызвать проблемы, если вы хотите поменять использование производных типов. Это часто называют Принципом замены Лискова.
Преимущества явных подписей
В то же время я не считаю правильным, чтобы все ваши методы имели подпись *args
, **kwargs
. Явные подписи:
- Помогите документировать метод с помощью хороших имен аргументов
- Помогите документировать метод, указав, какие аргументы необходимы и которые имеют значения по умолчанию
- обеспечивает неявное подтверждение (отсутствующие обязательные аргументы указывают очевидные исключения)
Аргументы и связь переменной длины
Не принимайте аргументы переменной длины для хорошей практики сочетания. Между родительским классом и производными классами должно быть определенное сцепление, иначе они не будут связаны друг с другом. Нормально, что связанный код приводит к сочетанию, которое отражает уровень сплоченности.
Места для использования аргументов переменной длины
Использование аргументов переменной длины не должно быть вашим первым вариантом. Его следует использовать, если у вас есть веская причина:
- Определение оболочки функции (то есть декоратора).
- Определение параметрической полиморфной функции.
- Когда аргументы, которые вы можете предпринять, действительно являются полностью переменными (например, обобщенная функция соединения с БД). Функции соединения с DB обычно принимают строку во многих разных формах, как в виде одного аргумента, так и в форме с несколькими аргументами. Существуют также различные наборы параметров для разных баз данных.
- ...
Вы делаете что-то не так?
Если вы обнаружите, что часто создаете методы, которые принимают множество аргументов или производных методов с разными сигнатурами, у вас может возникнуть большая проблема в том, как вы организуете свой код.
Ответ 2
Мой выбор:
class Child(Parent):
def save(self, commit=True, **kwargs):
super(Child, self).save(commit, **kwargs)
# more logic
Он избегает доступа к аргументу фиксации из *args
и **kwargs
, и он сохраняет все в безопасности, если изменяется подпись Parent:save
(например, добавление нового аргумента по умолчанию).
Обновить. В этом случае наличие аргументов * args может вызвать проблемы, если новый родительский аргумент добавлен. Я бы сохранил только **kwargs
и управлял только новыми аргументами со значениями по умолчанию. Это предотвратит распространение ошибок.
Ответ 3
Если вы уверены, что Child сохранит подпись, безусловно, предпочтительный явный подход, но когда Child изменит подпись, я лично предпочитаю использовать оба подхода:
class Parent(object):
def do_stuff(self, a, b):
# some logic
class Child(Parent):
def do_stuff(self, c, *args, **kwargs):
super(Child, self).do_stuff(*args, **kwargs)
# some logic with c
Таким образом, изменения в сигнатуре вполне читаемы в Child, в то время как исходная подпись вполне читаема в Parent.
По-моему, это также лучший способ, когда у вас есть множественное наследование, потому что вызов super
несколько раз довольно отвратительный, когда у вас нет аргументов и kwargs.
Для чего это стоит, это также предпочтительный способ в довольно нескольких библиотеках и инфраструктурах Python (Django, Tornado, Requests, Markdown, чтобы назвать несколько). Хотя не следует основывать свой выбор на таких вещах, я просто подразумеваю, что этот подход довольно распространен.
Ответ 4
Не совсем ответ, но больше примечание: если вы действительно хотите, чтобы значения по умолчанию для родительского класса распространялись на дочерние классы, вы можете сделать что-то вроде:
class Parent(object):
default_save_commit=True
def save(self, commit=default_save_commit):
# ...
class Derived(Parent):
def save(self, commit=Parent.default_save_commit):
super(Derived, self).save(commit=commit)
Однако я должен признать, что это выглядит довольно уродливо, и я буду использовать его только в том случае, если почувствую, что мне это действительно нужно.
Ответ 5
Я предпочитаю явные аргументы, потому что auto complete позволяет вам видеть подпись метода функции при вызове функции.