Инстанцирование суперкласса Python 3 через конструктор по умолчанию производного класса

Вопрос или проблема

Инстанцирование суперкласса 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 подразумевает следующее:

  1. Вызов B("Hi") вызывает B.__new__, который создает экземпляр.
  2. Поскольку B.__init__ не определен, вызывается A.__init__, которому передаются токены.
  3. Параметры передаются правильно благодаря методу разрешения порядка (MRO).
  4. Метод __str__ в B использует super() для вызова родительского метода.

Эти механизмы делают Python мощным и гибким языком объектно-ориентированного программирования, позволяя эффективно использовать наследование.

Оцените материал
Добавить комментарий

Капча загружается...