Причина вызова конструктора суперкласса с использованием суперкласса .__ init () __ вместо superclass () - PullRequest
0 голосов
/ 05 июля 2018

Я новичок в Python и использую книгу Лутца, чтобы понять OOPS в Python. Этот вопрос может быть основным, но я был бы признателен за любую помощь. Я исследовал SO и нашел ответы «как», но не «почему».

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

Пример:

class Super:
    def __init__(self,name):
        self.name=name
        print("Name is:",name)

class Sub(Super):
    pass

a = Sub("Harry")
a.name

Над кодом присваивается атрибут name объекту a. Он также печатает name, как и ожидалось.

Однако, если я изменю код как:

class Super:
    def __init__(self,name):
        print("Inside Super __init__")
        self.name=name
        print("Name is:",name)

class Sub(Super):
      def __init__(self,name):
          Super(name) #Call __init__ directly

a = Sub("Harry")
a.name

Приведенный выше код не работает нормально. Хорошо, я имею в виду, что хотя Super.__init__() вызывается (как видно из операторов печати), к a не прикреплен атрибут. Когда я запускаю a.name, я получаю ошибку, AttributeError: 'Sub' object has no attribute 'name'

Я исследовал эту тему в SO и нашел исправление для Цепные вызовы родительских конструкторов в python и Почему методы суперкласса __init__ не вызываются автоматически?

Эти два потока говорят о том, как это исправить, но они не дают причину почему.

Вопрос: Почему мне нужно звонить Super __init__, используя Super.__init__(self, name) ИЛИ super(Sub, self).__init__(name) вместо прямого вызова Super(name)?

В Super.__init__(self, name) и Super(name) мы видим, что __init__() Super вызывается (как видно из операторов print), но только в Super.__init__(self, name) мы видим, что атрибут присоединяется к Sub class.

Не будет ли Super(name) автоматически передавать self (дочерний) объект в Super? Теперь вы можете спросить, откуда мне знать, что self автоматически передается? Если я изменю Super(name) на Super(self,name), я получу сообщение об ошибке TypeError: __init__() takes 2 positional arguments but 3 were given. Как я понял из книги, self автоматически пропускается. Таким образом, по сути, мы заканчиваем прохождение self дважды.

Я не знаю, почему Super(name) не прикрепляет атрибут name к Sub, даже если Super.__init__() запущен. Буду признателен за любую помощь.


Для справки, вот рабочая версия кода, основанная на моих исследованиях SO:

class Super:
    def __init__(self,name):
        print("Inside __init__")
        self.name=name
        print("Name is:",name)

class Sub(Super):
    def __init__(self,name):
        #Super.__init__(self, name) #One way to fix this
        super(Sub, self).__init__(name) #Another way to fix  this

a = Sub("Harry")
a.name

PS: я использую Python-3.6.5 в разделе Anaconda Distribution.

Ответы [ 3 ]

0 голосов
/ 05 июля 2018

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

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

class Rot13Reader:
    def __init__(self, filename):
        self.file = open(filename):
    def close(self):
        self.file.close()
    def dostuff(self):
        line = next(file)
        return codecs.encode(line, 'rot13')

Представьте, что вам нужно все поведение этого класса, но со строкой, а не с файлом. Единственный способ сделать это - пропустить open:

class LocalRot13Reader(Rot13Reader):
    def __init__(self, s):
        # don't call super().__init__, because we don't have a filename to open
        # instead, set up self.file with something else
        self.file = io.StringIO(s)

Здесь мы хотели избежать назначения self.file в суперклассе. В вашем случае, как и почти во всех классах, которые вы когда-либо будете писать, вы не не хотите избегать назначения self.name в суперклассе. Вот почему, хотя Python позволяет вам не вызывать __init__ суперкласса, вы почти всегда вызываете его.

Обратите внимание, что здесь нет ничего особенного в __init__. Например, мы можем переопределить dostuff для вызова версии базового класса, а затем выполнить дополнительные действия:

def dostuff(self):
    result = super().dostuff()
    return result.upper()

… или мы можем переопределить close и намеренно не вызывать базовый класс:

def close(self):
    # do nothing, including no super, because we borrowed our file

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


Вопрос: Зачем мне звонить Super's __init__, используя Super.__init__(self, name) ИЛИ super(Sub, self).__init__(name) вместо прямого звонка Super(name)?

Потому что они делают очень разные вещи.

Super(name) создает новый экземпляр Super, вызывает __init__(name) и возвращает его вам. И тогда вы игнорируете это значение.

В частности, Super.__init__ вызывается один раз в любом случае, но self, с которым он вызывается, - это новый экземпляр Super, который вы просто собираетесь выбросить, в случае Super(name) в то время как это ваше self в super(Sub, self).__init__(name) случае.

Итак, в первом случае он устанавливает атрибут name для какого-либо другого объекта, который выбрасывается, и никто никогда не устанавливает его для вашего объекта, поэтому self.name позже поднимает AttributeError.

Это может помочь вам понять это, если вы добавите что-то в оба метода __init__ класса, чтобы показать, какой экземпляр задействован:

class Super:
    def __init__(self,name):
        print(f"Inside Super __init__ for {self}")
        self.name=name
        print("Name is:",name)

class Sub(Super):
    def __init__(self,name):
        print(f"Inside Sub __init__ for {self}")
        # line you want to experiment with goes here.

Если последняя строка super().__init__(name), super(Sub, self).__init__name) или Super.__init__(self, name), вы увидите что-то вроде этого:

