Python元类


纸上得来终觉浅,绝知此事要躬行。

Python元类


[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。而元类就是类的类,怎么理解呢,请看下面这个图。

python-meta-class

从图中,我们可以看出实例是类的实例,而类是元类的实例。而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
  • 编写兼容的元类

Python2Python3兼容的元类代码,就需要使用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了。


文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !