为什么 __qualname__ 在 @classmethod 中的工作方式与在元类中的工作方式不同

问题描述

我通过编写实现了一个简约的枚举

class SymbolClass(type):
    def __repr__(cls): return cls.__qualname__
class Symbol(Metaclass=SymbolClass): pass

class Animal:
    class Dog(Symbol): pass
    class Cat(Symbol): pass
    class Cow(Symbol): pass

效果很好

>>> {Animal.Dog: 'Wan Wan',Animal.Cat: 'Nyaa',Animal.Cow: 'Moooh'}
{Animal.Dog: 'Wan Wan',Animal.Cow: 'Moooh'}

但是,我想知道是否可以通过使用类方法来摆脱元类

class Symbol: 
    @classmethod
    def __repr__(cls): return cls.__qualname__

class Animal:
    class Dog(Symbol): pass
    class Cat(Symbol): pass
    class Cow(Symbol): pass

效果不佳:

>>> {Animal.Dog: 'Wan Wan',Animal.Cow: 'Moooh'}
{<class '__main__.Animal.Dog'>: 'Wan Wan',<class '__main__.Animal.Cat'>: 'Nyaa',<class '__main__.Animal.Cow'>: 'Moooh'}

然后我写了一小段测试代码

class SymbolClass(type):
    def __repr__(cls): return cls.__qualname__
    def __str__(cls): return cls.__name__

class SymbolA(Metaclass=SymbolClass): pass
    
class SymbolB:
    @classmethod
    def __repr__(cls): return cls.__qualname__
    
class SymbolC:
    def __repr__(cls): return cls.__qualname__
    
class Animal:
    class Dog(SymbolA): pass
    class Cat(SymbolB): pass
    class Cow(SymbolC): pass

import sys
def printcompare(n1,l1n,l1v,n2,l2n):
    v1 = eval(n1)
    v2 = eval(n2)
    file = sys.stdout if v2==v1 else sys.stderr
    print(f'{n1:>{l1n}} = {v1+",":<{l1v+2}} {n2:>{l2n}} = {v2}',file=file,flush=True)

for name in "SymbolClass","SymbolA","SymbolB","SymbolC","Animal","Animal.Dog","Animal.Cat","Animal.Cow":
    printcompare(f"{name}.__qualname__",24,11,f"repr({name})",16)

这为我提供了以下结果:

enter image description here

我不知道是什么造成了这种差异。看起来@classmethod 就像一个普通方法一样工作......

解决方法

您可以追溯这两种情况下发生的情况以了解差异。

dict 打印其键和值的 __repr__。由于 __repr__ 是一个魔法方法,python 不会在实例上查找它。理解这一点至关重要。调用 repr(obj) 时,绑定如下:

type(obj).__repr__(obj)     # No binding happening,just calling function

不是

obj.__repr__.__get__(obj)()  # Binding of non-data function descriptor

你得到了前者,但期待后者。

元类

type(Animal.Dog).__repr__(Animal.Dog) 完全等同于 SymbolClass.__repr__(Animal.Dog),它只返回准确的字符串 Animal.Dog.__qualname__。这里几乎没有歧义。但是,会出现一些混淆,因为如果您忽略 __repr__ 已优化为绑定到类的事实,您将得到相同的结果。也就是说,Animal.Dog.__repr__() 将跟随 MRO 到 Animal.Dog 的类型,并使用 __repr__ 的隐式 self 调用 Animal.Dog。你不会看到任何区别。

类方法

然而,当您使用 classmethod 时,您正试图绕过 Python 对魔法方法的优化绑定。这是因为 classmethod 首先依赖于作为描述符绑定,而是作为简单函数调用。

这是第一个键的实际情况:

type(Animal.Dog).__repr__(Animal.Dog)

这归结为type.__repr__(Animal.Dog)。请注意,如果没有元类,您现在调用的是默认的 __repr__,而不是自定义的。此时输出应该非常清楚。

如果魔术方法机制按您的预期工作,您将改为执行 Animal.Dog.__repr__(),这会按预期工作,因为它现在可以通过 __repr__ 正确地将 classmethod 绑定到类,并将 Animal.Dog 作为 cls 传递。您可以尝试以这种方式调用它,看看会发生什么。事实上,即使这个调用约定也能工作,因为 classmethod 装饰器不会盲目地将 type(self) 传递给包装方法,而是首先检查它是否是父类对象。