Вызов super() внутри метода класса класса для доступа к методу метакласса

Как только я понял метаклассы...

Отказ от ответственности: я поискал ответ перед публикацией, но большинство ответов, которые я нашел, касаются вызова super() для получения другого @classmethod в MRO (без участия метакласса) или, что удивительно, много из них были попытки сделать что-то в metaclass.__new__ или metaclass.__call__, что означало, что класс еще не был полностью создан. Я почти уверен (скажем, на 97%), что это не одна из тех проблем.


Среда: Python 3.7.2


Проблема: у меня есть метакласс FooMeta, который определяет метод get_foo(cls), класс Foo, созданный на основе этого метакласса (то есть экземпляр FooMeta) и имеющий @classmethod get_bar(cls). Затем другой класс Foo2, наследуемый от Foo. В Foo2 я создаю подкласс get_foo, объявляя его @classmethod и вызывая super(). Это с треском проваливается...

то есть с этим кодом

class FooMeta(type):
    def get_foo(cls):
        return 5


class Foo(metaclass=FooMeta):
    @classmethod
    def get_bar(cls):
        return 3


print(Foo.get_foo)
# >>> <bound method FooMeta.get_foo of <class '__main__.Foo'>>
print(Foo.get_bar)
# >>> <bound method Foo.get_bar of <class '__main__.Foo'>>


class Foo2(Foo):
    @classmethod
    def get_foo(cls):
        print(cls.__mro__)
        # >>> (<class '__main__.Foo2'>, <class '__main__.Foo'>, <class 'object'>)
        return super().get_foo()

    @classmethod
    def get_bar(cls):
        return super().get_bar()


print(Foo2().get_bar())
# >>> 3
print(Foo2().get_foo())
# >>> AttributeError: 'super' object has no attribute 'get_foo'


Вопрос: Итак, поскольку мой класс является экземпляром метакласса и проверено, что оба метода класса существуют в классе Foo, почему оба вызова super().get_***() не работают внутри Foo2? Что я не понимаю в метаклассах или super(), что мешает мне найти эти результаты логичными?

РЕДАКТИРОВАТЬ: Дальнейшее тестирование показывает, что методы Foo2, являющиеся методами класса или методами экземпляра, не меняют результат.

РЕДАКТИРОВАТЬ 2: Благодаря ответу @chepner, я думаю, проблема заключалась в том, что super() возвращал суперобъект, представляющий Foo (это проверено с помощью super().__thisclass__), и я ожидал, что super().get_foo() будет вести себя (возможно, даже вызывать) get_attr(Foo, 'get_foo') за кулисами. Вроде нет... До сих пор удивляюсь, почему, но становится все яснее :)


person takeshi2010    schedule 05.11.2019    source источник
comment
Самый прямой путь будет return FooMeta.get_foo(cls) + 1, но возникает вопрос - метаклассы предназначены для настройки создания класса. Вы этого не делаете. Итак, почему вы используете метакласс? super больше касается обхода MRO (родительских или родственных классов при использовании наследования), поэтому я не совсем уверен, почему вы пытаетесь использовать его, чтобы получить метод метакласса в первую очередь, это совершенно не связанные проблемы.   -  person wim    schedule 05.11.2019
comment
@wim Я сделал код максимально простым, чтобы проиллюстрировать проблему. Очевидно, что существует тысяча различных и более простых способов написать эту логику без метаклассов, но это не мой вопрос (TBH, фактический код, который заставил меня задать себе вопрос, и ТАК, что этот вопрос уже поставлен в более простом форма, но все же с мкс, потому что она была нужна). Мой вопрос скорее теоретический, чем практический (см. последнее предложение исходного поста). Благодаря этому я пытаюсь лучше понять, как работает Python.   -  person takeshi2010    schedule 05.11.2019
comment
Чем больше я тестирую, тем больше я думаю, что виновато мое понимание того, как работает super()...   -  person takeshi2010    schedule 05.11.2019
comment
С вашим пониманием пока все в порядке, но мне не совсем понятно, почему вы вообще ожидаете, что super(FooMeta, Foo) будет проксировать существующий метод get_foo? Потому что, в отсутствие чего-либо еще в цепочке наследования, следующим методом здесь будет просто попытка разрешения метода на type. Если вы хотите получить доступ к методу метакласса get_foo, вы просто используете FooMeta.get_foo напрямую.   -  person wim    schedule 05.11.2019
comment
@wim Я обновил пример и вопрос. Я проводил больше тестов со своей стороны, поэтому, не найдя фактического ответа, я надеюсь, что мне удалось сузить вопрос до его сути.   -  person takeshi2010    schedule 05.11.2019
comment
У меня нет фактического ответа, но я отмечу как интересное (по крайней мере), что get_foo не появляется в возвращаемом значении dir(Foo). (На самом деле это просто еще одно указание на то, что super().get_foo потерпит неудачу, а не объяснение того, почему она потерпит неудачу).   -  person chepner    schedule 05.11.2019
comment
@chepner: Это действительно просто потому, что dir предназначен для интерактивного удобства, а не для согласованности. Цитируя документы: удобство использования в интерактивной подсказке, он пытается предоставить интересный набор имен больше, чем он пытается предоставить строго или последовательно определенный набор имен, и его подробное поведение может меняться в разных выпусках. Например, атрибуты метакласса отсутствуют в списке результатов, если аргументом является класс.   -  person user2357112 supports Monica    schedule 05.11.2019


