Что «супер» делает в Python?

564

Какая разница между:

class Child(SomeBaseClass):
    def __init__(self):
        super(Child, self).__init__()

а также:

class Child(SomeBaseClass):
    def __init__(self):
        SomeBaseClass.__init__(self)

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

user25785
источник

Ответы:

309

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

Однако почти невозможно использовать множественное наследование без него super(). Это включает в себя общие идиомы, такие как миксины, интерфейсы, абстрактные классы и т. Д. Это распространяется на код, который позже расширяет ваш. Если кто-то позже захочет написать расширенный класс Childи миксин, его код не будет работать должным образом.

Джон Милликин
источник
7
Можете ли вы привести пример того, что вы подразумеваете под словом «это не будет работать должным образом»?
Чарли Паркер
320

Какая разница?

SomeBaseClass.__init__(self) 

средство для вызова SomeBaseClass«S __init__. пока

super(Child, self).__init__()

означает вызвать границу __init__из родительского класса, который следует Childв Порядке разрешения метода (MRO).

Если экземпляр является подклассом Child, то в MRO может быть другой родитель.

Объяснил просто

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

Как говорит Боб Мартин, хорошая архитектура позволяет вам откладывать принятие решений как можно дольше.

super() может включить такую ​​архитектуру.

Когда другой класс наследует класс, который вы написали, он также может наследоваться от других классов. И эти классы могут иметь __init__после этого __init__порядок упорядочения классов для разрешения методов.

Без super вы, вероятно, жестко закодировали бы родительский класс класса, который вы пишете (как в примере). Это будет означать, что вы не будете вызывать next __init__в MRO и, следовательно, не сможете повторно использовать код в нем.

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

Питон 2 против 3

Это работает в Python 2 и 3:

super(Child, self).__init__()

Это работает только в Python 3:

super().__init__()

Он работает без аргументов, перемещаясь вверх по фрейму стека и получая первый аргумент метода (обычно selfдля метода экземпляра или clsдля метода класса - но могут быть и другие имена) и находя класс (например Child) в свободных переменных ( это ищется с именем__class__ как свободная переменная замыкания в методе).

Я предпочитаю продемонстрировать кросс-совместимый способ использования super, но если вы используете только Python 3, вы можете вызывать его без аргументов.

Непрямое движение с прямой совместимостью

Что это тебе дает? Для одиночного наследования примеры из вопроса практически идентичны с точки зрения статического анализа. Однако использование superдает вам уровень косвенности с прямой совместимостью.

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

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

Внедрение зависимости

Другие люди могут использовать ваш код и ввести родителей в разрешение метода:

class SomeBaseClass(object):
    def __init__(self):
        print('SomeBaseClass.__init__(self) called')

class UnsuperChild(SomeBaseClass):
    def __init__(self):
        print('UnsuperChild.__init__(self) called')
        SomeBaseClass.__init__(self)

class SuperChild(SomeBaseClass):
    def __init__(self):
        print('SuperChild.__init__(self) called')
        super(SuperChild, self).__init__()

Допустим, вы добавили еще один класс к своему объекту и хотите добавить класс между Foo и Bar (для тестирования или по какой-либо другой причине):

class InjectMe(SomeBaseClass):
    def __init__(self):
        print('InjectMe.__init__(self) called')
        super(InjectMe, self).__init__()

class UnsuperInjector(UnsuperChild, InjectMe): pass

class SuperInjector(SuperChild, InjectMe): pass

При использовании дочернего элемента un-super не удается внедрить зависимость, поскольку используемый вами дочерний элемент жестко запрограммировал метод, вызываемый после его собственного:

>>> o = UnsuperInjector()
UnsuperChild.__init__(self) called
SomeBaseClass.__init__(self) called

Однако класс с дочерним элементом, который использует, superможет правильно внедрить зависимость:

>>> o2 = SuperInjector()
SuperChild.__init__(self) called
InjectMe.__init__(self) called
SomeBaseClass.__init__(self) called

Обращаясь к комментарию

Почему в мире это было бы полезно?

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

