最近在阅读《Python Cookbook》,看到第九章的时候有些关于元编程的地方还是不太了解,特别是有关元类的那一部分,所以找了一下网上一些教程文档,弄通了元类后,自己再总结一下有关元类的一些基本知识以及常见的用法。
其实,元类并不难理解,只要我们走出类的思维框架,以更高的层次去看待类本身是如何构造出来的,就弄懂了元类的概念。进而使用元类去“干涉”类。
1. 从type说起
我们都清楚,创建一个实例(Instance)的方式是通过调用类(Class)的构造函数(Constructor):
>>> class A:
... def __init__(self, name):
... self.name = name
... def spam(self):
... print("name:", self.name)
...
>>> a = A("jezachen")
>>> a.spam()
name: jezachen
我们也知道,type()是一个可以查看一个变量或者类型的类型,从以下解释器的运行结果可以知道,a是一个实例,它的类型是A;而A是一个类,它的类型是type:
>>> type(a)
<class '__main__.A'>
>>> type(type(a))
<class 'type'>
可以发现,type本身是一个class,而class A的类型却是type,那么是否可以说明类A实质上是type的一个实例呢?
答案是正确的,type本身是一个class,所谓的type()函数,实质上也具有构造函数的功能,可以直接通过调用type()函数去动态生成一个等价的A类:
>>> def __init__(self, name):
... self.name = name
...
>>> def spam(self):
... print("name:", self.name)
...
>>> A = type("A", (object,), {"__init__":__init__, "spam":spam})
>>> a = A("jeza chen")
>>> a.spam()
name: jeza chen
Python作为一个动态语言,强大的特性不得不叹为观止。我们甚至可以通过一个type()函数动态生成一个类,这也意味着在Python中似乎有一个比类(Class)更加抽象、高级的层次,似乎如上帝一般创建世间万物。这就是Python元编程一个重要特性——元类(Metaclass)。
在说明元类前,我们还是要弄清楚type()函数(亦type类的构造函数)的参数意义,以弄懂后面元类的__new__函数打下基础:
type(name, bases, dict)
其中,
name
:字符串类型,指定类的名字;
bases
:元组(tuple)类型,指定所继承的类。在上文,这个参数是(object,)
(注意,逗号不能省略,否则不是元组),意味着类A继承于object,也就是类A就是所谓的新式类;
dict
:字典(dict)类型,指定该类的体(body)所包含的所有属性(attributes)和方法(method)。在上文,该参数是{"__init__":__init__, "spam":spam}
,而作为键值的__init__
、spam
函数则是在调用type()函数前定义完毕。
因此,再强调一遍,下面两个定义类A的方式是等价的:
>>> class A:
... def __init__(self, name):
... self.name = name
... def spam(self):
... print("name:", self.name)
...
A = type("A", (object,), {"__init__":__init__, "spam":spam})
事实上,也不难推理得知,Python解释器在遇到class ...
关键字时会一步步解析类的内容,最终调用 type(...)
(准确说是指定的元类)的构造函数来创建类。而我们后面所学的元类知识,其实是掌握如何去”干涉“元类的构造函数(或__new__函数),从而”批量制造“出我们想要的类。
弄懂上面这三个参数的意义后,我们就比较容易过渡到元类的学习了。
2. 初窥元类(Metaclass)
通过上面我们可以知道,一个类可以使用type()函数(即type类的构造函数)来动态创建。那么,我们是否能自己定义一个继承于type的类,从而控制类的创建过程呢?答案当然是可以的。
先说实例(Instance)与类(Class)的关系,一个实例,可以通过类来创建出实例。即先定义类,再创建实例;
那么,同理,一个类(Class),可以通过元类(Metaclass)来创建出类。即先定义元类,在创建类。
换句话说,类,就是元类的一个实例。
而元类,要么是type本身,要么是继承于type的子类。
元类可以说是Python中最难理解和运用的一部分,但其丰富的特性使Python编程带来了更多的可能性与创造性。在正常情况下,普通的程序员不会碰到太多的元编程;但是,如果你对一些大型的框架感兴趣的话,元类是不得不接触到的重要内容。
在详细讲述元类之前,我们可以通过以下代码,感受元类的魅力:
在某种情况下,我们可能需要在某些类上实现单例模式(Singleton)。在一般情况下,我们可能使用这种方式来实现:
class Spam:
def __new__(cls, *args, **kwargs):
if not hasattr(Spam, "_instance"):
setattr(Spam, "_instance", object.__new__(cls))
return getattr(Spam, "_instance")
def __init__(self):
print("Creating Spam")
运行解释器,可以得到如下结果:
>>> a = Spam()
Creating Spam
>>> b = Spam()
Creating Spam
>>> a is b
True
我们可以通过__new__()
方法成功实现单例模式,但”Creating Spam”的信息却输出了两次。虽然在这里不影响使用,但在一些场合中,极有可能出现多次初始化(customize)的情况,这是我们所不想看到的。当然,我们也可以在__init__()
方法中增加一些判定的逻辑以消除这种情况,亦或是通过一个类方法(classmethod)绕过__init__()
方法;而且,如果有多个类需要实现单例模式,则要重复多次这些步骤,增加不必要的工作量。
因此,我们可以使用元类去更好地实现单例模式:
class Singleton(type):
def __init__(self, *args, **kwargs):
self._instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
if self._instance is None:
self._instance = super().__call__(*args, **kwargs)
return self._instance
else:
return self._instance
class Spam(metaclass=Singleton):
def __init__(self):
print("Creating Spam")
运行解释器,看看结果:
>>> a = Spam()
Creating Spam
>>>
>>> b = Spam()
>>> a is b
True
从这里我们可以看到,使用元类不仅可以实现单例模式,且初始化方法只调用了一次!与此同时,如果有多个类需要实现单例模式,只要在该类的定义中指定metaclass为Singleton,在__init__()
方法写好对应的初始化逻辑即可,而无需过多的繁琐步骤。这样何乐而不为呢?
3. 元类是如何定义的
在上面我们已经知道,类是元类的一个实例。只要我们定义好元类,就可以按照特定的方式去“批量”生成我们想要的类(如具有单例模式的类)。那么,我们该如何定义一个元类呢?
首先我们要清楚的是,元类需要继承于type类,即:
class MetaclassTest(type):
...
这样,一个元类就诞生出来了,但这个和type类是无异的。我们还需要重载一些方法,去实现更多的功能:
3.1 __new__()
方法
同理于类创建实例,元类先调用__new__()
方法创建一个类(即元类的实例),在__init__()
方法对创建出来的类进行初始化(或定制)操作。在该节我们先解释__new__方法所接收到的各个参数的定义:
__new__(cls, clsname, bases, clsdict)
其中:
cls
: 元类
clsname
: 类的名字
bases
: 继承的父类的集合
clsdict
: 字典对象,类的属性/方法集合
为方便说明,我们可以定义一个元类,用来打印每个参数的内容:
class PrintParamMeta(type):
def __new__(cls, clsname, bases, clsdict):
print("PrintParamMeta.__new__()")
print(cls, clsname, bases, clsdict, end="\n")
return super().__new__(cls, clsname, bases, clsdict)
class BaseOfA:
pass
class A(BaseOfA, metaclass=PrintParamMeta):
def spam(self):
print(self.name)
def __init__(self, name, age):
print("A.__init__()")
self.name = name
self.age = age
age = 31
def print_age(self):
print(self.age)
if __name__ == '__main__':
print("execute a = A()")
a = A("a", 22)
a.spam()
a.print_age()
print("execute aa = A()")
aa = A("aa", 25)
aa.spam()
aa.print_age()
a.print_age()
输出结果:
PrintParamMeta.__new__()
<class '__main__.PrintParamMeta'>
A
(<class '__main__.BaseOfA'>,)
{'__qualname__': 'A', '__module__': '__main__', 'age': 31, 'print_age': <function A.print_age at 0x7f95790f5b70>, 'spam': <function A.spam at 0x7f95790f5a60>, '__init__': <function A.__init__ at 0x7f95790f5ae8>}
execute a = A()
A.__init__()
a
22
execute aa = A()
A.__init__()
aa
25
22
可以看到,元类PrintParamMeta的__new__()
函数只调用了一次,即发生在类A的创建期间(注意并不是A的实例a或aa的创建期间);而实例a或实例aa的创建并不会调用类A的__new__()
函数(因为此时类A已经创建出来了)。
同时,通过元类PrintParamMeta的__new__()
函数所打印的信息可以看到,cls
参数为元类PrintParamMeta;而clsname
参数为一个字符串,是类A的名字;bases
参数为一个元组(tuple),里面的元素是类A的父类;clsdict
则是一个字典,里面存储着类A的属性/方法。
因此,我们可以通过定制元类的__new__()
方法,无需改动类的定义,去给类实现额外的功能。
3.2 __init__()
方法
在上面我们知道,Python先通过元类的__new__()
方法创造出一个类,在调用元类的__init__()
方法去初始化这个类。这和类创造实例的过程是差不多的。__init__()
方法的各个参数和__new__()
方法是差不多的,除了第一个参数从cls变成了self(因为此时类对象已经创建出来了)。
__init__(self, clsname, bases, clsdict)
在这里举个例子,如果我们想阻挠在类定义中包含大小写混用的方法名(驼峰命名)这种行为,可以编写以下元类:
class NoMixedCaseMeta(type):
def __init__(self, clsname, bases, clsdict):
for name in clsdict:
if name.lower() != name:
raise TypeError("Do not define MIXED-CASE Methods or attributes.")
super().__init__(clsname, bases, clsdict)
我们在命令行定义两个类测试一下:
>>> class test(metaclass=NoMixedCaseMeta):
... def foo_bar(self):
... print("Hello")
...
>>> class test2(metaclass=NoMixedCaseMeta):
... def fooBar(self):
... print("Hello")
...
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "/home/jeza/PycharmProjects/PythonCookbook/metaclass_test.py", line 61, in __init__
raise TypeError("Do not define MIXED-CASE Methods or attributes.")
TypeError: Do not define MIXED-CASE Methods or attributes.
可以看到,在创建类test2对象期间,因为在元类NoMixedCaseMeta的__init__()
方法发现了类test2的一个方法fooBar混用了大小写,因此抛出了一个TypeError异常,阻止了类test2的创建。
至于在元类中定义__new__()
还是__init__()
,取决于我们打算如何使用得到的结果类。__new__()
会在类创建之前先得到调用,当元类想以某种方式修改类的定义时(通过修改类字典中的内容,即前文的clsdict)一般会有这个方法。而__init__()
方法会在类已经创建完成后才得到调用,如果想编写代码同完全成形(fully formed)的类对象打交道,那么重新定义__init__()
方法会很有用。在这里,我们先不具体描述它们之间的区别。
3.3 __call__()
方法
在上面我们可以看到,在元类中定义__new__()
方法或者__init__()
方法可以“干涉”类的创建过程,其两者区别在于“干涉“的时机(即__new__()
方法调用时,类还没创建出来;而__init__()
调用时,类已经创建出来了,该方法是对成形的类对象进行初始化或定制操作。从这两个方法的第一个参数可以看出两者的调用时机的不同)。那么,这一节所说的__call__()
方法,究竟是什么东西呢?
我们清楚,如果一个类定义了__call__()
方法,那么其创建出来的实例是可以像函数那样直接调用,即:
>>> class CallableTest:
... def __init__(self, name):
... self.name = name
... def __call__(self, age):
... print('{} is {} years old.'.format(self.name, age))
...
>>>
>>> a = CallableTest("jeza")
>>> a(21)
jeza is 21 years old.
那么,如果一个元类定义了一个__call__()
方法,这就意味着通过该元类创建的类是可调用的——而类所谓的调用,其实就是类的构造函数。所以,通过重载元类的__call__()
方法,我们就可以”干涉“类实例的创建过程。
在”2. 初窥元类“这一章节我们已经接触到__call__()
方法了,在那个例子中,我们在实例创建之前去”干涉“它的过程,如果发现先前已经创建过实例_instance了,就直接返回_instance,从而绕过类的__init__()
方法,实现单例模式。
那在这里我们探讨一下:实例创建的过程中,元类的__call__()
方法,类的__new__()
方法和__init__()
方法之间的调用顺序。
第一种,通过传统的函数调用方式实例化一个类,它们之间的调用顺序。我们编写一下代码:
class Meta(type):
def __call__(self, *args, **kwargs):
print("Meta.__call__")
return super().__call__(*args, **kwargs) # 该语句千万不能漏
class A(metaclass=Meta):
def __new__(cls, *args, **kwargs):
print("A.__new__")
return super().__new__(cls, *args, **kwargs) # 该语句千万不能漏
def __init__(self):
print("A.__init__")
if __name__ == '__main__':
a = A()
输出的结果如下:
Meta.__call__
A.__new__
A.__init__
可知,当使用函数调用的方式实例化一个类,会先调用元类的__call__()
方法,随后再调用类的__new__()
方法,最后再使用__init__()
方法进行最后的实例化操作。
第二种,如果使用类的__new__()
方法直接创建一个实例:
>>> a = A.__new__(A)
A.__new__
可知,使用类的__new__()
方法不仅可以绕过了类的__new__()
方法,还绕过了元类的__call__()
方法。从中我们知道,使用函数调用的方式实例化一个类就相当于执行元类的魔术方法__call__()
。
4. 类的创建过程
要了解元类的作用,我们就需要了解 Python中类的创建过程 ,如下:
-
当Python见到
class
关键字时,会首先解析class ...
中的内容。例如解析基类信息,最重要的是找到对应的元类信息(默认是type
)。 -
元类找到后,Python 需要准备命名空间namespace(也可以认为是上节中
type
的dict
参数)。如果元类实现了__prepare__ ()
函数,则会调用它来得到默认的 namespace 。 -
之后是调用
exec()
来执行类的body,包括属性和方法的定义,最后这些定义会被保存进 namespace。 -
上述步骤结束后,就得到了创建类需要的所有信息,这时Python会调用元类的
__new__()
函数、__init__()
函数来真正创建类。
如果你想在类的创建过程中做一些定制(customization)的话,创建过程中任何用到了元类的地方,我们都能通过覆盖元类的默认方法来实现定制。这也是元类“无所不能”的所在,它深深地嵌入了类的创建过程。
5. 聊聊__prepare__()
函数
在第4节我们知道,元类的__prepare__()
方法会在类定义一开始的时候立刻得到调用,且需要返回一个映射性对象(mapping object)供处理类定义体(body)时使用。
具体来说,__prepare__()
为一个类方法(classmethod),调用时以类名和基类名称作为参数,即:
__prepare__(cls, clsname, bases)
那么,这个方法到底有什么具体应用情景呢?在一些情况下,我们可能序列化某些对象,序列化成csv表格等。此时我们可能想自动记录下属性和方法在类定义的顺序,这样就能利用这个顺序来完成csv表格的序列化操作。
相关代码如下所示:
from collections import OrderedDict
class Typed:
_expected_type = type(None)
def __init__(self, name=None):
self._name = name
def __set__(self, instance, value):
if not isinstance(value, self._expected_type):
raise TypeError('Expected ' + str(self._expected_type))
instance.__dict__[self._name] = value
class Integer(Typed):
_expected_type = int
class Float(Typed):
_expected_type = float
class String(Typed):
_expected_type = str
class OrderedMeta(type):
def __new__(cls, clsname, bases, clsdict):
d = dict(clsdict)
order = []
for name, value in clsdict.items():
if isinstance(value, Typed):
value._name = name
order.append(name)
d['_order'] = order
return type.__new__(cls, clsname, bases, d)
@classmethod
def __prepare__(mcs, name, bases):
return OrderedDict()
class Structure(metaclass=OrderedMeta):
def as_csv(self):
return ','.join(str(getattr(self, name)) for name in self._order)
class Stock(Structure):
name = String()
shares = Integer()
price = Float()
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
这里用到了描述器(descriptor),如果不太清楚描述器可以自行谷歌学习。
我们运行下解释器,看看结果:
>>> s = Stock("GOOG", 100, 49.1)
>>> s.name
'GOOG'
>>> s.as_csv()
'GOOG,100,49.1'
>>> s = Stock("AAPL", 'a lot', 9.22)
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "/home/jeza/PycharmProjects/PythonCookbook/metaclass_test2.py", line 56, in __init__
self.shares = shares
File "/home/jeza/PycharmProjects/PythonCookbook/metaclass_test2.py", line 12, in __set__
raise TypeError('Expected ' + str(self._expected_type))
TypeError: Expected <class 'int'>
在这里,我们可以看到as_csv方法可以严格按照类定义体各个属性的顺序输出成字符串的形式。原因就在于元类的__prepare__()
方法返回了一个有序字典OrderDict的实例,因此类中各个属性间的顺序可以方便得到维护。这就是__prepare__()
方法的重要用处之一。
值得注意的是,当创建最终的类对象的时候,我们需要在元类的__new__()
方法中将这个字典转换成一个合适的dict实例才行,这就是为什么需要d = dict(clsdict)
的原因。所以,__prepare__()
方法放回的字典更多是充当一个中间的角色,用于将类定义体的各个属性和方法以自己所需的形式存储下来,并在__new__()
方法中完成自己所需的操作。
6. 总结
-
对象的类型称为类,类的类型就称为元类。
-
Python 中对元类实例化的结果就是“类”,这个过程是动态的。
-
在定义类时可以指定元类来”干涉“类的创建过程。
-
Previous
[Python 进阶] Python描述器的介绍及基于描述器协议的属性(property)、方法(method)简介 -
Next
[Python] Python-从装饰器(decorator)谈到闭包(closure)