Ответы (4)


Foo может иметь метод get_foo, но super не предназначен для проверки атрибутов суперкласса. super важно, какие атрибуты происходят в суперклассе.


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

class A:
    @classmethod
    def f(cls):
        return 1
class B(A):
    pass
class C(A):
    @classmethod
    def f(cls):
        return 2
class D(B, C):
    @classmethod
    def f(cls):
        return super().f() + 1

Все A, B, C и D имеют метод класса f, но f класса B унаследован от порядка разрешения методов A. D, последовательность классов, проверенных для поиска атрибутов, становится (D, B, C, A, object).

super().f() + 1 ищет в MRO cls реализацию f. Он должен найти C.f, но B имеет унаследованную реализацию f, и B находится перед C в MRO. Если бы super забрал B.f, это разрушило бы попытку C переопределить f в ситуации, обычно называемой «алмазной проблемой».

Вместо того, чтобы смотреть, какие атрибуты имеет B, super смотрит непосредственно в __dict__ B, поэтому он рассматривает только атрибуты, фактически предоставленные B, а не суперклассами или метаклассами B.


Теперь вернемся к вашей ситуации get_foo/get_bar. get_bar происходит от самого Foo, поэтому super().get_bar() находит Foo.get_bar. Однако get_foo предоставляется не Foo, а метаклассом FooMeta, и в Foo.__dict__ нет записи для get_foo. Таким образом, super().get_foo() ничего не находит.

person user2357112 supports Monica    schedule 05.11.2019
comment
Спасибо !!! Это был ахах! момент для меня :) Ваш пример с множественным наследованием совершенно ясно показывает, почему super().get_foo() не может просто сделать getattr(Foo, 'get_foo') за кулисами. Спасибо :) - person takeshi2010; 05.11.2019
comment
Это отличный ответ. Я просматривал исходный код CPython, чтобы узнать, дает ли реализация super какое-либо объяснение того, почему он работает именно так (нет). Если вам интересно, вы можете найти код C для того, что эффективно super.__getattribute__ здесь. - person Blckknght; 05.11.2019

get_foo не является атрибутом Foo, а является атрибутом type(Foo):

>>> 'get_foo' in Foo.__dict__
False
>>> 'get_foo' in type(Foo).__dict__
True

Таким образом, в то время как Foo.get_foo разрешается в type(Foo).get_foo, super().get_foo нет, потому что прокси, возвращаемый super(), похож на Foo, но не является самим Foo.

person chepner    schedule 05.11.2019
comment
Этот ответ, я думаю, требует более глубокого погружения в протокол дескриптора, но у меня нет на это времени. - person chepner; 05.11.2019
comment
Спасибо, я думаю, что это на самом деле шаг ближе к ответу на вопрос. Возможно, просто get_attr в суперобъекте не совпадает с get_attr в объекте, представленном super_object.__thisclass__. Меня все еще беспокоит асимметрия (в конце концов, это оба метода Foo). - person takeshi2010; 05.11.2019
comment
@chepner Здесь нет дескрипторов с __get__ и __set__. @classmethod — это декоратор, а не дескриптор. - person Samuel Muldoon; 05.11.2019
comment
@SamuelMuldoon: объекты, возвращаемые декоратором classmethod, на самом деле являются дескрипторами. Они не могли выполнять свою функциональность, не будучи дескрипторами. - person user2357112 supports Monica; 05.11.2019
comment
Вся цель типа classmethod состоит в том, чтобы предоставить объекту другую реализацию __get__. - person chepner; 05.11.2019

Примечание 1

Просто быть чистым:

  • Foo не наследует от FooMeta.
  • FooMeta не надкласс Foo

super не получится.

Заметка 2

