Python 中的元类

Python 中的元类

《流畅的 Python》一书中类元编程章的开头第一段话就提到:

(元类)是很深奥的知识,99%的用户都无需关注。如果你想知道是否需要使用元类,我告诉你,不需要(真正需要使用元类的人确信他们需要,无需解释原因)。 ——Tim Peters

事实上确实如此,在日常的编码过程中,几乎没有遇到过不得不使用元类的情况,或许只有编写框架的开发者们会用到元类。

说到元类,就不得不提到特殊方法__new__。或许你已经知道,特殊方法__init__并不是类的构造方法,而是类的初始化方法,真正的构造方法是__new__,书中的第19章作者编写FrozenJSON类的那一节已经详细地解释了这一点。

元类与装饰器有点异曲同工。比如@foo这个语句中,函数foo就是装饰器函数,但如果是@foo(arg)的话,你应该能认识到装饰器函数并不是foo,而是foo(arg)foo(arg)运行之后返回了一个装饰器函数。我们可以把foo函数理解为一个装饰器工厂,把传给它的参数arg想象成原材料,生产出的产品foo(arg)就是装饰器,这么一想就能理解了,而且也就理解为什么有的装饰器函数中函数被嵌套得一层又一层了。而元类就是负责生产类的类工厂函数。

初次接触__new__时很容易一脸懵逼:这是什么?这做了什么?这返回了什么?为什么要这样做?

我简单地做了一下总结,大致上可以这么理解:

__init__:类的初始化方法
__new__:类的构造方法

__init__方法不允许返回任何值
__new__方法必须返回一个实例

__new__方法返回的是类本身的实例,则返回的实例将作为第一个参数(即self)传递给__init__方法并进行调用
__new__方法返回的是其他类的实例,则不会调用__init__方法

配合书中最终版的FrozenJSON代码来看就更容易理解了:

from collections import abc

class FrozenJSON:

    def __new__(cls, arg): 
        # 如果arg是字典 返回一个FrozenJSON实例
        # 调用基类的__new__方法 代入的唯一的参数是类本身
        # 这样就可以返回一个正常的FrozenJSON实例了 之后就会调用__init__方法
        # 参数arg也会顺便传给__init__方法
        if isinstance(arg, abc.Mapping):
            return super().__new__(cls) 
        # 如果arg是列表
        # 注: 当__new__返回的是其他类的实例时 不会调用__init__方法
        elif isinstance(arg, abc.MutableSequence): 
            return [cls(item) for item in arg]
        # 如果arg既不是字典也不是列表 则直接返回
        else:
            return arg

    def __init__(self, mapping):
        """ 下略 """

可惜的是FrozenJSON类没有突出类的工厂函数的概念,如果要理解类工厂函数的用法,可以参考廖老师的Python教程中使用元类一节。