如何使用自定义类方法覆盖元类中定义的特殊方法?

Ton*_*sip 6 python overriding metaclass magic-methods

例如,请考虑以下情况:

class FooMeta(type):
    def __len__(cls):
        return 9000


class GoodBar(metaclass=FooMeta):
    def __len__(self):
        return 9001


class BadBar(metaclass=FooMeta):
    @classmethod
    def __len__(cls):
        return 9002

len(GoodBar) -> 9000
len(GoodBar()) -> 9001
GoodBar.__len__() -> TypeError (missing 1 required positional argument)
GoodBar().__len__() -> 9001
len(BadBar) -> 9000 (!!!)
len(BadBar()) -> 9002
BadBar.__len__() -> 9002
BadBar().__len__() -> 9002
Run Code Online (Sandbox Code Playgroud)

问题在于len(BadBar)返回 9000 而不是 9002,这是预期的行为。

这种行为(在某种程度上)记录在Python 数据模型 - 特殊方法查找中,但它没有提到任何有关类方法的内容,而且我并不真正理解与@classmethod装饰器的交互。

除了明显的元类解决方案(即替换/扩展FooMeta)之外,是否有一种方法可以覆盖或扩展元类函数,以便len(BadBar) -> 9002

编辑:

为了澄清,在我的特定用例中,我无法编辑元类,并且我不想对其进行子类化和/或创建我自己的元类,除非这是唯一可能的方法

jsb*_*eno 1

当用于类本身时,类中的定义__len__总是会被忽略len(...):当执行其运算符时,像“hash”,“iter”,“len”这样的方法可以粗略地说具有“运算符状态”,Python总是检索通过直接访问类的内存结构,从目标类中获取相应的方法。这些 dunder 方法在类的内存布局中具有“物理”槽:如果该方法存在于实例的类中(在本例中,“实例”是类“GoodBar”和“BadBar”,“实例”) FooMeta") 或其超类之一,它被调用 - 否则操作符失败。

因此,这就是适用于 的推理len(GoodBar()):它将调用的类__len__中的定义,并且将调用其类中的定义,GoodBar()len(GoodBar)len(BadBar)__len__FooMeta

我不太明白与 @classmethod 装饰器的交互。

“classmethod”装饰器从装饰函数中创建一个特殊的描述符,这样当它被检索时,通过“getattr”从它所绑定的类中检索,Python会创建一个“部分”对象,其中“cls”参数已经就位。就像从实例中检索普通方法一样,会创建一个预先绑定了“self”的对象:

这两件事都是通过“描述符”协议进行的 - 这意味着普通方法和类方法都是通过调用其__get__方法来检索的。这个方法有3个参数:“self”,描述符本身,“instance”,它绑定的实例,以及“owner”:它所绑定的类。问题是,对于普通方法(函数),当第二个(实例)参数为 时__get__None将返回函数本身。@classmethod用具有不同 的对象包装函数__get__:返回与 等效的对象partial(method, cls),而不管 的第二个参数如何__get__

换句话说,这个简单的纯 Python 代码复制了classmethod装饰器的工作:

class myclassmethod:
    def __init__(self, meth):
         self.meth = meth
    def __get__(self, instance, owner):
         return lambda *args, **kwargs: self.meth(owner, *args, **kwargs)
Run Code Online (Sandbox Code Playgroud)

这就是为什么在使用klass.__get__()and显式调用类方法时会看到相同的行为klass().__get__():实例被忽略。

TL;DRlen(klass) 将始终通过元类槽,并通过 getattr 机制klass.__len__()检索__len__ ,然后在调用之前正确绑定类方法。

除了明显的元类解决方案(即替换/扩展 FooMeta)之外,是否有一种方法可以覆盖或扩展元类函数,以便 len(BadBar) -> 9002?

(...)为了澄清,在我的特定用例中,我无法编辑元类,并且我不想对其进行子类化和/或创建我自己的元类,除非这是唯一可能的方法。

没有其他办法。len(BadBar)总是会经历元类__len__

不过,扩展元类可能并不那么痛苦。可以通过简单调用type传递新__len__方法来完成:

In [13]: class BadBar(metaclass=type("", (FooMeta,), {"__len__": lambda cls:9002})):
    ...:     pass
    

In [14]: len(BadBar)
Out[14]: 9002
Run Code Online (Sandbox Code Playgroud)

只有当 BadBar 稍后将与具有不同自定义元类的另一个类层次结构合并为多重继承时,您才需要担心。即使有其他类具有FooMeta元类,上面的代码片段也将起作用:动态创建的元类将是新子类的元类,作为“最派生的子类”。

但是,如果存在子类的层次结构,并且它们具有不同的元类,即使通过此方法创建,您也必须在创建新的“普通”子类之前将两个元类组合在一个公共subclass_of_the_metaclasses中。

如果是这种情况,请注意,您可以拥有一个可参数化的元类,从而扩展您的原始元类(但无法回避这一点)

class SubMeta(FooMeta):
    def __new__(mcls, name, bases, ns, *,class_len):
         cls = super().__new__(mcls, name, bases, ns)
         cls._class_len = class_len
         return cls

    def __len__(cls):
        return cls._class_len if hasattr(cls, "_class_len") else super().__len__()
Run Code Online (Sandbox Code Playgroud)

和:


In [19]: class Foo2(metaclass=SubMeta, class_len=9002): pass

In [20]: len(Foo2)
Out[20]: 9002

Run Code Online (Sandbox Code Playgroud)