Теперь это примечание (1) не по пути, если вы хотите получить доступ к методу метакласса изнутри метода экземпляра метакласса, вы можете сделать это следующим образом:

class FooMeta(type):
    _foo = 5

    def get_foo(cls):
        print("`get_foo` from `FooMeta` was called!")

    class Foo(metaclass=FooMeta):

        @classmethod
        def bar(Foo):
            FooMeta = type(Foo)
            FooMeta_dot_getfoo = FooMeta.get_foo
            FooMeta_dot_getfoo(Foo)

        def baz(self):
            Foo = type(self)
            FooMeta = type(Foo)
            FooMeta_dot_getfoo = FooMeta.get_foo
            FooMeta_dot_getfoo(Foo)

    Foo.bar()
    foo = Foo()
    foo.baz()

Результат:

`get_foo` from `FooMeta` was called!
`get_foo` from `FooMeta` was called!

Заметка 3

Если у вас есть метод класса с тем же именем, что и у метода в метаклассе, почему метод метакласса НЕ вызывается? Рассмотрим следующий код:

class FooMeta(type):
    def get_foo(cls):
        print("META!")

class Foo(metaclass=FooMeta):
    @classmethod
    def get_foo(cls):
        print("NOT META!")

Foo.get_foo()

Результатом является NOT META!. В следующем обсуждении предположим, что:

  • foo является экземпляром Foo
  • Foo является экземпляром FooMeta

Впервые в этом посте у меня будет псевдокод, а не питон. Не пытайтесь запустить следующее. __getattribute__ выглядит следующим образом:

class FooMeta(type):
    def get_foo(Foo):
        print("META!")

class Foo(metaclass=FooMeta):
    @classmethod
    def get_foo(Foo):
        print("NOT META!")

    def __getattribute__(foo, the string "get_foo"):
        try:
            attribute = "get_foo" from instance foo
        except AttributeError:
            attribute = "get_foo" from class Foo

        # Begin code for handling "descriptors"
        if hasattr(attribute, '__get__'):
            attr = attribute.__get__(None, Foo)
        # End code for handling "descriptors"

        return attribute

foo = Foo()
foo.get_foo() # prints "NOT META!"

get_foo = Foo.__getattribute__(foo, "get_foo")
get_foo.__call__()

На самом деле вы можете игнорировать то, что говорит «code for handling "descriptors"». Я включил это только для полноты картины.

Обратите внимание, что __getattribute__ нигде не говорит: «получить get_foo из метакласса».

  1. Во-первых, мы пытаемся получить get_foo из экземпляра. Возможно, get_foo является переменной-членом. Возможно, у одного экземпляра get_foo = 1, а у другого экземпляра get_foo = 5 Компьютер не знает. Компьютер тупой.
  2. Компьютер понимает, что у экземпляра нет переменной-члена с именем get_foo. Затем он говорит: «Ах-ха! Держу пари, что get_foo принадлежит к КЛАССУ». Итак, он выглядит там, и о чудо, вот оно: Foo имеет атрибут с именем get_foo. FooMeta также имеет атрибут get_foo, но кого это волнует.

На что следует обратить внимание, так это на то, что:

  • Foo имеет атрибут с именем get_foo
  • MetaFoo имеет атрибут с именем get_foo

Они оба имеют атрибуты с именами get_foo, но Foo и MetaFoo — это разные объекты. Это не так, как если бы два get_foo были общими. Я могу иметь obj1.x = 1 и obj2.x = 99. Без проблем.

FooMeta имеет собственный метод __getattribute__. Раньше я говорил о Foo.__getattribute__, а теперь давайте поговорим о MeTa __getattribute__

class FooMeta(type):
    def get_foo(Foo):
        print("META!")

    def __getattribute__(Foo, the string "get_foo"):
        try:                                            # LINE 1
            attribute = "get_foo" from class Foo        # LINE 2
        except AttributeError:                          # LINE 3
            attribute = "get_foo" from class FooMeta    # LINE 4
                                                        # LINE 5
        # Begin code for handling "descriptors"
        if hasattr(attribute, '__get__'):
            attr = attribute.__get__(None, Foo)
        # End code for handling "descriptors"

        return attribute

class Foo(metaclass=FooMeta):
    @classmethod
    def get_foo(Foo):
        print("NOT META!")

Foo.get_foo()

get_foo = FooMeta.__getattribute__(Foo, "get_foo")
get_foo.__call__() 

Порядок событий:

  • Строки 1 и 2 случаются
  • Строки 3, 4 и 5 не выполняются
  • Вы можете игнорировать информацию об дескрипторах, потому что ни один из разных get_foo в этой задаче не имеет метода __get__.

