Python 系列学习十五:面向对象编程 - 元类(Metadata Class)

前言

打算写一系列文章来记录自己学习 Python 3 的点滴;本章主要介绍 Python 面向对象编程中有关元类的相关内容;

本文为作者的原创作品,转载需注明出处;

type

正如之前的文章中,查看类的类型 以及 查看实例的类型 所描述的那样,type()可以查看对象的类型,不管是类还是实例;不过type()还有一个更为强大的功能,那就是在运行过程中动态的创建出一个类class对象;

这也就是静态语言和动态语言最大区别所在的地方,静态语言的类在编译的时候就创建好了,动态语言,是可以在程序的执行过程之中,边执行边创建一个新的类出来;(不过这个边界也越来越模糊了,典型的静态语言 Java,在动态执行的过程中,也可以通过 ClassLoader 类加载器在程序执行的过程当中,通过字节码的方式,动态的加载(创建)一个类 Class 对象 );

那么,下面,笔者就带领大家来看看 Python 是如何通过type()在程序的运行时动态的创建出一个 Class 对象的;

  1. 先定义出一个函数

    1
    2
    >>> def fn(self, name='world'): # 先定义函数
    ... print('Hello, %s.' % name)
  2. 通过type()生成一个新的 Class 对象

    1
    2
    3
    4
    >>> Hello = type('Hello', (object,), dict(hello=fn))
    >>> h = Hello("everyone")
    >>> h.hello()
    Hello, everyone.

    可见,我们通过type()动态的构造出来了一个 class 对象 Hello;在通过该对象实例化了一个对象 h,调用实例方法 hello() 得到了对应的输出;

那么type()函数所对应的参数是什么意义呢?type()函数需要输入三个位置参数,分别对应的是,

  1. 类名: str 类型
    所以上述所动态创建出来的 class 的类名为Hello
  2. 所继承的父类的集合: tuple 类型
    注意,上述例子中 (object,) 的写法表示当该 tuple 有且只有一个对象的时候的写法;
  3. 创建 class 的函数: dict 类型
    第三个参数接收的是一个 dict 对象,其中的键值对的表示的是方法名表示方法的引用

metaclass

先来看一个例子,首先定义一个 ListMetaclass,

1
2
3
4
5
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
print('ListMetaclass gets invoked ~, parameters: cls: %s, name: %s, bases: %s, attrs: %s' % (cls, name, bases, attrs) )
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)

然后,我们定义一个 MyList 对象,该对象继承自两个父类,一个是 List 还有一个就是 metaclass

1
2
3
class MyList(list, metaclass=ListMetaclass):
description = 'My List'
pass

OK,现在大家都还不清楚 metaclass 到底能干嘛,看看下面的这个执行的例子,

首先,我们初始化 MyList class

1
2
3
4
5
>>> class MyList(list, metaclass=ListMetaclass):
... description = 'My List'
... pass
...
ListMetaclass gets invoked ~, parameters: cls: <class '__main__.ListMetaclass'>, name: MyList, bases: (<class 'list'>,), attrs: {'__module__': '__main__', '__qualname__': 'MyList', 'description': 'My List'}

可见,当 Python 解释器在解释执行到 MyList 的时候,调用了 ListMetaclass 中的 __new__ 方法,该方法内部,通过 lambda 为 MyList class 添加了一个新的方法 add(self, value),并将该方法通过 type.__new__() 方法注入;回顾一下 type 的作用,就是在程序动态的执行过程当中,生成一个新的 class 并返回,该 class 就是最终通过 Python 解释器所得到的 MyList class;突然让我想到了 Java 的字节码技术,Java 可以通过修改和添加字节码的方式在编译期、执行期通过修改 class 的字节码已达到动态的修改 class 的目的,比较典型的就是 ASM 字节码技术;所以,Python 中的 ListMetaclass 可类比为 Java 的字节码技术,在运行期,动态的修改 class 的属性和功能;这也就是 Python 的 Metaclass 的魔力所在了;最后,注意构建 ListMetaclass 的时候参数 attrs 的输出,里面会包含 MyList 的类属性,‘description’: ‘My List’;这一点尤为重要,提供了将当前类的定义当做模板来构建顶层设计的可能,正式这一点,使得 metadata 大量的使用在了 Python 的 ORM 领域;

然后,我们测试一下被 ListMetaclass 所修饰的 MyList 对象;

1
2
3
4
>>> l = MyList()
>>> l.add(5)
>>> l
[5]

我们知道,原生的 list 对象是不包含 add 方法的,这样,我们通过元类 metaclass 的方式动态的为 list 的子类 MyList 扩展出了 add 方法;

读者会问,弄得这么复杂干嘛呢?直接在 MyList 对象中定义一个 add() 方法不就 OK 了?是的,上面的这个 case 的确如此了;但是,有没有想过,如果我有几十上百个拥有同种类型的 class,典型的就是 ORM 中的 Object class,每一个 class 都需要实现增、删、查、改的方法;所以 metaclass 提供了对同类 class 的一系列公用方法的抽象;对了,你又会问了,写一个抽象类不就 Ok 了吗?是的,那也是一种实现方法,而且在 Java 中屡见不鲜,而且有一种专门的模式,Template 模式就是由此而来的;不过,Template 或者抽象类的方式有一个弊端,就是抽象类不知道子类内部的元素的定义,不能讲子类的定义当做输入元素来作为顶层设计的模板,而这一点,只有 metadata 可以做到;