如何使用自定义类方法重写元类中定义的特殊方法



作为示例,请考虑以下内容:

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

最新更新