Наилучшая практика наследования: * 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 позволяет вам видеть подпись метода функции при вызове функции.