Что делает "супер" в Python?
Какая разница между:
class Child(SomeBaseClass):
def __init__(self):
super(Child, self).__init__()
и
class Child(SomeBaseClass):
def __init__(self):
SomeBaseClass.__init__(self)
Я видел, что super
используется довольно много в классах с единственным одиночным наследованием. Я могу понять, почему вы используете его в нескольких наследованиях, но неясно, в чем преимущества его использования в такой ситуации.
Ответы
Ответ 1
Преимущества super()
в одиночном наследовании минимальны - в основном вам не нужно жестко закодировать имя базового класса в каждом методе, который использует его родительские методы.
Однако почти невозможно использовать множественное наследование без super()
. Это включает в себя общие идиомы, такие как mixins, интерфейсы, абстрактные классы и т.д. Это распространяется на код, который позже расширяет ваши. Если кто-то позже захотел написать класс, который расширил Child
и mixin, их код не будет работать должным образом.
Ответ 2
Какая разница?
SomeBaseClass.__init__(self)
означает вызвать SomeBaseClass
__init__
. в то время как
super(Child, self).__init__()
означает вызов связанного __init__
из родительского класса, который следует за Child
в экземпляре Order Resolution Order Order (MRO).
Если экземпляр является подклассом Child, то в MRO может быть другой родитель.
Объяснил просто
Когда вы пишете класс, вы хотите, чтобы другие классы могли использовать его. super()
позволяет другим классам использовать класс, который вы пишете.
Как говорит Боб Мартин, хорошая архитектура позволяет вам откладывать принятие решений как можно дольше.
super()
может включить такую архитектуру.
Когда другой класс наследует класс, который вы написали, он также может наследоваться от других классов. И эти классы могут иметь __init__
который следует за этим __init__
зависимости от порядка классов для разрешения методов.
Без super
вы, вероятно, жестко закодировали бы родительский класс класса, который вы пишете (как в примере). Это будет означать, что вы не будете вызывать следующий __init__
в MRO, и, следовательно, вы не сможете повторно использовать код в нем.
Если вы пишете свой собственный код для личного использования, вас может не волновать это различие. Но если вы хотите, чтобы другие использовали ваш код, использование super
- это то, что обеспечивает большую гибкость для пользователей кода.
Питон 2 против 3
Это работает в Python 2 и 3:
super(Child, self).__init__()
Это работает только в Python 3:
super().__init__()
Он работает без аргументов, перемещаясь вверх по фрейму стека и получая первый аргумент метода (обычно self
для метода экземпляра или cls
для метода класса - но могут быть и другие имена) и находя класс (например, Child
) в свободные переменные (__class__
с именем __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
, он должен был бы
- получить Mro от типа экземпляра
- ищите тип, который определяет метод
- найти следующий тип с помощью метода
- связать этот метод и вызвать его с ожидаемыми аргументами
У UnsuperChild
не должно быть доступа к InjectMe
. Почему не вывод "Всегда избегайте использования super
"? Что мне здесь не хватает?
UnsuperChild
не имеет доступа к InjectMe
. Это UnsuperInjector
который имеет доступ к InjectMe
- и все же не может вызвать этот метод класса из метода, который он наследует от UnsuperChild
.
Оба дочерних класса намереваются вызвать метод с тем же именем, которое будет следующим в MRO, что может быть другим классом, о котором он не знал, когда создавался.
Тот, у которого нет super
жесткого кода, является родительским методом - таким образом, он ограничил поведение своего метода, и подклассы не могут вводить функциональность в цепочку вызовов.
Тот, у кого super
обладает большей гибкостью. Цепочка вызовов для методов может быть перехвачена, и функциональность может быть введена.
Вам может не понадобиться эта функциональность, но могут быть подклассы вашего кода.
Заключение
Всегда используйте super
для ссылки на родительский класс, а не жестко его кодируйте.
Вы намереваетесь сослаться на следующий родительский класс, а не на тот, от которого унаследовал дочерний класс.
Если вы не используете super
может наложить ненужные ограничения на пользователей вашего кода.
Ответ 3
Не все ли это предполагают, что базовый класс является классом нового стиля?
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
должен быть новым, i.e: class A(object)
Ответ 4
Я немного поиграл с 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
Для дополнительного чтения я бы рекомендовал следующие ответы:
Ответ 5
При вызове super()
для разрешения родительской версии метода класса, метода экземпляра или staticmethod мы хотим передать текущий класс, объем которого мы находимся в качестве первого аргумента, чтобы указать, какую родительскую область мы пытаемся для разрешения и в качестве второго аргумента объект, представляющий интерес для указания того, к какому объекту мы пытаемся применить эту область.
Рассмотрим иерархию классов A
, B
и C
, где каждый класс является родителем следующего за ним, и A
, B
и C
соответствующих экземпляров каждого.
super(B, b)
# resolves to the scope of B 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 parent i.e. B
# and applies that scope to c
super(B, c)
# resolves to the scope of B 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 как classmethod, а скорее статический метод. То есть ссылка на класс должна быть явно передана в качестве первого аргумента при непосредственном вызове __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()
, чтобы перейти к родительскому классу, мы передаем дочерний класс A
в качестве своего первого аргумента, затем передаем ссылку на интересующий объект, в этом случае это ссылка на класс, которая была передана когда был вызван A.__new__(cls)
. В большинстве случаев это также ссылка на дочерний класс. В некоторых ситуациях это может быть не так, например, в случае наследования с несколькими поколениями.
super(A, cls)
3, так как в качестве общего правила __new__()
является статическим методом, super(A, cls).__new__
также возвращает статический метод и должен явно указывать все аргументы, включая ссылку на объект insterest, в данном случае 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
class Child(SomeBaseClass):
def __init__(self):
SomeBaseClass.__init__(self)
Это довольно легко понять.
class Child(SomeBaseClass):
def __init__(self):
super(Child, self).__init__()
Хорошо, что происходит сейчас, если вы используете super(Child,self)
?
Когда создается экземпляр Child, его MRO (Порядок разрешения методов) имеет порядок (Child, SomeBaseClass, object) в зависимости от наследования. (предположим, что SomeBaseClass не имеет других родителей, кроме объекта по умолчанию)
Передавая Child, self
, super
ищет в MRO экземпляра self
и возвращает прокси-объект, следующий за Child, в данном случае это SomeBaseClass, этот объект затем вызывает метод __init__
SomeBaseClass. Другими словами, если это super(SomeBaseClass,self)
, прокси-объект, который super
возвращает, будет object
Для множественного наследования MRO может содержать много классов, так что в основном super
позволяет вам решить, где вы хотите начать поиск в MRO.
Ответ 7
здесь есть несколько хороших ответов, но они не касаются того, как использовать super()
в случае, когда разные классы в иерархии имеют разные подписи... особенно в случае __init__
чтобы ответить на эту часть и иметь возможность эффективно использовать super()
я бы предложил прочитать мой ответ super() и изменить сигнатуру кооперативных методов.
вот только решение этого сценария:
- классы верхнего уровня в вашей иерархии должны наследоваться от пользовательского класса, такого как
SuperObject
: - если классы могут принимать разные аргументы, всегда передавайте все полученные аргументы суперфункции в качестве аргументов с ключевыми словами и всегда принимайте
**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
Ответ 8
Много отличных ответов, но для визуальных учеников: сначала давайте рассмотрим с аргументами супер, а затем без.
Представьте себе экземпляр jack
созданный из класса Jack
, у которого есть цепочка наследования, как показано зеленым на рисунке. Вызов:
super(Jack, jack).method(...)
будет использовать MRO (Порядок разрешения методов) jack
(его дерево наследования в определенном порядке) и начнет поиск с Jack
. Почему можно предоставить родительский класс? Хорошо, если мы начнем поиск с экземпляра jack
, он найдет метод instance, и все дело в том, чтобы найти его родительский метод.
Если кто-то не предоставляет аргументы для super, он, как и первый передаваемый аргумент, является классом self
, а второй передаваемый аргумент является self
. Они автоматически рассчитываются для вас в Python3.
Однако, скажем, мы не хотим использовать метод Jack
, вместо того, чтобы передавать Jack
, мы могли бы перейти в Jen
чтобы начать поиск метода от Jen
вверх.
Он выполняет поиск по одному слою за раз (ширина, а не глубина), например, если у Adam
и Sue
есть требуемый метод, сначала будет найден слой из Sue
.
Если у Cain
и Sue
есть требуемый метод, метод Cain
будет вызван первым. Это соответствует в коде:
Class Jen(Cain, Sue):
MRO слева направо.