当类本身使用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