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

How can I override a special method defined in a metaclass with a custom classmethod?

例如,请考虑以下内容:

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

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

此行为(在某种程度上)记录在 Python Data Model - Special Method Lookup 中,但它没有提及任何关于类方法的内容,而且我不太了解与 @classmethod 装饰器的交互。

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

编辑:

澄清一下,在我的特定用例中,我无法编辑元类,并且我不想将其子类化 and/or 创建我自己的元类,除非它是唯一的 这样做的可能方式

正如您已经提到的,len() 将根据对象的类型而不是对象本身查找 __len__。所以 len(BadBar) 被称为 type(BadBar).__len__(BadBar)FooMeta__len__ 方法正在获取实例,这里是 BadBar 作为第一个参数。

class FooMeta(type):
    def __len__(cls):
        print(cls is BadBar)
        return 9000


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


print(len(BadBar)) # True - 9000

这就是它的工作原理,如果您对 metaclass 的 __len__ 进行任何操作,其他具有该 metaclass 的 classes 将受到影响。不能一概而论。

您可以为 BadBar 设置另一个特定的元class,如果有的话,在元class 的 __len__ 中调用 cls.__len__,否则 return 默认值。

class BadBar_meta(type):
    def __len__(cls):
        if hasattr(cls, "__len__"):
            return cls.__len__()
        return 9000


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


print(len(BadBar))  # 9002

或者如果你只想重写 classes 的 class 方法,你可以检查 cls class 是否已经定义 __len__ 作为 class 方法,如果是这样的话,请改为调用它。

class FooMeta(type):
    def __len__(cls):
        m = cls.__len__
        if hasattr(m, "__self__"):
            return cls.__len__()

        return 9000


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


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


print(len(GoodBar)) # 9000
print(len(BadBar))  # 9002

Here 是寻找 class 方法的更可靠方法。

编辑后:

如果您无法访问元数据class,那么您可以对元数据进行装饰和猴子修补class:

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


def decorator(mcls):
    org_len = mcls.__len__

    def custom_len(cls):
        # check too see if it is a classmethod
        if hasattr(cls.__len__, "__self__"):
            return cls.__len__()
        return org_len(cls)

    FooMeta.__len__ = custom_len
    return mcls


FooMeta = decorator(FooMeta)

print(len(GoodBar))  # 9000
print(len(BadBar))   # 9002

在 class 中定义的 __len__ 在对 class 本身使用 len(...) 时将始终被忽略:在执行其运算符和“哈希”等方法时, "iter", "len" 可以粗略的说是具有"运算符状态", Python总是从目标的class中获取对应的方法,通过直接访问[=的内存结构81=]。这些 dunder 方法在 class 的内存布局中具有“物理”插槽:如果该方法存在于实例的 class 中(在这种情况下,“实例”是 classes“GoodBar”和“BadBar”,“FooMeta”的实例),或其超classes之一,它被调用 - 否则运算符失败。

因此,这就是适用于 len(GoodBar()) 的推理:它将调用 GoodBar() 的 class 中定义的 __len__len(GoodBar) len(BadBar) 将调用在其 class、FooMeta

中定义的 __len__

I don't really understand the interaction with the @classmethod decorator.

“classmethod”装饰器从装饰函数中创建了一个特殊的描述符,因此当它被检索时,通过 class 中的“getattr”它也被绑定,Python 创建一个带有“cls”参数的“部分”对象。就像从实例中检索一个普通方法创建一个带有“self”的对象 pre-bound:

这两件事都通过“描述符”协议进行 - 这意味着,普通方法和 class 方法都可以通过调用其 __get__ 方法来检索。这个方法有 3 个参数:“self”,描述符本身,“instance”,它绑定到的实例,以及“owner”:它绑定到的 class。问题是对于普通方法(函数),当 __get__ 的第二个(实例)参数是 None 时,函数本身被返回。 @classmethod 用具有不同 __get__ 的对象包装函数:returns 相当于 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)

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

TL;DR: len(klass) 将始终通过 metaclass 插槽,并且 klass.__len__() 将检索 __len__通过 getattr 机制,然后在调用之前正确绑定 class 方法。

Aside from the obvious metaclass solution (ie, replace/extend FooMeta) is there a way to override or extend the metaclass function so that len(BadBar) -> 9002?

(...) To clarify, in my specific use case I can't edit the metaclass, and I don't want to subclass it and/or make my own metaclass, unless it is the only possible way of doing this.

没有别的办法。 len(BadBar) 将始终通过 metaclass __len__.

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

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

In [14]: len(BadBar)
Out[14]: 9002

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

但是,如果存在子classes 的层次结构并且它们具有不同的 metaclasses,即使通过此方法创建,您也必须将两个 metaclass 结合起来]在创建新的“普通”子class.

之前在公共subclass_of_the_metaclasses

如果是这样,请注意您可以有一个单一的可参数化元class,扩展您原来的元(虽然不能回避)

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__()

并且:


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

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