Принудительный класс для вызова родительского метода при его переопределении

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

Пример:

class Parent(object):

    def __init__(self):
        self.someValue=1

    def start(self):
        ''' Force method to be called'''
        self.someValue+=1

Правильная реализация того, что я хотел, чтобы мой дочерний класс Child выполнял:

class ChildCorrect(Parent):

    def start(self):
        Parent.start(self)
        self.someValue+=1

Однако существует ли способ заставить разработчиков, которые разрабатывают классы Child, специально вызвать (не просто переопределить) метод "start", определенный в классе Parent, если они забудут его называть:

class ChildIncorrect(Parent):

    def start(self):
        '''Parent method not called, but correctly overridden'''
        self.someValue+=1

Кроме того, если это не считается лучшей практикой, что может быть альтернативой?

Ответы

Ответ 1

Как (не) сделать это

Нет, нет безопасного способа заставить пользователей звонить супер. Давайте рассмотрим несколько вариантов, которые могут достичь той или иной аналогичной цели, и обсудим, почему это плохая идея. В следующем разделе я также расскажу, как разумно (в отношении сообщества Python) справиться с ситуацией.

  1. Во время определения подкласса метакласс может проверить, вызывает ли метод, переопределяющий целевой метод (= с тем же именем, что и целевой метод), вызов супер с соответствующими аргументами.

    Это требует глубокого специфического для реализации поведения, такого как использование модуля dis в CPython. Нет обстоятельств, в которых я мог бы представить, что это хорошая идея - это зависит от конкретной версии CPython и того факта, что вы используете CPython вообще.

  2. Метакласс может взаимодействовать с базовым классом. В этом сценарии базовый класс уведомляет метакласс, когда вызывается унаследованный метод, и метакласс обертывает любые переопределяющие методы частью защитного кода.

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

    Это также открывает вопрос: что делать, если переопределяющий метод не вызвал унаследованный метод? Вызов исключения может быть неожиданным для кода, использующего метод (или, что еще хуже, он может предполагать, что метод не имел никакого эффекта вообще, что не соответствует действительности).

    Также плохая обратная связь с разработчиком, переопределяющим класс (если он вообще получит такую обратную связь!).

  3. Метакласс может генерировать кусок защитного кода для каждого переопределенного метода, который автоматически вызывает унаследованный метод до или после выполнения переопределенного метода.

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

    Это нарушает хороший набор разумных принципов при кодировании Python (избегайте неожиданностей, явное лучше, чем неявное, и, возможно, больше).

  4. Комбинация пунктов 2 и 3. Используя взаимодействие base- и метакласса из пункта 2, защитный код из пункта 3 может быть расширен для автоматического вызова super, если переопределяющий метод не вызвал super себя.

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

    Тем не менее, все еще остаются проблемы. Хотя безопасность потока можно исправить с помощью локальных потоков, для переопределенного метода по-прежнему нет способа прервать вызов super, если не выполнено предварительное условие, за исключением вызова исключения, которое может быть нежелательным во всех случаях. Кроме того, super может вызываться автоматически только после переопределения метода, а не до, что, опять же, в некоторых случаях нежелательно.

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

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

Кроме того, если это не считается наилучшей практикой, что будет альтернативой?

Распространенная лучшая практика с Python - предполагать, что вы относитесь к взрослым по обоюдному согласию. Это означает, что никто активно не пытается делать неприятные вещи с вашим кодом, если вы не позволите им. В такой экосистеме было бы целесообразно добавить .. warning:: в документацию метода или класса, чтобы любой, кто наследовал от этого класса, знал, что он должен делать.

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

Это также нарушает принцип наименьшего удивления и "явный лучше, чем неявный" (никто не ожидает, что унаследованный метод будет вызван неявно!). Это, опять же, должно быть хорошо задокументировано, и в этом случае вы также можете прибегнуть к тому, чтобы просто не вызывать супер вызовы автоматически и просто документировать, что вызывать унаследованный метод имеет даже больший смысл, чем обычно.

