跳至主要内容

Python 元类入门

在本教程中,了解什么是元类、如何在 Python 中实现它们,以及如何创建自定义元类。
· 6分钟 阅读

使用 AI 探索

在Python中,元类是“类的类”,用于定义类的行为。类本身是元类的实例。Python 中的类定义了该类实例的行为。要充分理解元类,您需要具备使用 Python 类的经验。在深入探讨元类之前,我们先理清几个概念。

Python 中的一切都是对象

class TestClass():
    pass

my_test_class = TestClass()
print(my_test_class)
<__main__.TestClass object at 0x7f6fcc6bf908>

Python 类可以动态创建

Python 中的 type 使我们能够获取对象的类型。我们可以继续检查上面创建的对象的类型。

type(TestClass)
type
type(type)
type

等等,发生了什么?我们可能会期望上面创建的对象类型是 class,但结果并不是。先记住这个问题,我们稍后会解释。我们还注意到 type 本身的类型也是 type。它是 type 的一个实例。type 的另一个“魔法”是可以让我们动态创建类。下面展示如何做到这一点。下面的 DataCamp 类可以用 type 如下创建:

class DataCamp():
    pass
DataCampClass = type('DataCamp', (), {})
print(DataCampClass)
print(DataCamp())
<class '__main__.DataCamp'>
<__main__.DataCamp object at 0x7f6fcc66e358>

在上面的示例中,DataCamp 是类名,而 DataCampClass 是保存该类引用的变量。使用 type 时,我们可以通过字典传入类的属性,如下所示:

PythonClass = type('PythonClass', (), {'start_date': 'August 2018', 'instructor': 'John Doe'} )
print(PythonClass.start_date, PythonClass.instructor)
print(PythonClass)
August 2018 John Doe
<class '__main__.PythonClass'>

如果我们希望 PythonClass 继承自 DataCamp 类,可以在使用 type 定义类时,将其作为第二个参数传入:

PythonClass = type('PythonClass', (DataCamp,), {'start_date': 'August 2018', 'instructor': 'John Doe'} )
print(PythonClass)
<class '__main__.PythonClass'>

理清这两个概念后,我们会意识到 Python 是通过元类创建类的。我们已经看到,Python 中的一切都是对象,而这些对象是由元类创建的。每当我们用 class 创建一个类时,幕后的元类会负责完成类的创建。我们已在上文中看到 type 的实际表现。这类似于 str 创建字符串、int 创建整数。在 Python 中,___class__ 属性可以让我们查看当前实例的类型。下面创建一个字符串并检查其类型。

article = 'metaclasses'
article.__class__
str

我们也可以使用 type(article) 来检查类型。

type(article)
str

当我们检查 str 的类型时,会发现它的类型是 type。

type(str)
type

当我们检查 float、int、list、tuple 和 dict 的类型时,也会得到类似的输出。这是因为这些对象的类型都是 type。

print(type(list),type(float), type(dict), type(tuple))
<class 'type'> <class 'type'> <class 'type'> <class 'type'>

我们已经看到 type 会创建类。因此,当我们检查 __class__ 的 __class__ 时,它应返回 type。

article.__class__.__class__
type

创建自定义元类

在 Python 中,我们可以在类定义中传入 metaclass 关键字来自定义类的创建过程。也可以通过继承已传入该关键字的类来实现。

class MyMeta(type):
    pass

class MyClass(metaclass=MyMeta):
    pass

class MySubclass(MyClass):
    pass

如下所示,MyMeta 类的类型是 type,而 MyClass 和 MySubClass 的类型是 MyMeta。

print(type(MyMeta))
print(type(MyClass))
print(type(MySubclass))
<class 'type'>
<class '__main__.MyMeta'>
<class '__main__.MyMeta'>

当定义一个类而未指定元类时,将使用默认的 type 元类。如果给定了元类,且它不是 type() 的实例,那么它将被直接用作元类。

__new__ 和 __init__

元类也可以通过下面两种方式之一来定义。我们会在下文解释它们的区别。

class MetaOne(type):
    def __new__(cls, name, bases, dict):
        pass

class MetaTwo(type):
    def __init__(self, name, bases, dict):
        pass

__new__ 用于在类被创建之前设定 dict 或 bases 元组。__new__ 的返回值通常是 cls 的一个实例。__new__ 允许不可变类型的子类自定义实例创建。它可以在自定义元类中被重写,以定制类的创建过程。__init__ 通常在对象创建之后被调用,用于初始化对象。

元类的 __call__ 方法

根据官方文档,我们还可以在元类中定义自定义的 __call__() 方法,以在类被调用时实现自定义行为,从而覆盖其他类方法。

元类的 __prepare__ 方法

根据 Python 的数据模型文档

一旦确定了合适的元类,就会准备类的命名空间。如果元类具有 __prepare__ 属性,则按 namespace = metaclass.__prepare__(name, bases, **kwds) 调用(其中额外的关键字参数(如果有)来自类定义)。如果元类没有 __prepare__ 属性,则类命名空间会被初始化为一个空的有序映射。 - docs.python.org

使用元类实现单例设计

这是一种将类的实例化限制为唯一对象的设计模式。例如,在设计用于连接数据库的类时,这可能很有用。此时我们可能只需要连接类的一个实例。

class SingletonMeta(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(SingletonMeta,cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class SingletonClass(metaclass=SingletonMeta):
    pass

结论

在本文中,我们了解了什么是元类,以及如何在 Python 编程中实现它们。元类可用于日志记录、在创建时注册类、性能分析等场景。它们看起来相当抽象,您可能会想自己是否真的需要用到它们。长期的 Pythonista,Tim Peters 给出了最佳回答。

“元类是一种比 99% 的用户需要关心的更深层的魔法。如果您在想自己是否需要它们,那就不需要(真正需要的人会非常确定自己需要它们,也不需要解释原因)。” - realpython

如果您想进一步学习 Python,请开始 DataCamp 的 Python Programming 技能路径,并查看我们的 Python Classes 教程。

参考资料

链接一

链接二

主题