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

发布于 2025-01-29 21:22:00 字数 1039 浏览 2 评论 0 原文

例如,请考虑以下内容:

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,这是预期的行为。

此行为(某种程度上)在,但它没有提及ClassMethods,我真的不理解与 @ClassMethod Decorator的互动。

除了明显的Metaclass解决方案(即,替换/扩展 foometa ),还有一种方法可以覆盖或扩展MetaClass函数,以便 len(badbar) - > 9002

编辑:

要澄清,在我的特定用例中,我不能编辑元素,我不想对其进行亚类和/或制作自己的元素这样做。

As an example, consider the following:

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

The issue being with len(BadBar) returning 9000 instead of 9002 which is the intended behaviour.

This behaviour is (somewhat) documented in Python Data Model - Special Method Lookup, but it doesn't mention anything about classmethods, and I don't really understand the interaction with the @classmethod decorator.

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?

Edit:

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

耀眼的星火 2025-02-05 21:22:00

当使用 len(...)对类本身使用 __ len __ ,始终将忽略类中的 当执行其运算符时,以及诸如“哈希”,“”,“可以粗略地说iTer”,“ len”具有“操作员状态”,Python总是通过直接符合类的内存结构来从目标类中检索相应的方法。这些dunder方法在类的内存布局中具有“物理”插槽:如果该方法存在于您实例的类中(在这种情况下,“实例”是“ boodbar”和“ badbar”类,则是“ badbar”,则是“ badbar”的实例。 foometa”)或它的超类之一,被称为 - 否则操作员会失败。

因此,这是适用于 len(goodbar())的推理:它将调用 __ len __ goodbar()的类中定义的 ,和 len(goodbar) len(badbar)将调用 __ len __ __ 在其类中定义的, foometa

我不太了解与@classmethod的互动
装饰器。

“ classMethod”装饰器从装饰函数中创建了一个特殊的描述符,因此当通过“ getAttr”从类中检索它时,python也可以使用“ cls”参数创建一个“部分”对象。就像从一个实例中检索普通方法一样,用“自我”预绑来创建一个对象一样:

这两种内容都是通过“描述符”协议携带的 - 这意味着,通过调用其 __ get__ get __来检索一个普通方法和classMethod 方法。此方法采用3个参数:“自我”,描述符本身,“实例”,实例与“ body”和“所有者”:类别的类别。事实是,对于普通方法(函数),第二个(实例)参数到 __获取__ get __ is none none 时,函数本身将返回。 @ClassMethod 用不同的对象包装一个函数 __获取__ :一个返回等同于 partial(方法,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 __()明确调用ClassMethod时会看到相同的行为。 Klass().__获取__():该实例被忽略。

tl; dr len(klass)将始终通过Metaclass插槽, klass .__ len __()将检索/code>通过getAttr机制,然后在调用之前正确绑定分类。

除了明显的元素解决方案(即,更换/扩展foometa)
有没有办法覆盖或扩展元函数,以便
Len(Badbar) - > 9002?

(...)
为了澄清,在我的特定用例中,我无法编辑元素,我
不想将其分类和/或制作我自己的元素,除非是
这样做的唯一可能的方法。

没有其他方法。 len(badbar)将始终通过Metaclass __ Len __

不过,扩展元口可能并不那么痛苦。
可以通过简单调用 type 传递新的 __ len __ 方法:

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

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

仅当稍后将与其他类别的层次结构合并为 不同的 自定义元素您必须担心。即使还有其他类 foometa 作为metaclass的类,上面的摘要也将起作用:动态创建的元口将是新子类的元类,作为“最派生的子类”。

但是,如果有一个子类的层次结构,并且它们具有不同的元类别,即使通过这种方法创建,您也必须将两个元素组合在公共 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

The __len__ defined in the class will always be ignored when using len(...) for the class itself: when executing its operators, and methods like "hash", "iter", "len" can be roughly said to have "operator status", Python always retrieve the corresponding method from the class of the target, by directly acessing the memory structure of the class. These dunder methods have "physical" slot in the memory layout for the class: if the method exists in the class of your instance (and in this case, the "instances" are the classes "GoodBar" and "BadBar", instances of "FooMeta"), or one of its superclasses, it is called - otherwise the operator fails.

So, this is the reasoning that applies on len(GoodBar()): it will call the __len__ defined in GoodBar()'s class, and len(GoodBar) and len(BadBar) will call the __len__ defined in their class, FooMeta

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

The "classmethod" decorator creates a special descriptor out of the decorated function, so that when it is retrieved, via "getattr" from the class it is bound too, Python creates a "partial" object with the "cls" argument already in place. Just as retrieving an ordinary method from an instance creates an object with "self" pre-bound:

Both things are carried through the "descriptor" protocol - which means, both an ordinary method and a classmethod are retrieved by calling its __get__ method. This method takes 3 parameters: "self", the descriptor itself, "instance", the instance its bound to, and "owner": the class it is ound to. The thing is that for ordinary methods (functions), when the second (instance) parameter to __get__ is None, the function itself is returned. @classmethod wraps a function with an object with a different __get__: one that returns the equivalent to partial(method, cls), regardless of the second parameter to __get__.

In other words, this simple pure Python code replicates the working of the 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)

That is why you see the same behavior when calling a classmethod explicitly with klass.__get__() and klass().__get__(): the instance is ignored.

TL;DR: len(klass) will always go through the metaclass slot, and klass.__len__() will retrieve __len__ via the getattr mechanism, and then bind the classmethod properly before calling it.

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.

There is no other way. len(BadBar) will always go through the metaclass __len__.

Extending the metaclass might not be all that painful, though.
It can be done with a simple call to type passing the new __len__ method:

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

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

Only if BadBar will later be combined in multiple inheritance with another class hierarchy with a different custom metaclass you will have to worry. Even if there are other classes that have FooMeta as metaclass, the snippet above will work: the dynamically created metaclass will be the metaclass for the new subclass, as the "most derived subclass".

If however, there is a hierarchy of subclasses and they have differing metaclasses, even if created by this method, you will have to combine both metaclasses in a common subclass_of_the_metaclasses before creating the new "ordinary" subclass.

If that is the case, note that you can have one single paramtrizable metaclass, extending your original one (can't dodge that, though)

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

And:


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

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

冧九 2025-02-05 21:22:00

如您已经提到的, len()将在对象的类型不是对象本身上找到 __ Len __ 。因此, len(badbar)被称为 type(badbar)。方法是在此处获取 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的其他类别将受到影响。您无法概括它。

您可以为 badbar 拥有另一个特定的元素价值。

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 class是否已经定义 __ len __ 为classMethod,如果是这样,请使用。

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

这里是找到ClassMethods的更强大方法。

编辑后:

如果您无法访问Metaclass,则可以装饰和猴子补丁元素:

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

As you already mentioned, len() is going to find __len__ on the object's type not the object itself. So len(BadBar) is called like type(BadBar).__len__(BadBar), FooMeta's __len__ method is getting the instance which is BadBar here as the first parameter.

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

This is how it works, if you do anything with metaclass's __len__, other classes with that metaclass will be affected. You can not generalize it.

You can have another specific metaclass for BadBar and call the cls.__len__ inside the __len__ of the metaclass if it has one, otherwise return the default value.

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

Or if you just want to override only classmethods of the classes, you can check to see if the cls class has already defined __len__ as a classmethod, if so call that instead.

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 are more robust approaches to find classmethods.

After your edit:

If you don't have access to the metaclass, then you can decorate and monkey patch the metaclass:

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文