Мы хотим, чтобы методы рассматривались в этом порядке .

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

  1. получить Mro от типа экземпляра
  2. ищите тип, который определяет метод
  3. найти следующий тип с помощью метода
  4. связать этот метод и вызвать его с ожидаемыми аргументами

Не UnsuperChildдолжен иметь доступа к InjectMe. Почему не вывод «Всегда избегать использования super»? Что мне здесь не хватает?

UnsuperChildВовсе не имеют доступа к InjectMe. Это тот, UnsuperInjectorкоторый имеет доступ InjectMe- и все же не может вызвать метод этого класса из метода, от которого он наследуется UnsuperChild.

Оба дочерних класса намереваются вызвать метод с тем же именем, которое будет следующим в MRO, что может быть другим классом, о котором он не знал, когда создавался.

Тот, у которого нет superжестких кодов метода его родителя - поэтому он ограничил поведение своего метода, и подклассы не могут вводить функциональность в цепочку вызовов.

Один с super имеет большую гибкость. Цепочка вызовов для методов может быть перехвачена, а функциональность введена.

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

Вывод

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

Вы намереваетесь сослаться на следующий родительский класс, а не тот, от которого наследует дочерний класс.

Неиспользование superможет наложить ненужные ограничения на пользователей вашего кода.

Аарон Холл
источник
В C, DI, как это . Код здесь . Если я добавлю еще одну реализацию listинтерфейса, скажем, doublylinkedlistтогда приложение плавно выберет ее. Я могу сделать мой пример более настраиваемым, представив config.txtи связав реализацию во время загрузки. Это правильный пример? Если да, как мне соотнести ваш код? Смотрите первый совет DI в вики. Где настраивается любая новая реализация? в вашем коде
overexchange
Новая реализация создается путем наследования, например, когда один из классов «Инжектор» наследуется от InjectMeкласса. Однако комментарии не предназначены для обсуждения, поэтому я предлагаю вам обсудить это в чате с другими или задать новый вопрос на главном сайте.
Аарон Холл
отличный ответ! но при использовании множественного наследования возникают сложности с super () и __init__функциями. особенно если сигнатура __init__варьируется между классами в иерархии. Я добавил ответ, который фокусируется на этом аспекте
Aviad Rozenhek
35

Я немного поиграл super()и понял, что мы можем изменить порядок звонков.

Например, у нас есть следующая иерархическая структура:

    A
   / \
  B   C
   \ /
    D

В этом случае MRO D будет (только для Python 3):

In [26]: D.__mro__
Out[26]: (__main__.D, __main__.B, __main__.C, __main__.A, object)

Давайте создадим класс, где super()вызовы после выполнения метода.

In [23]: class A(object): #  or with Python 3 can define class A:
...:     def __init__(self):
...:         print("I'm from A")
...:  
...: class B(A):
...:      def __init__(self):
...:          print("I'm from B")
...:          super().__init__()
...:   
...: class C(A):
...:      def __init__(self):
...:          print("I'm from C")
...:          super().__init__()
...:  
...: class D(B, C):
...:      def __init__(self):
...:          print("I'm from D")
...:          super().__init__()
...: d = D()
...:
I'm from D
I'm from B
I'm from C
I'm from A

    A
   / 
  B  C
    /
    D

Итак, мы видим, что порядок разрешения такой же, как в MRO. Но когда мы вызываем super()в начале метода:

In [21]: class A(object):  # or class A:
...:     def __init__(self):
...:         print("I'm from A")
...:  
...: class B(A):
...:      def __init__(self):
...:          super().__init__()  # or super(B, self).__init_()
...:          print("I'm from B")
...:   
...: class C(A):
...:      def __init__(self):
...:          super().__init__()
...:          print("I'm from C")
...:  
...: class D(B, C):
...:      def __init__(self):
...:          super().__init__()
...:          print("I'm from D")
...: d = D()
...: 
I'm from A
I'm from C
I'm from B
I'm from D

У нас другой порядок, это обратный порядок кортежа MRO.

    A
   / 
  B  C
    /
    D 

Для дополнительного чтения я бы порекомендовал следующие ответы:

  1. Пример линеаризации C3 с супер (большая иерархия)
  2. Важные изменения поведения между старыми и новыми классами стиля
  3. Внутренняя история на классах нового стиля