Хорошо сейчас! Почему только 1 и 2 строки? Потому что ты сделал @classmethod глупым! Мы проверяем Foo, есть ли у него get_foo, и он есть! Зачем проверять атрибуты класса, если мы сначала находим атрибут экземпляра? Мы всегда проверяем, принадлежит ли атрибут экземпляру (Foo) first-and-first, прежде чем проверять, может быть, существует только одна копия статической переменной-члена. принадлежащий классу (FooMeta) и общий для всех экземпляров.

Обратите внимание, что если у Foo нет get_foo, то FooMeta.__getattribute__(Foo, "get_foo") вернет get_foo из метакласса, потому что первая попытка (получение его из экземпляра) не удалась. Вы как бы заблокировали эту опцию, дав экземпляру что-то с тем же именем, что и статическая переменная-член класса.

class K:

    im_supposed_to_be_shared = 1

    def __init__(self, x):
        # NOPE!
        self.im_supposed_to_be_shared = x
        # maybe try type(self)

obj1 = K(14)
obj2 = K(29)
print(obj1.im_supposed_to_be_shared)
print(obj2.im_supposed_to_be_shared)
print(K.im_supposed_to_be_shared)

Отпечатки:

14
29
1

НЕ печатает:

29
29
29

Обратите внимание, что если вы хотите установить статическую переменную-член класса, instance.mem_var = 5 — это очень ⱽᵉᴿʸ плохая идея. Вы дадите instance новую переменную-член, а статическая (общая) переменная-член класса будет затенена. Вы можете исправить это примерно так:

def __setattr__(self, attr_name, attr_val):
    if hasattr(type(self), attr_name):
        setattr(type(self), attr_name, attr_val)
    else:
        super_class = inspect.getmro(type(self))[1]
        super_class.__setattr__(self, attr_name, attr_val)

Тогда ваша компиляция напечатает:

29
29
29

Примечание 4

class Foo:
    @classmethod
    def funky(cls):
        pass

НЕ MetaClass.funky = funky. Вместо этого это:

def funky(cls)
   pass
Funky = classmethod (funky)

... что почти то же самое, что:

def funky(cls):
     pass
funky = lambda self, *args, **kwargs: funky(type(self), *args, **kwargs)

Мораль истории note 4 заключается в том, что classmethod funky является атрибутом Foo, а не атрибутом FooMeta.

person Samuel Muldoon    schedule 05.11.2019
comment
Кажется, вы нигде в этом посте не обращались к super, что довольно странно для ответа, опубликованного на вопрос о super. - person user2357112 supports Monica; 05.11.2019
comment
@user2357112 user2357112 Вы не можете использовать super для получения метакласса, потому что класс не наследуется от метакласса. В своем ответе я объясняю, что вы можете использовать type(class) вместо super() - person Samuel Muldoon; 05.11.2019
comment
Что касается вашего псевдокода доступа к атрибутам в примечании 3, обратите внимание, что дескрипторы с __set__ или __delete__ (дескрипторы данных) имеют приоритет, даже если атрибут существует и в __dict__ экземпляра. - person jsbueno; 06.11.2019

obj.attr звонит type(obj).__getattribute__(obj, 'attr'), т.е.

  • а) object.__getattribute__(obj, 'attr'), который ищет 'attr' в самом obj, а также в классе obj и его родителей; или
  • б) type.__getattribute__(obj, 'attr'), если obj является экземпляром type, который ищет 'attr' в самом obj и его родителях, а также в классе obj и его родителей; или
  • c) super.__getattribute__(obj, 'attr') if obj is a super instance,
    • c.1) which looks up 'attr' in the class of instance and its parents past cls, if obj is super(cls, instance), or
    • c.2), который ищет 'attr' в самом subclass и его родителях после cls, если obj равно super(cls, subclass).

Когда вы вызываете super().get_foo() в методе класса Foo2.get_foo, что эквивалентно вызову super(Foo2, cls).get_foo(), вы находитесь в случае c.2), то есть вы ищете 'get_foo' в самом cls и его родителях после Foo2, то есть вы ищете 'get_foo' в Foo. Вот почему звонок не работает.

Вы ожидаете, что вызов super().get_foo() в методе класса Foo2.get_foo будет успешным, поскольку считаете, что он эквивалентен вызову Foo.get_foo() в случае b), т. е. вы думаете, что ищете 'get_foo' в самом cls и его родителях, а также в классе Foo (FooMeta) и его родителей.

person Maggyero    schedule 10.05.2021