"classmethod"和元类方法之间有什么区别



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

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

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

>>> 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'>

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

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

由于类是元类的实例,因此元类上的"实例方法"的行为与classmethod类似并不意外。

然而,是的,存在差异——其中一些不仅仅是语义上的:

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

因此,类方法使用的机制恰好是"描述符协议"。虽然普通函数的特征是__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)

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

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


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

而且,除了方法之外,值得注意的是,元类上的@property等专门属性将作为专门的类属性工作,同样,不会有任何令人惊讶的行为。

当你像在问题中那样表达它时,@classmethod和元类可能看起来很相似,但它们的目的截然不同。在@classmethod的参数中注入的类通常用于构造实例(即替代构造函数)。另一方面,元类通常用于修改类本身(例如,像Django对其DSL模型所做的那样)。

这并不是说你不能在类方法中修改类。但问题是,为什么你一开始就没有按照你想要修改的方式定义类?如果没有,它可能会建议重构使用多个类。

让我们稍微扩展一下第一个例子。

class C:
@classmethod
def f(cls):
print(f'f called with cls={cls}')

借用Python文档,上面的内容将扩展到如下内容:

class ClassMethod(object):
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc
class C:
def f(cls):
print(f'f called with cls={cls}')
f = ClassMethod(f)

请注意,__get__可以采用实例或类(或两者都采用),因此可以同时使用C.fC().f。这与您给出的元类示例不同,后者将为C().f抛出AttributeError

此外,在元类示例中,f不存在于C.__dict__中。当用C.f查找属性f时,解释器查看C.__dict__,然后在查找失败后,查看type(C).__dict__(即M.__dict__)。如果您希望在C中灵活地覆盖f,这可能很重要,尽管我怀疑这是否有实际用途。

在您的示例中,不同之处在于其他一些类将M设置为元类。

class M(type):
def f(cls):
pass
class C(metaclass=M):
pass
class C2(metaclass=M):
pass
C.f()
C2.f()
class M(type):
pass
class C(metaclass=M):
@classmethod
def f(cls):
pass
class C2(metaclass=M):
pass
C.f()
# C2 does not have 'f'

以下是关于元类的更多信息元类的一些(具体的)用例是什么?

@classmethod和Metaclass都不同。

python中的所有东西都是一个对象。每件事都意味着每件事。

什么是Metaclass

正如所说,每一件事都是一个物体。类也是对象——事实上,类是其他神秘对象的实例,形式上称为元类。如果未指定,python中的默认元类为"type">

默认情况下,定义的所有类都是类型的实例。

类是元类的实例

理解模拟行为有几个要点

  • As类是元类的实例
  • 像每个实例化的对象一样,类对象(实例)从类中获取其属性。类将从Meta类中获取其属性

考虑以下代码

class Meta(type):
def foo(self):
print(f'foo is called self={self}')
print('{} is instance of {}: {}'.format(self, Meta, isinstance(self, Meta)))
class C(metaclass=Meta):
pass
C.foo()

其中,

  • 类C是类Meta的实例
  • "class C"是作为"class Meta"实例的类对象
  • 像任何其他对象(实例)一样,"类C"可以访问其类"类Meta"中定义的属性/方法
  • 所以,解码"C.foo()"。"C"是"Meta"的实例,"foo"是通过"Meta(C)"的实例调用的方法
  • 方法"foo"的第一个参数是对实例的引用,而不是与"classmethod"不同的类

我们可以验证"class C"是否是"class Meta "的实例

isinstance(C, Meta)

什么是classmethod

据说Python方法是绑定的。由于python强加了只能使用实例调用方法的限制。有时,我们可能想直接通过类调用方法,而不需要任何实例(很像java中的静态成员),而不必创建任何实例。默认情况下,调用方法需要实例。作为一种变通方法,python提供了内置的函数classmethod,将给定的方法绑定到类而不是实例。

As类方法绑定到类。它至少需要一个参数,该参数是对类本身的引用,而不是实例(自身)

如果使用内置函数/装饰器类方法。第一个论点将引用类而不是实例

class ClassMethodDemo:
@classmethod
def foo(cls):
print(f'cls is ClassMethodDemo: {cls is ClassMethodDemo}')

由于我们使用了"classmethod",我们在不创建任何实例的情况下调用方法"foo",如下所示

ClassMethodDemo.foo()

上面的方法调用将返回True。由于第一个参数cls确实引用了"ClassMethodDemo">

摘要:

  • Classmethod的receive first参数是"对类(传统上称为cls)本身的引用">
  • 元类的方法不是类方法。元类的方法接收的第一个参数是"对实例(传统上称为self)的引用,而不是类">

最新更新