纸上得来终觉浅,绝知此事要躬行。
[Tim Peters]:元类是深奥的知识,99%的用户都无需关注。如果你想知道是否需要使用元类,我告诉你不需要,真正需要使用元类的人确信他们需要,无需解释原因。
在介绍元类之前,我们需要补充下基础知识,那就是在Python
一切皆对象这个概念。字符串、数字、字典、列表、集合、函数、类等等,都是对象。而我们发现,这个类型的类型其实都是type
,这就是元类。
In [1]: type('abc')
Out[1]: str
In [2]: type(1)
Out[2]: int
In [3]: type({})
Out[3]: dict
In [4]: type([])
Out[4]: list
In [5]: type({1, 2, 3})
Out[5]: set
In [6]: classA: ...
In [7]: a = A()
In [8]: type(a)
Out: __main__.A
In [9]: type(str), type(int), type(dict), type(list), type(dict), type(A)
Out[9]: (type, type, type, type, type, type)
- 元类的创建
在这里就引出了元类这个概念,而元类就是用于创建所有的类型的,系统的默认元类就是type
。而元类就是类的类,怎么理解呢,请看下面这个图。
从图中,我们可以看出实例是类的实例,而类是元类的实例。而type
这个类除了可以返回对象的类型,还可以被用来动态的创建类。如下,可以看到type
的三种用法,第二种就是返回对象的类型的用法,其余两种就是元类的用法。
In [1]: type?
Init signature: type(self, /, *args, **kwargs)
Docstring:
type(object_or_name, bases, dict)
type(object) -> the object's type
type(name, bases, dict) -> a new type
Type: type
我们之前定义类就是使用class
关键字,进行创建的。现在,我们可以使用使用type
来在一行中创建元类了。这个type
接收三个参数,第一个参数是指定类的名称,第二个参数表示需要继承父类的元组,第三个参数指定类里面带的属性。可以看到,两则创建的类X
是一样的。
In [1]: classX:
...: a = 1
...:
In [2]: x = X()
In [3]: x.a
Out[3]: 1
In [4]: X
Out[4]: __main__.X
In [1]: X = type('X', (), {'a': 1})
In [2]: x = X()
In [3]: x.a
Out[3]: 1
In [4]: X
Out[4]: __main__.X
这里就演示了一个需要继承父类的type
用法,继承了Base
这个父类。注意的是,在Python3
中可以不写留空,但是在Python2
中需要继承自object
这个类,不然会报错的。
In [5]: classBase:
...: pass
...:
...:
In [6]: X = type('X', (Base,), {'a': 1})
而在Python2
中,需要使用添加object
类就可以了。
Foo = type("Foo", (object,), {"hello": hello})
- 元类的用途
而元类有什么用途呢?其实就是我们之前所说的,用于动态的创建一个类,而不是用一个 class 的语法去定义。在日常工作中,就有这种需要动态创建类的需求,如pop
数据的时候。
现在就有一个需要pop
的函数func
,它接收一个参数,而这个参数是某个类的实例。在函数体内,我需要访问这个实例的属性和方法。而现在如何单步调试或者测试这段代码呢?需要我们手动造一个instance
给它传递进去。
一个普通的做法就是,我们再写一个工厂函数去返回一个类,也就是把类写到函数里面,类还是使用class
关键字区创建的。
通过这个示例,我们可以看出其存在两个问题。第一个就是,这个类名Fake
不方便改变。第二个就是需要创建这个类的属性和方法越多,就要给这个generate_cls
函数添加对应的逻辑代码,不够灵活。
In [1]: def func(instance):
...: print(instance.a, instance.b)
...: print(instance.method_a(10))
...:
In [2]: def generate_cls(a, b):
...: classFake:
...: def method_a(self, n):
...: return n
...: Fake.a = a
...: Fake.b = b
...: return Fake
...:
In [3]: ins = generate_cls(1, 2)()
In [4]: ins.a, ins.b, ins.method_a(10)
Out: (1, 2, 10)
这时,就需要使用元类来完成对应的内容了。我们把method_a
方法抽出来,使用type
并且传入相应的值,然后使用就可以了。注意,这里是通过键值对传递的。
In [1]: def method_a(self, n):
...: return n
...:
In [2]: ins = type('Fake', (), {'a': 1, 'b': 2, 'method_a': method_a})()
In [3]: ins.a, ins.b, ins.method_a(10)
Out[3]: (1, 2, 10)
- 元类的自定义
除此之外,元类还支持自定义的。因为默认的type
它的定制十分有限的,只能修改类型的名称、继承父类的列表、还有类的一些属性和方法。所以,我们这里可以继承Type
来提高它的定制性。而定制性,主要依靠如下三个魔法方法来完成的。
魔法方法 | 功能描述 |
---|---|
__new__ |
生成类的类型对象 |
__init__ |
生成类的类型对象后被调用进行初始化,第一个参数是已经生成的类的类型对象 |
__call__ |
在生成类的实例对象时被调用的 |
其中,元类的__new__
和__init__
都是在创建类的时候调用一次,而创建类的实例的时候每次都会调用__call__
这个方法。
我们可以看到,下面这个简单示例中,类名HelloMeta
中的Meta
为创建元类的通常用法,便于其他人理解和使用。这个类__new__
魔法方法接收四个参数,分别是当前准备创建的类、类的名称、继承父类的一个元组、接收类属性和方法的字典。之后定义了两个实例方法__init__
和hello
,这两个方法最后都会变成实例对象给传递进去,如t.__init__ = __init__
。
之后,在Python3
使用的时候,就在参数metaclass
中指定我们之前创建的哪个元类名称,然后去使用就可以了。
class HelloMeta(type):
def __new__(cls, name, bases, attrs):
def __init__(cls, func):
cls.func = func
def hello(cls):
print('hello world')
t = type.__new__(cls, name, bases, attrs)
t.__init__ = __init__
t.hello = hello
return t
In [1]: classNewHello(metaclass=HelloMeta):
...: ...
...:
In [2]: inst = NewHello(lambda x: x * 3)
In [3]: inst.hello()
hello world
下来,我们看一个更为深入的示例代码。在__new__
里面多了一个kwargs
的可变参数,还有一个__prepare__
的类方法。其实,__prepare__
是Python3
中新添加的一个魔法方法,它会优先于__new__
方法执行,用于创建类型对象的一个名称空间的,也就是最后会变成这个NewHello.__dict__
里面的内容。
而且因为这个魔法的方法的原因,导致在多传参的情况下,可以将参数直接传递给kwargs
变量。通过输出信息,我们知道__prepare__
中的args
对应('NewHello', ())
,而kwargs
就是这个{'a': 1, 'b': 2}
的字典。
最后__prepare__
这个类方法,将kwargs
这个变量返回了,它们已经变成了NewHello.__dict__
里面的内容,所以初始化之后可以直接拿到对应的值。
class HelloMeta(type):
def __new__(cls, name, bases, attrs, **kwargs):
print('__new__', kwargs)
return type.__new__(cls, name, bases, attrs)
@classmethod
def __prepare__(cls, *args, **kwargs):
print('__prepare__', args, kwargs)
return kwargs
In [1]: class NewHello(metaclass=HelloMeta, a=1, b=2):
...: ...
...:
__prepare__ ('NewHello', ()) {'a': 1, 'b': 2}
__new__ {'a': 1, 'b': 2}
In [2]: inst = NewHello()
In [3]: inst.a, inst.b
Out[3]: (1, 2)
我们需要注意的是,在Python2
中使用元类的方法有所不同,需要使用__metaclass__
对其赋值,才能够正常使用。
classNewHello(object):
__metaclass__ = HelloMeta
- 编写兼容的元类
写Python2
和Python3
兼容的元类代码,就需要使用six
这个第三方模块来提供支持。它提供了一个with_metaclass
的函数,这样就实现了2/3
的兼容。
from six import with_metaclass
classMeta(type):
pass
classBase(object):
pass
classMyClass(with_metaclass(Meta, Base)):
pass
- 何时需要使用元类
上面我们已经说明了元类的基础知识和用法,下面我们说说什么时候需要使用到元类。日常的业务逻辑开发,基本是不需要使用到元类的,因为元类是用于拦截、修改类的创建用的。
元类能用到的场景很少,我能够想到的地方就是ORM
了,即对象关系映射意思。简单地说,就是把关系数据库中的一张表映射成为一个类,每一个字段映射成为一个属性,每一行记录映射成为一个对象。
那我们设想一下,ORM
里面的module
只能用一个动态定义的方式。因为在这个模式下,这种关系只能由使用者来定义,所以元类配合一个我们之前学习到的描述符,就可以用来实现ORM
了。