在 class' class 方法中调用 super() 以获取 metaclass 方法

Calling super() inside a class' classmethod to get at the metaclass method

就在我理解 metaclasses 的时候...

免责声明:我在发帖前四处寻找答案,但我找到的大部分答案都是关于打电话给 super() 以获得另一个 @classmethod 在 MRO 中(不涉及 metaclass),或者,令人惊讶的是,他们中的很多人试图在 metaclass.__new__metaclass.__call__ 中做一些事情,这意味着 class 不是尚未完全创建。我很确定(假设是 97%)这不是这些问题之一。


环境:Python3.7.2


问题: 我有一个 metaclass FooMeta 定义了一个方法 get_foo(cls),一个 class Foo 是从那个 metaclass 构建的(所以一个实例FooMeta) 的 @classmethod get_bar(cls)。然后是继承自 Foo 的另一个 class Foo2。在 Foo2 中,我通过将其声明为 @classmethod 并调用 super() 来子 class get_foo。这惨遭失败...

即使用此代码

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'


问题: 因此,我的 class 是 metaclass 的一个实例,并且已验证 class 方法都存在于 class Foo 上,为什么两者都不存在调用在 Foo2 内部工作的 super().get_***()我对 metaclasses 或 super() 有什么不理解,这会阻止我发现这些结果合乎逻辑?

编辑:进一步测试表明 Foo2 上的方法是 class 方法或实例方法不会改变结果。

编辑 2:感谢@chepner 的回答,我认为问题是 super() 正在返回一个代表 Foo 的超级对象(这已通过 super().__thisclass__ 验证),我是期待 super().get_foo() 在幕后表现(甚至可能打电话)get_attr(Foo, 'get_foo')。好像不是...我还在想为什么,但是越来越清楚了:)

注一

明确一点:

  • Foo 继承自FooMeta.
  • FooMeta 不是 Foo[=164= 的超级 class ]

super 将不起作用。

注2

既然注释 (1) 已经不在了,如果你想从 metaclass 实例的方法内部访问 metaclass 方法,你可以这样做它像这样:

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方法与metaclass中的方法同名,为什么metaclass方法NOT 接到电话了吗?考虑以下代码:

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! 在下面的讨论中,假设:

  • fooFoo
  • 的实例
  • FooFooMeta
  • 的实例

第一次在这个post,我会有伪代码,不是python。不要尝试 运行 以下内容。 __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 get_foo from the meta class."

  1. 首先,我们尝试从实例中获取get_foo。也许 get_foo 是一个成员变量。也许一个实例有 get_foo = 1 而另一个实例有 get_foo = 5 计算机不知道。电脑很笨
  2. 计算机发现实例没有名为get_foo的成员变量。然后它说,"ah ha! I bet that get_foo belongs to the CLASS." 所以,它看起来在那里,瞧瞧,它就在那里:Foo 有一个名为 get_foo 的属性。 FooMeta 也有一个名为 get_foo 的属性,但谁在乎那个。

需要重点关注的是:

  • Foo 有一个名为 get_foo
  • 的属性
  • MetaFoo 有一个名为 get_foo
  • 的属性

他们两者都有名为get_foo的属性,但是FooMetaFoo是不同的对象。这两个 get_foo 并不是共享的。我可以有 obj1.x = 1obj2.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 中的 none 有一个 __get__ 方法

好了!为什么只有第 1 和第 2 行?因为你把@classmethod弄傻了!我们检查 Foo 看它是否有 get_foo 并且确实有!如果我们首先找到实例属性,为什么要检查 class 属性?在检查是否可能发生之前,我们总是检查属性是否属于实例 (Foo) first-and-foremost仅是属于 class (FooMeta) 并由所有实例共享的静态成员变量的一个副本。

请注意,如果 Foo 没有 get_foo,那么 FooMeta.__getattribute__(Foo, "get_foo") 将从 metaclass return get_foo 因为第一次尝试(获取它来自实例)失败。您通过为实例提供与 class 的静态成员变量同名的内容来阻止该选项。

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

注意,如果要设置一个静态的class成员变量,instance.mem_var = 5是一个很ⱽᵉᴿʸ的坏主意。您将给 instance 一个新的成员变量,而 class 静态(共享)成员变量将被隐藏。你可以用这样的东西来解决这个问题:

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)

然后你的 lil' compy 将打印:

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 funkyFoo 的属性而不是 FooMeta

的属性

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 本身。

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类方法,但是B的f是继承自A的。D的方法解析顺序,类检查的顺序属性查找,进入 (D, B, C, A, object).

super().f() + 1cls 的 MRO 中搜索 f 实现。它应该找到的是 C.f,但 B 具有继承的 f 实现,并且 B 在 MRO 中位于 C 之前。如果 super 选择 B.f,这将破坏 C 在通常称为 "diamond problem" 的情况下覆盖 f 的尝试。 =44=]

super 不查看 B 具有哪些属性,而是直接查看 B 的 __dict__,因此它只考虑 B 而不是 B 实际提供的属性的 super类 或 meta类.


现在,回到您的 get_foo/get_bar 情况。 get_bar 来自 Foo 本身,所以 super().get_bar() 找到 Foo.get_bar。但是,get_foo 不是由 Foo 提供的,而是由 FooMeta 元类提供的,并且 Foo.__dict__ 中没有 get_foo 的条目。因此,super().get_foo() 什么也找不到。

obj.attr调用type(obj).__getattribute__(obj, 'attr'),即

  • a) object.__getattribute__(obj, 'attr'),它在 obj 本身以及 obj 的 class 及其 [=78] 中查找 'attr' =];或
  • b) type.__getattribute__(obj, 'attr') 如果 obj 是一个 type 实例,它在 obj 本身及其 parents 中查找 'attr',以及 obj 的 class 及其 parents;或
  • c) super.__getattribute__(obj, 'attr') 如果 obj 是一个 super 实例,
    • c.1) 在 instance 的 class 中查找 'attr' 及其 cls 之后的 parents,如果 objsuper(cls, instance),或
    • c.2) 在 subclass 本身及其 parents 过去 cls 中查找 'attr',如果 objsuper(cls, subclass) .

当你在class方法Foo2.get_foo中调用super().get_foo()时,相当于调用super(Foo2, cls).get_foo(),你是c.2)的情况,即你是在 cls 本身及其 parents 过去 Foo2 中查找 'get_foo',即您在 Foo 中查找 'get_foo'。这就是调用失败的原因。

您希望 class 方法 Foo2.get_foo 中的调用 super().get_foo() 成功,因为您认为它等同于案例 b) 中的调用 Foo.get_foo(),也就是说,您认为您正在 cls 本身及其 parents、 以及 Foo 的 class 中查找 'get_foo'FooMeta) 及其 parents.