SKhalymon
источник
Я не понимаю, почему порядок меняется. В первой части я понимаю, что DBCA, потому что D является первым классом, затем при загрузке self (B, C) в конечном итоге выведите B, C, затем только A, так как B (A), C (A) указали обратно на себя для окончательного часть. Если я следую этому пониманию, то не должна ли вторая часть походить на BCAD? Не могли бы вы объяснить мне немного, пожалуйста.
JJson
Плохо, я не заметил, что каждый экземпляр каждого класса сначала запускается с помощью super (). Тогда, если это так, разве это не должно быть ABCD? Я как-то понимаю, как появился ACBD, но до сих пор не смог убедить и все еще немного запутался. Насколько я понимаю, d = D () называется классом D (B, C) с 2 собственными параметрами, так как сначала вызывается super (), затем вызывается B вместе с его атрибутами, а D не печатается до C, потому что Class D (B, C) содержит 2 самопараметра, поэтому он должен выполнить второй класс C (A), после того, как выполнено, больше
самопараметров
Это происходит на основе определения Мро .
SKhalymon
1
затем будет напечатано C, затем напечатано B и, наконец, напечатано D. Я прав?
JJson
2
Это очень легко понять второй, если вы получаете первый. Это как стек. Вы помещаете print '' в стек и выполняете super (), когда он выполняет A, он начинает печатать вещи в этом стеке, так что порядок обратный.
Грант
35

Разве все это не предполагает, что базовый класс является классом нового стиля?

class A:
    def __init__(self):
        print("A.__init__()")

class B(A):
    def __init__(self):
        print("B.__init__()")
        super(B, self).__init__()

Не будет работать в Python 2. class Aдолжен быть в новом стиле, то есть:class A(object)

mhawke
источник
20

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

Рассмотрим иерархию классов A, Bи Cгде каждый класс является родителем одного следующего за ним, и a, bи cсоответствующие экземпляры каждого.

super(B, b) 
# resolves to the scope of B's parent i.e. A 
# and applies that scope to b, as if b was an instance of A

super(C, c) 
# resolves to the scope of C's parent i.e. B
# and applies that scope to c

super(B, c) 
# resolves to the scope of B's parent i.e. A 
# and applies that scope to c

Использование superсо статическим методом

например, используя super()изнутри __new__()метод

class A(object):
    def __new__(cls, *a, **kw):
        # ...
        # whatever you want to specialize or override here
        # ...

        return super(A, cls).__new__(cls, *a, **kw)

Объяснение:

1 - хотя обычно __new__()в качестве первого параметра принято ссылаться на вызывающий класс, в Python он реализован не как метод класса, а как статический метод. То есть ссылка на класс должна быть явно передана в качестве первого аргумента при __new__()непосредственном вызове :

# if you defined this
class A(object):
    def __new__(cls):
        pass

# calling this would raise a TypeError due to the missing argument
A.__new__()

# whereas this would be fine
A.__new__(A)

2 - при вызове super()get для родительского класса мы передаем дочерний класс в Aкачестве первого аргумента, затем передаем ссылку на интересующий объект, в данном случае это ссылка на класс, которая была передана при A.__new__(cls)вызове. В большинстве случаев это также является ссылкой на дочерний класс. В некоторых ситуациях это может быть не так, например, в случае наследования нескольких поколений.

super(A, cls)

3 - поскольку, как правило, __new__()является статическим методом, super(A, cls).__new__он также возвращает статический метод, и в этом случае необходимо явно указать все аргументы, включая ссылку на объект интереса cls.

super(A, cls).__new__(cls, *a, **kw)

4- делать то же самое без super

class A(object):
    def __new__(cls, *a, **kw):
        # ...
        # whatever you want to specialize or override here
        # ...

        return object.__new__(cls, *a, **kw)

Использование superс методом экземпляра

например, используя super()изнутри__init__()

class A(object): 
    def __init__(self, *a, **kw):
        # ...
        # you make some changes here
        # ...

        super(A, self).__init__(*a, **kw)

Объяснение:

