`classmethod` 和元类方法有什么区别?

use*_*783 14 python methods metaclass class-method

在 Python 中,我可以使用@classmethod装饰器创建一个类方法:

>>> class C:
...     @classmethod
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> C.f()
f called with cls=<class '__main__.C'>
Run Code Online (Sandbox Code Playgroud)

或者,我可以在元类上使用普通(实例)方法:

>>> class M(type):
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> class C(metaclass=M):
...     pass
...
>>> C.f()
f called with cls=<class '__main__.C'>
Run Code Online (Sandbox Code Playgroud)

如 的输出所示C.f(),这两种方法提供了相似的功能。

@classmethod在元类上使用和使用普通方法有什么区别?

jsb*_*eno 6

由于类是元类的实例,因此元类上的“实例方法”表现得像类方法也就不足为奇了。

但是,是的,存在差异 - 其中一些不仅仅是语义:

  1. 最重要的区别是元类中的方法从类实例中“不可见” 。发生这种情况是因为 Python 中的属性查找(以简化的方式 - 描述符可能优先)在实例中搜索一个属性 - 如果它不存在于实例中,Python 然后在该实例的类中查找,然后继续搜索类的超类,但不在类的类上。Python stdlib 在abc.ABCMeta.register方法中使用了这个特性。该功能可以很好地使用,因为与类本身相关的方法可以自由地作为实例属性重新使用而不会产生任何冲突(但方法仍然会发生冲突)。
  2. 另一个区别虽然很明显,但在元类中声明的方法可以在多个类中使用,而不是其他相关的 - 如果您有不同的类层次结构,它们处理的内容根本不相关,但想要所有类的一些通用功能,你必须想出一个 mixin 类,它必须作为基础包含在两个层次结构中(比如在应用程序注册表中包含所有类)。(注意,mixin 有时可能比元类更好)
  3. 类方法是专门的“类方法”对象,而元类中的方法是普通函数。

所以,恰好classmethods使用的机制是“描述符协议”。虽然普通函数具有一个__get__方法,该方法会在self从实例检索时插入参数,并在从类检索时将该参数留空,但 classmethod对象具有不同的__get__,它将插入类本身(“所有者”)作为两种情况下的第一个参数。

这在大多数情况下没有实际区别,但是如果您希望将方法作为函数访问,以便向其添加动态添加装饰器,或任何其他目的,元类中的方法meta.method检索函数,准备使用,而您必须使用cls.my_classmethod.__func__ 从类方法中检索它(然后您必须创建另一个classmethod对象并将其分配回来,如果您进行一些包装)。

基本上,这些是两个例子:


class M1(type):
    def clsmethod1(cls):
        pass

class CLS1(metaclass=M1):
    pass

def runtime_wrap(cls, method_name, wrapper):
    mcls = type(cls)
    setattr(mcls, method_name,  wrapper(getatttr(mcls, method_name)))

def wrapper(classmethod):
    def new_method(cls):
        print("wrapper called")
        return classmethod(cls)
    return new_method

runtime_wrap(cls1, "clsmethod1", wrapper)

class CLS2:
    @classmethod
    def classmethod2(cls):
        pass

 def runtime_wrap2(cls, method_name, wrapper):
    setattr(cls, method_name,  classmethod(
                wrapper(getatttr(cls, method_name).__func__)
        )
    )

runtime_wrap2(cls1, "clsmethod1", wrapper)
Run Code Online (Sandbox Code Playgroud)

换句话说:除了元类中定义的方法对实例可见而classmethod对象不可见这一重要区别之外,其他区别在运行时似乎是模糊和无意义的——但发生这种情况是因为语言不需要去排除类方法的特殊规则:作为语言设计的结果,声明类方法的两种方式都是可能的 - 一种是因为类本身就是一个对象,而另一种是许多中的一种可能性使用描述符协议,允许在实例和类中专门化属性访问:

classmethod内置在本机代码中定义,但它可能只是在纯Python进行编码,并在完全相同的方式是可行的。下面的 5 行类可以用作classmethod装饰器,当然与内置的@classmethod" at all (though distinguishable through introspection such as calls toisinstance , and evenrepr`没有运行时差异):


class myclassmethod:
    def __init__(self, func):
        self.__func__ = func
    def __get__(self, instance, owner):
        return lambda *args, **kw: self.__func__(owner, *args, **kw)
Run Code Online (Sandbox Code Playgroud)

并且,除了方法之外,有趣的是要记住,像@property元类上的a之类的专门属性将作为专门的类属性工作,完全一样,完全没有令人惊讶的行为。