Вопрос или проблема
Инстанцирование суперкласса Python 3 через конструктор по умолчанию производного класса
В этом коде:
class A():
def __init__(self, x):
self.x = x
def __str__(self):
return self.x
class B(A):
def __str__(self):
return super().__str__()
b = B("Привет")
print(b)
Вывод: Привет
.
Что происходит “под капотом”? Как конструктор по умолчанию в производном классе вызывает конструктор суперкласса? Как параметры, передаваемые объекту производного класса, сопоставляются с параметрами суперкласса?
Как конструктор по умолчанию в производном классе вызывает конструктор суперкласса?
Вы не переопределили его в B
, поэтому вы унаследовали его от A
. Для этого и нужно наследование.
>>> B.__init__ is A.__init__
True
В том же духе вы могли бы вообще не определять B.__str__
, поскольку он ничего не делает (кроме добавления бесполезного дополнительного фрейма в стек вызовов).
Как параметры, передаваемые объекту производного класса, сопоставляются с параметрами суперкласса?
Вы, возможно, слишком много об этом думаете. Как показано выше, B.__init__
и A.__init__
идентичны. B.__init__
определяется в пространстве имен A
, так как он отсутствует в пространстве имен B
.
>>> B.__mro__
(__main__.B, __main__.A, object)
>>> A.__dict__["__init__"]
<function __main__.A.__init__(self, x)>
>>> B.__dict__["__init__"]
...
# KeyError: '__init__'
Что происходит “под капотом”?
Обратите внимание, что методы __init__
не являются конструкторами. Если вы ищете аналог конструкторов в других языках программирования, метод __new__
может быть более подходящим, чем __init__
. Сначала будет создан экземпляр B
(с помощью __new__
), а затем этот экземпляр будет передан в качестве первого позиционного аргумента self
в A.__init__
, вместе со строковым значением “Привет” для второго позиционного аргумента x
.
Первое, что значит вызвать B
в первую очередь? B
является экземпляром type
, и экземпляры можно вызывать, когда их классы определяют __call__
. type.__call__
можно рассматривать как определяемый следующим образом:
def __call__(self, *args, **kwargs):
obj = self.__new__(self, *args, **kwargs)
if isinstance(obj, self):
obj.__init__(*args, **kwargs)
return obj
Таким образом, B("привет")
начинается как вызов type.__call__(B, "привет")
.
Внутри __call__
мы сначала пытаемся вызвать B.__new__(B, "привет")
. Поскольку B.__new__
не определен, нам нужно посмотреть на порядок разрешения методов B
, чтобы найти класс, который определяет __new__
.
>>> B.__mro___
(<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
A
не определяет его, но object
определяет, поэтому мы заканчиваем с
obj = object.__new__(B, "привет")
что фактически игнорирует "привет"
и возвращает “обнаженный” экземпляр B
(первый аргумент). *
Поскольку obj
действительно является экземпляром B
, isinstance
возвращает True
, и мы вызываем метод __init__
. Снова, у obj
нет атрибута __init__
, поэтому мы обращаемся к порядку разрешения методов type(obj)
, чтобы найти его, и мы находим A.__init__
. Таким образом, мы выполняем A.__init__(obj, "привет")
перед тем, как наконец вернуть obj
.
* Есть небольшое количество “магии”, о которой я не знаю. object.__new__
может вызвать TypeError
, если он получает аргументы, которые не ожидает:
>>> object.__new__(object, "привет")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object() takes no arguments
поэтому я не уверен, как именно object.__new__(B, "привет")
работает. Насколько я понимаю, наличие метода __init__
для первого аргумента достаточно для того, чтобы object.__new__
принял и проигнорировал произвольные аргументы.
Ответ или решение
Вопрос о том, как происходит инстанцирование родительского класса через конструктор производного класса в Python, можно рассмотреть более подробно.
Изучение кода
Начнем с представленного кода:
class A():
def __init__(self, x):
self.x = x
def __str__(self):
return self.x
class B(A):
def __str__(self):
return super().__str__()
b = B("Hi")
print(b)
Когда мы создаем экземпляр класса B
с помощью b = B("Hi")
, и затем печатаем b
, выводится Hi
. Давайте разберем, что происходит "под капотом".
Как вызывается конструктор родительского класса?
Поскольку метод __init__
в классе B
не определен, он унаследован от родителя A
. В Python, если метод не найден в текущем классе, интерпретатор будет искать его в родительских классах в порядке, установленном с помощью метода разрешения порядка (MRO).
Для классов B
и A
MRO таков:
B.__mro__ # возвращает (<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
Таким образом, вызов B("Hi")
в первую очередь пытается вызвать B.__init__
, но поскольку его нет, метод разрешения порядка находит A.__init__
и использует его.
Как параметры передаются между классами?
Параметры, переданные конструктору производного класса, передаются также в родительский класс. В случае с B("Hi")
, сначала создается экземпляр B
, затем вызывается A.__init__(self, x)
, где self
— это экземпляр B
и x
равен "Hi"
.
Python использует метод __call__
для создания экземпляра класса:
def __call__(self, *args, **kwargs):
obj = self.__new__(self, *args, **kwargs)
if isinstance(obj, self):
obj.__init__(*args, **kwargs)
return obj
-
Для начала происходит вызов
B.__new__(B, "Hi")
. Поскольку метод__new__
не определен вB
, Python ищет его в MRO, и тем самым находит его в классеobject
. -
object.__new__(B, "Hi")
создает "пустой" экземпляр классаB
, игнорируя аргументы. -
Затем идет проверка
isinstance
, которая возвращаетTrue
, и, следовательно, вызывается метод__init__
. Поскольку метод__init__
не найден в классеB
, Python снова обращается к MRO и находитA.__init__
. - Таким образом, фактически выполняется
A.__init__(obj, "Hi")
, гдеobj
— это экземплярB
, а"Hi"
— это аргумент, переданный в конструктор.
Что происходит в __str__
?
Когда мы вызываем print(b)
, выполняется метод __str__
. В классе B
переопределен метод __str__
, который вызывает super().__str__()
, фактически ссылаясь на метод __str__
родительского класса A
. Это позволяет получить значение self.x
, которое было установлено раннее в A.__init__
.
Заключение
Обобщая, процесс инстанцирования класса в Python подразумевает следующее:
- Вызов
B("Hi")
вызываетB.__new__
, который создает экземпляр. - Поскольку
B.__init__
не определен, вызываетсяA.__init__
, которому передаются токены. - Параметры передаются правильно благодаря методу разрешения порядка (MRO).
- Метод
__str__
вB
используетsuper()
для вызова родительского метода.
Эти механизмы делают Python мощным и гибким языком объектно-ориентированного программирования, позволяя эффективно использовать наследование.