作为示例,请考虑以下内容:
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 数据模型 - 特殊方法查找中,但它没有提到任何关于类方法的内容,我也不真正了解与@classmethod
装饰器的交互。
除了明显的元类解决方案(即替换/扩展FooMeta
)之外,有没有办法覆盖或扩展元类函数以便len(BadBar) -> 9002
?
编辑:
澄清一下,在我的特定用例中,我无法编辑元类,并且我不想对其进行子类化和/或创建自己的元类,除非这是执行此操作的唯一可能方法。
当对类本身使用len(...)
时,类中定义的__len__
将始终被忽略:在执行其运算符时,像 "hash"、"iter"、"len" 这样的方法可以粗略地说是具有 "运算符状态", Python 总是从目标的类中检索相应的方法, 通过直接访问类的内存结构。这些 dunder 方法在类的内存布局中具有"物理"插槽:如果该方法存在于实例的类中(在这种情况下,"实例"是类 "GoodBar" 和 "BadBar","FooMeta" 的实例),或其超类之一,则调用它 - 否则运算符将失败。
所以,这就是适用于len(GoodBar())
的推理:它将调用GoodBar()
类中定义的__len__
,len(GoodBar)
和len(BadBar)
将调用其类中定义的__len__
,FooMeta
">我真的不明白与@classmethod的互动 装饰。
classmethod"装饰器从装饰函数中创建一个特殊的描述符,因此当它被检索时,通过类中的"getattr"它也绑定,Python创建一个"部分"对象,其中"cls"参数已经到位。就像从实例中检索普通方法会创建一个预先绑定了"self"的对象一样:
这两件事都是通过"描述符"协议进行的 - 这意味着,普通方法和类方法都是通过调用其__get__
方法来检索的。此方法采用 3 个参数:"self"、描述符本身、"实例"、其绑定到的实例和"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)
这就是为什么在使用klass.__get__()
和klass().__get__()
显式调用类方法时会看到相同的行为:实例被忽略。
TL;DR:len(klass)
将始终通过元类插槽,klass.__len__()
将通过getattr机制检索__len__
,然后在调用之前正确绑定classmethod。
除了明显的元类解决方案(即替换/扩展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
仅当 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__()
和:
In [19]: class Foo2(metaclass=SubMeta, class_len=9002): pass
In [20]: len(Foo2)
Out[20]: 9002
正如你已经提到的,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
这就是它的工作原理,如果你对元类的__len__
做任何事情,该元类的其他类将受到影响。你不能一概而论。
您可以为BadBar
使用另一个特定的元类,并在元类的__len__
中调用cls.__len__
(如果有),否则返回默认值。
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
或者,如果您只想重写类的类方法,则可以检查cls
类是否已将__len__
定义为类方法,如果是,则改为调用它。
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
以下是查找类方法的更健壮的方法。
编辑后:
如果您无权访问元类,则可以装饰和修补元类:
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