Python 元类介绍及实践

作者: 潘峰 / 2022-08-09 / 分类: Work

Python

Python 元类介绍及实践

一、 元编程和元类

1. 元编程技术简介

元编程 (Metaprogramming) 是一种计算机编程技术,这种技术可以使计算机程序将其它程序当做其数据进行处理。这意味着一个程序可以对另外一个程序进行读取、分析、生成、替换等操作,甚至是在程序运行时修改自身。

通俗地理解就是,普通的代码程序操作一般数据,元程序操作普通代码程序

2. 元编程技术价值

  • 可以最大限度减少计算机程序的代码量,尤其是模板代码,从而节约开发时间
  • 将某些计算从运行时前置到编译时,通过编译时程序生成一些代码并启用这些代码。

3. 元编程和反射

一门语言同时也可以作为自身的元语言的特性称为反射 (Reflection)。

4. 元类简介

元类 (Metaclass) 是面向对象编程语言中特有的概念,元类是一种实例对象是类的类。

通俗地理解就是,普通的类用来定义一般对象的属性和行为,元类用来定义普通的类及其实例对象的属性和行为

二、 Python 元类介绍

“Metaclasses are deeper magic than 99% of users should ever worry about.
If you wonder whether you need them, you don’t
(the people who actually need them know with certainty that they need them,
and don’t need an explanation about why).”
— Tim Peters (Author of Zen of Python)

话虽如此,对于一个 Python 开发者来说,了解这样一种强大的黑魔法还是很有必要的。

1. 默认元类 type

typeclass type(name, bases, dict, **kwds) 是 Python 中默认的元类,Python 作为一种动态语言,默认情况下,其所有的类对象均是由 type 在运行时动态创建的。

以下两种写法最终会生成相同的类(即 type 的实例对象)。

class Foo(object):
  foo = 0

  def func_foo():
    pass
Foo = type('Foo', (object,), dict(foo=0, func_foo=<function FooClass.func_foo at 0x3924f342f>))

参数说明:

  • name: 字符串类型,类名,会作为类的 __name__ 属性的值
  • bases: 元组类型,包含需要被继承的基类,会作为类的 __bases__ 属性的值
  • dict: 字典类型,包含类的属性和方法定义,会作为类的 __dict__ 属性的值,当在这之前可能会被拷贝或包装

2. 自定义元类

自定义元类主要是指,通过定义一个继承于 type 的元类,并重写 __new__ 方法来对类的创建过程实现定制。

其具体步骤分为:

  • 定义阶段:定义一个继承于 type 的元类,并编写 __new__ 方法
  • 使用阶段:在需要被定制的类中添加 metaclass=<your metaclass> 关键字参数
class FooMeta(type):
  def __new__(cls, name, bases, attrs)
    ... # 类创建过程中需要进行定制化处理逻辑
    return type.__new__(cls, name, bases, attrs)

class FooClass(metaclass=FooMeta):
  ...

另外,通过继承上面这个 FooClass 的类,也同样会执行元类中增加的定制化处理逻辑。

3. 类定义被执行时的步骤

  • 解析 MRO 条目
  • 确定类适当的元类
  • 执行元类中的代码程序
  • 准备类的命名空间
  • 执行类中的语句
  • 完成类的创建

三、 Python 元类实践

1. tortoise-orm

tortoise-orm 库中,所有的用于定义 DB 字段的 XXField 类均继承于 Field,在 Field 类中设置了 metaclass=_FieldMeta,其实现代码如下:

class _FieldMeta(type):
    def __new__(mcs, name: str, bases: Tuple[Type, ...], attrs: dict):
        if len(bases) > 1 and bases[0] is Field:
            # Instantiate class with only the 1st base class (should be Field)
            cls = type.__new__(mcs, name, (bases[0],), attrs)
            # All other base classes are our meta types, we store them in class attributes
            cls.field_type = bases[1] if len(bases) == 2 else Union[bases[1:]]  # type: ignore
            return cls
        return type.__new__(mcs, name, bases, attrs)

稍作分析,不难看出在通过 type() 进行字段类创建时,主要对字段类继承的类(即:typebases 参数)进行了处理。
当字段类继承于 Field 和其它类且 MRO 顺序中 Field 位于首位时,设置当前字段类的字段类型 field_type 为继承的其它类的类型,如:str int 等。

2. test-x

在自研 test-x 测试框架中,所有用于定义一个关键字库的关键字类最终都必须继承于一个关键字基类,在这个基类中,定义了用于控制关键字方法的元类 KeywordMeta,其具体实现如下:

class KeywordMeta(type):
    def __new__(mcs, name, bases, attrs):
        ori = type.__new__(mcs, name, bases, attrs)

        if name != "KeywordBase":
            for func_name, func in attrs.items():
                # 判断方法是否为关键字方法
                if inspect.isroutine(func) and not func_name.startswith('_'):
                    # 控制关键字方法必须编写文档
                    if settings.KW_CHECK_DOC and not func.__doc__ and \
                            not any(func_name.startswith(_) for _ in settings.KW_NOT_STARTS):
                        raise KeywordNoDocError(f"关键字方法 <{name}.{func_name}> 必须编写文档,请补充!")

                    # 控制业务关键字上下文变量参数必须默认为 None
                    if sign := inspect.signature(func):
                        for k, v in sign.parameters.items():
                            if k.startswith(settings.CTX_VAR_PREFIX) and v.default is not None:
                                raise KeywordValueError(f"关键字方法 <{name}.{func_name}> 的上下文变量 <{k}> 默认值必须设置为 None !")

                    # 解析实参中包含的上下文变量
                    setattr(ori, func_name, reset_func_attribute(func))
        return ori

其作用是,在通过 type() 进行关键字类的创建时,获取到关键字类的创建数据(即:typename bases dict 参数)。
当关键字类名称 name 不等于基类 KeywordBase 名称,则认为是普通关键字类,进入处理逻辑。
在元类处理逻辑中,通过对类的成员数据 dict 进行解析和判断,这里做了两件简单的事情:

  1. 控制关键字方法必须编写文档
  2. 控制关键字上下文变量形参默认值必须设置为 None

参考来源
https://en.wikipedia.org/wiki/Metaprogramming 《Wiki-Metaprogramming》
https://www.zhihu.com/question/23856985 《知乎:怎么理解元编程》
https://en.wikipedia.org/wiki/Metaclass 《Wiki-Metaclass》
https://docs.python.org/3/reference/datamodel.html#metaclasses 《Python-metaclasses》
https://peps.python.org/pep-3115/ 《PEP 3115 – Metaclasses in Python 3000》
http://c.biancheng.net/view/2293.html 《Python MetaClass 元类详解》