Inside Sub __init__ for <__main__.Sub object at 0x10f7a9e80>
Inside Super __init__ for <__main__.Sub object at 0x10f7a9e80>

Обратите внимание, что это один и тот же объект, Sub по адресу 0x10f7a9e80, в обоих случаях.

… но если последняя строка Super(name):

Inside Sub __init__ for <__main__.Sub object at 0x10f7a9ea0>
Inside Super __init__ for <__main__.Super object at 0x10f7a9ec0>

Теперь у нас есть два разных объекта с разными адресами 0x10f7a9ea0 и 0x10f7a9ec0 и с разными типами.


Если вам интересно, как выглядит магия под одеялом, Super(name) делает что-то вроде этого (немного упрощает и пропускает некоторые шаги 1 ):

_newobj = Super.__new__(Super)
if isinstance(_newobj, Super):
    Super.__init__(_newobj, name)

… пока super(Sub, self).__init__(name) делает что-то вроде этого:

_basecls = magically_find_next_class_in_mro(Sub)
_basecls.__init__(self, name)

В качестве примечания: если книга говорит вам использовать super(Sub, self).__init__(name) или Super.__init__(self, name), возможно, это устаревшая книга, написанная для Python 2.

В Python 3 вы просто делаете это:

  • super().__init__(name): Вызывает правильный следующий суперкласс по порядку разрешения метода. Вы почти всегда этого хотите.
  • super(Sub, self).__init__(name): Вызывает правильный следующий суперкласс - если вы не ошиблись и не ошиблись Sub. Это необходимо только в том случае, если вы пишете код с двумя версиями, который должен работать в 2.7 и 3.x.
  • Super.__init__(self, name): Вызывает Super, будь то правильный следующий суперкласс или нет. Это необходимо только в том случае, если порядок разрешения метода неправильный, и вам нужно его обойти. 2

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

оригинальное введение в super, __new__ и все связанные с ним функции очень помогло мне понять все это. Я не уверен, будет ли это полезным для кого-то, кто не знаком с уже понимающими классы Python в старом стиле, но он довольно хорошо написан, и Гвидо (очевидно) знает, о чем говорит, так что, возможно, стоит прочитать .


1. Самый большой обман в этом объяснении состоит в том, что super на самом деле возвращает прокси-объект, который действует как _baseclass, связанный с self, так же, как связаны методы, которые могут использоваться для привязки методов, таких как __init__. Это полезное / интересное знание, если вы знаете, как работают методы, но, возможно, просто лишняя путаница, если вы этого не сделаете.

2. ... или если вы работаете с классами старого стиля, которые не поддерживают super (или правильный порядок разрешения методов). Это никогда не происходит в Python 3, который не имеет классов в старом стиле. Но, к сожалению, вы увидите это во многих примерах tkinter, потому что лучшим учебником по-прежнему является Effbot, написанный для Python 2.3, когда Tkinter был классами старого стиля и никогда не обновлялся.

0 голосов
/ 05 июля 2018

Super(name) создает новый экземпляр супер. Подумайте об этом примере:

def __init__(self, name):
    x1 = Super(name)
    x2 = Super("some other name")
    assert x1 is not self
    assert x2 is not self

Чтобы явно вызвать конструктор Super в текущем экземпляре, вам необходимо использовать следующий синтаксис:

def __init__(self, name):
    Super.__init__(self, name)

Теперь, может быть, вы не хотите читать дальше, если вы новичок.

Если вы это сделаете, вы увидите, что есть веская причина использовать super(Sub, self).__init__(name) (или super().__init__(name) в Python 3) вместо Super.__init__(self, name).


Super.__init__(self, name) работает нормально, если вы уверены, что Super на самом деле ваш суперкласс. Но на самом деле, вы этого точно не знаете.

Вы могли бы иметь следующий код:

class Super:
    def __init__(self):
        print('Super __init__')

class Sub(Super):
    def __init__(self):
        print('Sub __init__')
        Super.__init__(self)

class Sub2(Super):
    def __init__(self):
        print('Sub2 __init__')
        Super.__init__(self)

class SubSub(Sub, Sub2):
    pass

Теперь вы ожидаете, что SubSub() завершит вызов всех вышеупомянутых конструкторов, но это не так:

>>> x = SubSub()
Sub __init__
Super __init__
>>>

Чтобы исправить это, вам нужно будет сделать:

class Super:
    def __init__(self):
        print('Super __init__')

class Sub(Super):
    def __init__(self):
        print('Sub __init__')
        super().__init__()

class Sub2(Super):
    def __init__(self):
        print('Sub2 __init__')
        super().__init__()

class SubSub(Sub, Sub2):
    pass

Теперь это работает:

>>> x = SubSub()
Sub __init__
Sub2 __init__
Super __init__
>>>

Причина в том, что суперкласс Sub объявлен равным Super, в случае множественного наследования в классе SubSub MRO Python устанавливает наследование следующим образом: SubSub наследуется от Sub, что наследуется от Sub2, который наследуется от Super, который наследуется от object.

Вы можете проверить это:

>>> SubSub.__mro__
(<class '__main__.SubSub'>, <class '__main__.Sub'>, <class '__main__.Sub2'>, <class '__main__.Super'>, <class 'object'>)

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

См. https://www.python.org/download/releases/2.3/mro/

0 голосов
/ 05 июля 2018

Super(name) не является "прямым вызовом" суперкласса __init__. В конце концов, вы позвонили Super, а не Super.__init__.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...