Почему здесь происходит рекурсия?

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

def singleton(class_):
    class class_w(class_):
        _instance = None
        def __new__(class2, *args, **kwargs):
            if class_w._instance is None:
                class_w._instance = super(class_w, class2).__new__(class2, *args, **kwargs)
                class_w._instance._sealed = False
            return class_w._instance
        def __init__(self, *args, **kwargs):
            if self._sealed:
                return
            super(class_w, self).__init__(*args, **kwargs)
            self._sealed = True
    class_w.__name__ = class_.__name__
    return class_w

@singleton
class MyClass(object):
    def __init__(self, text):
        print text
    @classmethod
    def name(class_):
        print class_.__name__

x = MyClass(111)
x.name()
y = MyClass(222)
print id(x) == id(y)

Выход:

111     # the __init__ is called only on the 1st time
MyClass # the __name__ is preserved
True    # this is actually the same instance

Утверждается, что если мы используем super(MyClass, self).__init__(text) внутри __init__ of MyClass, мы попадаем в рекурсию.

Я тестировал и действительно рекурсия. Но, как я понимаю, MyClass наследует object, поэтому super(MyClass, self) должен просто быть object, но оказывается, что super(MyClass, self) есть __main__.MyClass

Не могли бы вы объяснить, что здесь происходит шаг за шагом, чтобы понять причины, по которым происходит рекурсия?

Ответ 1

Проблема заключается в том, что, написав super(MyClass, self).__init__(text), вы говорите, что используется суперсоответствие по отношению к любому классу MyClass, к которому относится в то время, когда вызывается super. Но декоратор заменяет MyClass подклассом самого себя. Поэтому, когда ваш оригинальный метод __init__ называется MyClass, фактически относится к подклассу класса, который определяет исполняемый метод.

Чтобы сказать это шаг за шагом, я собираюсь назвать исходный класс (как написано в источнике) OrigMyClass, и полученную версию (после декоратора) DecMyClass. Я использую MyClass как переменную, потому что ее значение изменяется во время выполнения.

  • Вы определяете метод __init__ на OrigMyClass, но метод __init__ вызывает super(MyClass, self), а не super(OrigMyClass, self). Таким образом, какой метод будет фактически вызван, зависит от того, к чему относится MyClass во время вызова метода. Значение MyClass просматривается во время выполнения, как и любая другая переменная; размещение его внутри вызова super или внутри метода __init__ не магически привязывает его к классу, в котором он находится, когда вы его пишете; переменные в функциях оцениваются, когда они вызывается, а не когда они определены.

  • Декоратор работает. Декоратор определяет новый класс DecMyClass как подкласс OrigMyClass. DecMyClass определяет __init__, который вызывает super(DecMyClass, self).

  • После запуска декоратора имя MyClass привязано к классу DecMyClass. Обратите внимание, что это означает, что при последующем вызове super(MyClass, self) он будет выполнять super(DecMyClass, self).

  • Когда вы выполняете MyClass(111), вы создаете объект DecMyClass. DecMyClass.__init__ вызывает super(DecMyClass, self).__init__. Выполняется OrigMyClass.__init__.

  • OrigMyClass.__init__ вызывает super(MyClass, self).__init__. Поскольку MyClass относится к DecMyClass, это то же самое, что и super(DecMyClass, self).__init__. Но DecMyClass является подклассом OrigMyClass. Ключевым моментом является то, что, поскольку MyClass относится к DecMyClass, OrigMyClass на самом деле вызывает супер в подклассе самого себя.

  • Таким образом, super(DecMyClass, self).__init__ снова вызывает OrigMyClass.__init__, который снова вызывает себя и т.д. до бесконечности.

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

>>> class Super(object):
...     def __init__(self):
...         print "In super init"
...         super(Sub, self).__init__()
>>> class Sub(Super):
...     def __init__(self):
...         print "In sub init"
...         super(Sub, self).__init__()

Обратите внимание, что super вызывает super(Sub, self). Он пытается вызвать метод суперкласса, но он пытается вызвать метод суперкласса Sub. Суперкласс Sub равен super, поэтому super завершает вызов своего собственного метода снова.

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

>>> class Super(object):
...     def __init__(self):
...         print "In super init"
...         super(someClass, self).__init__()
>>> class Sub(Super):
...     def __init__(self):
...         print "In sub init"
...         super(Sub, self).__init__()
>>> someClass = Sub

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