1 __init__- это метод экземпляра, означающий, что в качестве первого аргумента он принимает ссылку на экземпляр. При вызове непосредственно из экземпляра ссылка передается неявно, то есть вам не нужно указывать ее:

# you try calling `__init__()` from the class without specifying an instance
# and a TypeError is raised due to the expected but missing reference
A.__init__() # TypeError ...

# you create an instance
a = A()

# you call `__init__()` from that instance and it works
a.__init__()

# you can also call `__init__()` with the class and explicitly pass the instance 
A.__init__(a)

2- при вызове super()внутри __init__()мы передаем дочерний класс в качестве первого аргумента, а интересующий объект - в качестве второго аргумента, который в общем случае является ссылкой на экземпляр дочернего класса.

super(A, self)

3. Вызов super(A, self)возвращает прокси-сервер, который разрешит область и применит ее, selfкак если бы это был экземпляр родительского класса. Давайте назовем этот прокси s. Так __init__()как это метод экземпляра, вызов s.__init__(...)неявно передаст ссылку в selfкачестве первого аргумента родителю __init__().

4 - чтобы сделать то же самое без superнас, нам нужно явно передать ссылку на экземпляр в родительскую версию __init__().

class A(object): 
    def __init__(self, *a, **kw):
        # ...
        # you make some changes here
        # ...

        object.__init__(self, *a, **kw)

Использование superс методом класса

class A(object):
    @classmethod
    def alternate_constructor(cls, *a, **kw):
        print "A.alternate_constructor called"
        return cls(*a, **kw)

class B(A):
    @classmethod
    def alternate_constructor(cls, *a, **kw):
        # ...
        # whatever you want to specialize or override here
        # ...

        print "B.alternate_constructor called"
        return super(B, cls).alternate_constructor(*a, **kw)

Объяснение:

1. Метод класса может быть вызван напрямую из класса и в качестве первого параметра принимает ссылку на класс.

# calling directly from the class is fine,
# a reference to the class is passed implicitly
a = A.alternate_constructor()
b = B.alternate_constructor()

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

super(B, cls_or_subcls)

3- Вызов super(B, cls)разрешается в объеме Aи применяется к нему cls. Так alternate_constructor()как это метод класса, вызов super(B, cls).alternate_constructor(...)неявно передаст ссылку в clsкачестве первого аргумента в Aверсиюalternate_constructor()

super(B, cls).alternate_constructor()

4 - чтобы сделать то же самое без использования, super()вам необходимо получить ссылку на несвязанную версию A.alternate_constructor()(то есть явную версию функции). Просто сделать это не будет работать:

class B(A):
    @classmethod
    def alternate_constructor(cls, *a, **kw):
        # ...
        # whatever you want to specialize or override here
        # ...

        print "B.alternate_constructor called"
        return A.alternate_constructor(cls, *a, **kw)

Выше не будет работать, потому что A.alternate_constructor()метод принимает неявную ссылку в Aкачестве первого аргумента. clsСущество прошло здесь будет его второй аргумент.

class B(A):
    @classmethod
    def alternate_constructor(cls, *a, **kw):
        # ...
        # whatever you want to specialize or override here
        # ...

        print "B.alternate_constructor called"
        # first we get a reference to the unbound 
        # `A.alternate_constructor` function 
        unbound_func = A.alternate_constructor.im_func
        # now we call it and pass our own `cls` as its first argument
        return unbound_func(cls, *a, **kw)
Майкл Экока
источник
6

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

Представьте себе экземпляр, jackсозданный из класса Jack, у которого есть цепочка наследования, как показано зеленым на рисунке. Вызов:

super(Jack, jack).method(...)

будет использовать MRO (Порядок разрешения методов) of jack(его дерево наследования в определенном порядке) и начнет поиск с Jack. Почему можно предоставить родительский класс? Хорошо, если мы начнем поиск с экземпляра jack, он найдет метод экземпляра, и все дело в том, чтобы найти метод его родителей.

Если кто-то не предоставляет аргументы для super, то он, как и первый передаваемый аргумент, является классом self, а второй передаваемый аргумент - self. Они автоматически рассчитываются для вас в Python3.