Ответ 2

Если иерархия классов находится под вашим контролем, вы можете использовать то, что книга Gang of Four (Gamma, et al) Design Patterns вызывает шаблон шаблона шаблона:

class MyBase:
   def MyMethod(self):
      # place any code that must always be called here...
      print "Base class pre-code"

      # call an internal method that contains the subclass-specific code
      self._DoMyMethod()

      # ...and then any additional code that should always be performed
      # here.
      print "base class post-code!"

   def _DoMyMethod(self):
      print "BASE!"


class MyDerived(MyBase):
   def _DoMyMethod(self):
      print "DERIVED!"


b = MyBase()
d = MyDerived()

b.MyMethod()
d.MyMethod()

выходы:

Base class pre-code
BASE!
base class post-code!
Base class pre-code
DERIVED!
base class post-code!

Ответ 3

С некоторыми хаками метакласса вы можете обнаружить во время выполнения, был ли вызван родительский старт или нет. Однако я, безусловно, НЕ рекомендую использовать этот код в реальных ситуациях, он, вероятно, глючит многими способами, и он не должен использовать pythonic для таких ограничений.

class ParentMetaClass(type):

    parent_called = False

    def __new__(cls, name, bases, attrs):
        print cls, name, bases, attrs
        original_start = attrs.get('start')
        if original_start:
            if name == 'Parent':
                def patched_start(*args, **kwargs):
                    original_start(*args, **kwargs)
                    cls.parent_called = True

            else:
                def patched_start(*args, **kwargs):
                    original_start(*args, **kwargs)
                    if not cls.parent_called:
                        raise ValueError('Parent start not called')
                    cls.parent_called = False  

            attrs['start'] = patched_start

        return super(ParentMetaClass, cls).__new__(cls, name, bases, attrs)


class Parent(object):
    __metaclass__ = ParentMetaClass

    def start(self):
        print 'Parent start called.'


class GoodChild(Parent):
    def start(self):
        super(GoodChild, self).start()
        print 'I am a good child.'


class BadChild(Parent):
    def start(self):
        print 'I am a bad child, I will raise a ValueError.'

Ответ 4

Очень мало в Python заставляет других программистов кодировать определенный путь. Даже в стандартной библиотеке вы найдете предупреждения такие как это:

Если подкласс переопределяет конструктор, он должен обязательно вызвать конструктор базового класса, прежде чем делать что-либо еще в потоке.

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

Другими словами, Python не построен таким образом.

Ответ 5

Однако есть ли способ заставить разработчиков разрабатывать классы Child специально для вызова (а не только переопределения) метода "start", определенного в класс родителя в случае, если они забудут его называть

В Python вы не можете контролировать, как другие переопределяют ваши функции.

Ответ 6

Это не относится напрямую к Python, это относится к большинству ОО-языков. Вы должны определить абстрактный метод в вашем базовом классе, сделать его закрытым (но доступным для переопределения подклассами), а также создать открытый шаблонный метод в родительском классе, который будет вызывать этот абстрактный метод (определенный в вашем подклассе). Код клиента не сможет напрямую вызывать соответствующий метод, но сможет вызывать метод шаблона, который, в свою очередь, обеспечит вызов соответствующего метода в правильном порядке/контексте.

Другими словами, ваша система не должна зависеть от подклассов, вызывающих родительскую реализацию. Если это необходимо, то вам действительно следует пересмотреть свою реализацию и разложить рассматриваемый метод (например, использовать шаблонный шаблонный метод).

Кроме того, некоторые языки (например, Java) неявно вызывают родительский конструктор, если конструктор подкласса не делает этого явно (хотя не работает для простых методов).

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

Ответ 7

Все, что вам нужно, это super.

С его помощью ваш код может выглядеть так:

class Parent(object):
    def __init__(self):
        self.someValue = 1

    def start(self):
        self.someValue += 1


class Child(Parent):
    def start(self):
        # put code here
        super().start()
        # or here