例如,考虑以下内容:

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中,但它没有提到任何关于类方法的内容,我也不太理解与@classmethoddecorator 的交互.

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

编辑:

要澄清的是,在我的特定用例中,我不能编辑元类,我也不想对它进行子类化和/或创建自己的元类,除非它是only possible way个这样做的元类.

推荐答案

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

这就是适用于len(GoodBar())的推理:它将调用GoodBar()类中定义的__len__len(GoodBar)len(BadBar)将调用其类中定义的__len__FooMeta

我真的不理解与@classmethod的交互

"classmethod"修饰符从修饰后的函数中创建一个特殊的描述符,这样,当从绑定的类中通过"getattr"检索到该描述符时,Python会创建一个"partial"对象,其中"cls"参数已经存在.正如从实例检索普通方法创建具有"self"预定义的对象一样:

这两件事都是通过"描述符"协议进行的,也就是说,通过调用其__get__方法来检索普通方法和classmethod.这个方法有3个参数:"self"、描述符本身、"instance"、绑定到的实例和"owner":它所绑定到的类.问题是,对于普通方法(函数),当__get__的第二个(实例)参数是None时,函数本身就会返回.@classmethod用一个不同__get__的对象包装函数:无论第二个参数是__get__还是__get__,该对象都返回与partial(method, cls)等价的值.

换句话说,这个简单的纯Python代码复制了classmethod decorator的工作方式:

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__()显式调用classmethod时会看到相同的行为:忽略实例.

TL;DR:len(klass)将始终通过元类槽,klass.__len__()将通过getattr机制检索__len__,然后在调用它之前正确绑定classmethod.

除了明显的元类解决方案(即,替换/扩展FooMeta)

(…)

没有别的办法了.len(BadBar)将始终通过元类__len__.

不过,扩展元类可能没有那么痛苦.

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

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

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

然而,如果存在子类的层次 struct ,并且它们具有不同的元类,即使是通过此方法创建的,在创建新的"普通"子类之前,您也必须将这两个元类组合在一个公共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__()

以及:


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

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

Python相关问答推荐

将jit与numpy linSpace函数一起使用时出错

在Google Colab中设置Llama-2出现问题-加载判断点碎片时Cell-run失败

如何访问所有文件,例如环境变量

计算组中唯一值的数量

如何在给定的条件下使numpy数组的计算速度最快?

Pandas:将多级列名改为一级

对象的`__call__`方法的setattr在Python中不起作用'

SQLAlchemy Like ALL ORM analog

转换为浮点,pandas字符串列,混合千和十进制分隔符

当点击tkinter菜单而不是菜单选项时,如何执行命令?

多指标不同顺序串联大Pandas 模型

名为__main__. py的Python模块在导入时不运行'

如何使用SentenceTransformers创建矢量嵌入?

解决调用嵌入式函数的XSLT中表达式的语法移位/归约冲突

* 动态地 * 修饰Python中的递归函数

python—telegraph—bot send_voice发送空文件

在numpy数组中寻找楼梯状 struct

Python日志(log)库如何有效地获取lineno和funcName?

为什么在Python中00是一个有效的整数?

如何删除剪裁圆的对角线的外部部分