揭开 Python 元类的神秘面纱 为什么它们如此特别?
带有类的类 — Python 中的元类
当你开始学习任何面向对象的编程语言时,你需要了解两个概念:类和实例。允许您快速理解它们的基本简化是,类是蓝图或示意图,即基于创建实例的逻辑实体。另一方面,实例是具有状态和某些特定行为的实际“物理”对象。进一步进入与机器相关的东西,声明一个类不会分配内存,而创建一个实例会。这有点过于简单化,但在大多数情况下是正确的。
实例是使用构造函数创建的,构造函数是一个代码块,用于指定为对象分配内存时应发生的情况。在其他编程语言中,你会在构造函数中做的事情,通常在 Python 中的 init 方法中执行。
class Dog:
def __init__(self, good_boy: bool):
self.good_boy = good_boy
但是,当您仔细查看传递给构造函数的参数时,您会注意到第一个参数始终是 self。此时此对象不是已经创建吗?井。。。是的。当谷歌搜索“python构造函数”时,你最有可能发现的实际上不是一个构造函数,而是一个所谓的“初始值设定项”。它不负责创建对象实例,而是负责实例化其状态。在 Python 中创建对象的方法称为 new 。
class Dog:
def __new__(cls, *args, **kwargs):
return super().__new__(cls, *args, **kwargs)
让我们再次看一下论点。这次我们将 cls 作为第一个参数而不是 self。它的名字可能已经暗示了它是什么,但让我们通过将 print 添加到构造函数来进行健全性检查。
现在,当我们创建对象时,我们应该看到那个神秘的cls是怎么回事。
dog = Dog()
# <class 'Dog'>
实际类将传递给对象的构造函数。但这是否可能?我们不是在第一段中确定典型的类只是不存储在运行时内存中的逻辑实体吗?嗯,我们当然做到了。但是,使用内置的 id() 方法将返回我们类的内存地址。因此,我们现在可以绝对肯定地说这是一个实际的对象。这是一等公民的示例 - 可以在代码中的任何操作中使用的实体,无论是将其作为参数传递给函数,从函数返回它,还是在运行时修改它。我们经常说在Python中一切都是对象,但是由于某种原因,很难接受类也是对象。
好的,但是如果一个类是一个对象,它不应该也以某种方式构造吗?不应该有一些…嗯,class?由于Python强大的内省能力,我们可以很容易地检查这一点。
dog_class = dog.__class__
print(dog_class)
# <class 'Dog'>
print(dog_class.__class__)
# <class 'type'>
现在它会变得有点奇怪。看起来我们的 Dog 类的类是类型 它可能看起来与我们用来获取对象类型的内置方法类型非常相似。
type('hello')
# <class 'str'>
它非常相似,因为它实际上是同一件事。在这一点上可能会变得有点混乱,但不要担心。当我们遇到麻烦时,Python 有另一个有用的内置方法让我们清理问题。
help(type)
"""
Help on class type in module builtins:
class type(object)
| type(object_or_name, bases, dict)
| type(object) -> the object's type
| type(name, bases, dict) -> a new type
...
"""
所以正如你所看到的,内置的类型方法是重载的,并且根据它作为参数得到的内容而表现不同(它并不真正符合 Python 的禅宗,对吧?“简单总比复杂好”?.哦,好吧,他们有他们的理由)。
记住这一点,我们已经缓慢但肯定地达到了本文的实际主题——元类。我们的内置类型方法是元类 - 用作创建类的蓝图的类。让我们尝试更深入。什么是元类的类?
obj.__class__.__class__.__class__
# <class 'type'>
幸运的是,就是这样。默认情况下,Python 中的所有类都将类型作为元类。然后使用元类创建类,然后使用该类创建对象。
但是我们可以用这些知识做什么呢?首先,我们可以使用类型的第二个功能来做一些在大多数其他编程语言中闻所未闻的事情。我们可以动态创建类。我们可以调用类型并为其提供三个参数。首先是我们新班级的名称。第二个是基元组 — 我们的新类应该派生的父类。第三个是包含我们类属性的字典。
Cat = type("Cat", (), {})
c = Cat()
c
# <__main__.Cat object at 0x7fcbb21eb358>
创建这样的类不是很常见的方案,但在某些情况下可能很有用。
此外,现在我们知道类型是一个元类,我们可以对其进行子类化以创建我们自己的元类,并为类构造函数提供我们自己的逻辑。
class GreatestMetaclass(type):
def __new__(metacls, name, bases, attrs):
x = super().__new__(metacls, name, bases, attrs)
# do some absolutely amazing stuff here...
return x
现在,如果我们想创建一个实现我们的 GreatestMetaclass 的类,我们可以这样做:
class GreatestClass(meta=GreatestMetaclass):
pass
GreatestClass 现在将实现我们在元类定义中编写的所有魔法。
我们可以使用元类做什么?很多事情。例如,您可以使用它来保留实现它的类的注册表。
registry = {}
class RegistryMetaclass(type):
def __new__(metacls, name, bases, attrs):
registry[name] = super().__new__(metacls, name, bases, attrs)
return registry[name]
元类在许多不同的Python框架中也大量使用。我们可以看看最大的Python Web框架Django及其在其ORM中广泛使用的元类(仅ModelBase元类构造函数的自定义逻辑就需要大约300行代码!)。
由于在用于反映数据库表的模型类中使用了 ModelBase 元类,我们可以简单地像这样声明我们的模型:
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
通过这样的实现,我们可以非常快速地用最少的代码创建模型。我们只是通过创建属性并为其分配字段类型来声明字段。与将类与 Django ORM 连接相关的所有工作都包含在元类的内部机制中,您可以专注于创建模型结构和应用程序逻辑。
另一个使用元类的流行库的例子是Django Rest Framework及其序列化程序。SerializerMetaclass 在序列化程序类中创建一个_declared_fields字典,其中包含作为属性包含在序列化程序类中的 Field 类的所有实例(或作为继承的超类中的属性)。然后,实现 SerializerMetaclass 的对象可以创建这些字段的深层副本并使用它们。
class UserSerializer(serializers.Serializer):
email = serializers.EmailField()
username = serializers.CharField(max_length=200)
许多 python 程序员可能使用的一个元类是抽象基类元类 (ABCMeta)。通常我们通过从 ABC 派生我们的类来在 Python 中创建抽象类,但实际上它只是一个以 ABCMeta 作为其元类的辅助类。创建 ABC 是为了在比简单继承更令人困惑的情况下绕过元类的使用。
from abc import ABC, ABCMeta
class MyABC(ABC):
pass
# this is the same thing as :
class AlsoABC(metaclass=ABCMeta):
pass
好的,所以早些时候我写过“有很多事情你可以使用元类”。真正的问题是——我们应该这样做吗?老实说,我只有少数情况认为创建一个元类会对我有所帮助。即便如此,我还是立即得出结论,还有很多其他解决方案要简单得多。我认为知道它们的存在很有用,因为它可以让你更好地了解 Python 是如何设计的,以及它是如何在引擎盖下工作的。但是,如果您想在代码中实际使用这些功能,您应该有充分的理由这样做。否则,您只会使您的实现更加模糊,这绝不是一个好主意。再一次,看看Python的禅宗:“如果实现很难解释,那就是一个坏主意”。