Однако, скажем, мы не хотим использовать Jackметод, вместо того, чтобы передать Jack, мы могли бы передать, Jenчтобы начать поиск метода сверху Jen.

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

Если Cainи то и Sueдругое имеет требуемый метод, Cainметод будет вызван первым. Это соответствует в коде:

Class Jen(Cain, Sue):

MRO слева направо.

run_the_race
источник
2

некоторые отличные ответы здесь, но они не решают, как использовать super()в случае, когда разные классы в иерархии имеют разные подписи ... особенно в случае__init__

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

Вот только решение этого сценария:

  1. классы верхнего уровня в вашей иерархии должны наследоваться от пользовательского класса, такого как SuperObject:
  2. если классы могут принимать разные аргументы, всегда передавайте все полученные аргументы суперфункции в качестве аргументов с ключевыми словами и всегда принимайте **kwargs.
class SuperObject:        
    def __init__(self, **kwargs):
        print('SuperObject')
        mro = type(self).__mro__
        assert mro[-1] is object
        if mro[-2] is not SuperObject:
            raise TypeError(
                'all top-level classes in this hierarchy must inherit from SuperObject',
                'the last class in the MRO should be SuperObject',
                f'mro={[cls.__name__ for cls in mro]}'
            )

        # super().__init__ is guaranteed to be object.__init__        
        init = super().__init__
        init()

пример использования:

class A(SuperObject):
    def __init__(self, **kwargs):
        print("A")
        super(A, self).__init__(**kwargs)

class B(SuperObject):
    def __init__(self, **kwargs):
        print("B")
        super(B, self).__init__(**kwargs)

class C(A):
    def __init__(self, age, **kwargs):
        print("C",f"age={age}")
        super(C, self).__init__(age=age, **kwargs)

class D(B):
    def __init__(self, name, **kwargs):
        print("D", f"name={name}")
        super(D, self).__init__(name=name, **kwargs)

class E(C,D):
    def __init__(self, name, age, *args, **kwargs):
        print( "E", f"name={name}", f"age={age}")
        super(E, self).__init__(name=name, age=age, *args, **kwargs)

E(name='python', age=28)

вывод:

E name=python age=28
C age=28
A
D name=python
B
SuperObject
Авиад Розенхек
источник
0
class Child(SomeBaseClass):
    def __init__(self):
        SomeBaseClass.__init__(self)

Это довольно легко понять.

class Child(SomeBaseClass):
    def __init__(self):
        super(Child, self).__init__()

Хорошо, что происходит сейчас, если вы используете super(Child,self)?

Когда создается дочерний экземпляр, его MRO (Порядок разрешения метода) имеет порядок (Child, SomeBaseClass, object) в зависимости от наследования. (предположим, что SomeBaseClass не имеет других родителей, кроме объекта по умолчанию)

Передавая Child, self, superвыполняет поиск в MRO selfэкземпляра и возвращает прокси-объект, следующий за Child, в данном случае это SomeBaseClass, этот объект затем вызывает __init__метод SomeBaseClass. Другими словами, если это так super(SomeBaseClass,self), superвозвращаемый прокси-объект будетobject

Для множественного наследования MRO может содержать много классов, поэтому, в основном, superвы можете решить, где вы хотите начать поиск в MRO.

dorisxx
источник
0

Рассмотрим следующий код:

class X():
    def __init__(self):
        print("X")

class Y(X):
    def __init__(self):
        # X.__init__(self)
        super(Y, self).__init__()
        print("Y")

class P(X):
    def __init__(self):
        super(P, self).__init__()
        print("P")

class Q(Y, P):
    def __init__(self):
        super(Q, self).__init__()
        print("Q")

Q()

Если изменить конструктор Yна X.__init__, вы получите:

X
Y
Q

Но используя super(Y, self).__init__(), вы получите:

X
P
Y
Q

И Pили Qдаже могут быть задействованы из другого файла, который вы не знаете, когда пишете Xи Y. Так что, по сути, вы не будете знать, на что super(Child, self)будет ссылаться, когда будете писать class Y(X), даже подпись Y так же проста, как и Y(X). Вот почему супер может быть лучшим выбором.

ТТГ
источник