Python中metaclass的理解及使用

Python 中的**元类(metaclass)**是一个深度魔法,平时我们可能比较少接触到元类,本文将通过一些简单的例子来理解这个魔法。

1 类也是对象

在 Python 中,一切皆对象。字符串,列表,字典,函数是对象,类也是一个对象,因此你可以:

  • 把类赋值给一个变量
  • 把类作为函数参数进行传递
  • 把类作为函数的返回值
  • 在运行时动态地创建类

看一个简单的例子

class Foo(object):
    foo = True

class Bar(object):
    bar = True
    
class Anchor(object):
    anchor = True

# 将类作为函数参数参数进行传递
def echo(cls):
    print(cls)

# 将类作为函数的返回值
def select(name):
    if name == 'foo':
        return Foo
    if name == 'bar':
        return Bar
    # 将类赋值给一个变量
    other_obj = Anchor
    return other_obj
    
>>> echo(Foo)             
<class '__main__.Foo'>
>>> cls = select('foo')
>>> cls
__main__.Foo
>>> cls = select("sds")
>>> cls
__main__.Anchor

2 type的使用

在日常使用中,我们经常使用 object 来派生一个类,事实上,在这种情况下,Python 解释器会调用 type 来创建类。这里,出现了 type,没错,是你知道的 type,我们经常使用它来判断一个对象的类型,比如:

class Foo(object):
    Foo = True

>>> type(10)
<type 'int'>
>>> type('hello')
<type 'str'>
>>> type(Foo())
<class '__main__.Foo'>
>>> type(Foo)
<type 'type'>

事实上,type 除了可以返回对象的类型,它还可以被用来动态地创建类(对象)。下面,我们看几个例子,来消化一下这句话。

使用 type 来创建类(对象)的方式如下:

type(类名, 父类的元组(针对继承的情况,可以为空),包含属性和方法的字典(名称和值))

2.1 最基本的class

假设有下面的类:

class Foo(object):
    pass

现在,我们不使用 class 关键字来定义,而使用 type,如下:

Foo = type("Foo", (object,), {})    # 使用 type 创建了一个类对象

上面两种方式是等价的。我们看到,type 接收三个参数:

  • 第 1 个参数是字符串 ‘Foo’,表示类名
  • 第 2 个参数是元组 (object, ),表示所有的父类
  • 第 3 个参数是字典,这里是一个空字典,表示没有定义属性和方法

在上面,我们使用 type() 创建了一个名为 Foo 的类,然后把它赋给了变量 Foo,我们当然可以把它赋给其他变量,但是,此刻没必要给自己找麻烦。

接着,我们看看使用:

Foo = type("Foo", (object,), {})    # 使用 type 创建了一个类对象

print(Foo)
>>> <class '__main__.Foo'>
print(Foo())
>>> <__main__.Foo object at 0x0000015008D2A048>

2.2 有属性和方法的class

假设有下面的类:

class Foo(object):
    foo = True
    def greet(self):
        print('hello world')
        print self.foo

type 来创建这个类,如下:

def greet(obj):
    print('hello world')
    print(obj.foo) 

Foo = type("Foo", (object,), {"foo": True, "greet": greet})

上面两种方式的效果是一样的,看下使用:

>>> f = Foo()
>>> f.foo
True
>>> f.greet
<bound method Foo.greet of <__main__.Foo object at 0x10c34f890>>
>>> f.greet()
hello world
True

2.3 包含继承关系的class

再来看看继承的情况,假设有如下的父类:

class Base(object):
    pass

我们用 Base 派生一个 Foo 类,如下:

class Foo(Base):
   foo = True

改用 type 来创建,如下:

Foo = type("Foo", (Base,), {"foo": True})

3 什么是metaclass

metaclass的通常定义为:元类(metaclass)是用来创建类(对象)的可调用对象。

怎么理解这句话尼?首先可以明确元类是可调用对象,其次它的功能是用来创建类的。对比一下类的解释,上文提到类也是可调用对象,类的功能是用来创建实例对象的;所以元类可以理解为一种更高级的类,由它创建的对象都是类对象。对于实例对象、类和元类,我们可以用下面的图来描述:

类是实例对象的模板,元类是类的模板

+----------+             +----------+             +----------+
|          |             |          |             |          |
|          | instance of |          | instance of |          |
| instance +------------>+  class   +------------>+ metaclass|
|          |             |          |             |          |
|          |             |          |             |          |
+----------+             +----------+             +----------+

前面举了许多例子来说明type的功能,可以发现type具有创造类对象的功能。正是因为**type 就是一个元类**。

那么,元类到底有什么用呢?

**元类的主要目的是为了控制类的创建行为。**我们还是先来看看一些例子,以消化这句话。

4 元类的使用案例

4.1 元类实现修改属性名

先从一个简单的例子开始,假设有下面的类:

class Foo(object):
    name = 'foo'
    def bar(self):
        print('bar')

现在我们想给这个类的方法和属性名称前面加上 my_ 前缀,即 name 变成 my_name,bar 变成 my_bar,另外,我们还想加一个 echo 方法。当然,有很多种做法,这里展示用元类的做法。

首先,定义一个元类,按照默认习惯,类名以 Metaclass 结尾,代码如下:

class PrefixMetaclass(type):
    def __new__(cls, name, bases, attrs):
        # 给所有属性和方法前面加上前缀 my_
        _attrs = (('my_' + name, value) for name, value in attrs.items())  

        _attrs = dict((name, value) for name, value in _attrs)  # 转化为字典
        _attrs['echo'] = lambda self, phrase: phrase  # 增加了一个 echo 方法

        return type.__new__(cls, name, bases, _attrs)  # 返回创建后的类

上面的代码有几个需要注意的点:

  • PrefixMetaClass 从 type 继承,这是因为 PrefixMetaclass 是用来创建类的
  • __new__是在__init__之前被调用的特殊方法,它用来创建对象并返回创建后的对象,对它的参数解释如下:
  • cls:当前准备创建的类
  • name:类的名字
  • bases:类的父类集合
  • attrs:类的属性和方法,是一个字典

接着,我们需要指示 Foo 使用 PrefixMetaclass 来定制类。

class Foo(metaclass=PrefixMetaclass):
    name = 'foo'
    def bar(self):
        print('bar')

现在,让我们看看使用:

>>> f = Foo()
>>> f.name    # name 属性已经被改变
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-774-4511c8475833> in <module>()
----> 1 f.name

AttributeError: 'Foo' object has no attribute 'name'
>>>
>>> f.my_name
'foo'
>>> f.my_bar()
bar
>>> f.echo('hello')
'hello'

可以看到,Foo 原来的属性 name 已经变成了 my_name,而方法 bar 也变成了 my_bar,这就是元类的魔法。

4.2 继承关系中的元类

类似前面的案例,区别Foo新增了一个子类Bar,下面给出完整代码:

class PrefixMetaclass(type):
    def __new__(cls, name, bases, attrs):
        # 给所有属性和方法前面加上前缀 my_
        _attrs = (('my_' + name, value) for name, value in attrs.items())  

        _attrs = dict((name, value) for name, value in _attrs)  # 转化为字典
        _attrs['echo'] = lambda self, phrase: phrase  # 增加了一个 echo 方法

        return type.__new__(cls, name, bases, _attrs)

class Foo(metaclass = PrefixMetaclass):
    name = 'foo'
    def bar(self):
        print('bar')

class Bar(Foo):
    prop = 'bar'

其中,PrefixMetaclass 和 Foo 跟前面的定义是一样的,只是新增了 Bar,它继承自 Foo。先让我们看看使用:

>>> b = Bar()
>>> b.prop     # 发现没这个属性
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-778-825e0b6563ea> in <module>()
----> 1 b.prop

AttributeError: 'Bar' object has no attribute 'prop'
>>> b.my_prop
'bar'
>>> b.my_name
'foo'
>>> b.my_bar()
bar
>>> b.echo('hello')
'hello'

我们发现,Bar 没有 prop 这个属性,但是有 my_prop 这个属性,这是为什么呢?

原来,当我们定义 class Bar(Foo) 时,Python 会首先在当前类,即 Bar 中寻找 __metaclass__,如果没有找到,就会在父类 Foo 中寻找 __metaclass__,如果找不到,就继续在 Foo 的父类寻找,如此继续下去,如果在任何父类都找不到 __metaclass__,就会到模块层次中寻找,如果还是找不到,就会用 type 来创建这个类。

这里,我们在 Foo 找到了 __metaclass__,Python 会使用 PrefixMetaclass 来创建 Bar,也就是说,元类会隐式地继承到子类,虽然没有显示地在子类使用 __metaclass__,这也解释了为什么 Bar 的 prop 属性被动态修改成了 my_prop。

4.3 使用metaclass实现单例模式

完整代码如下:

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 Foo(metaclass = Singleton):
    pass

一开始很疑惑为什么我们是重写元类的__init__方法,而不是使用__new__方法来为元类增加一个属性。其实只是被上面那一段关于元类中__new__方法迷惑了,它主要用于我们需要对类的结构进行改变的时候我们才要重写这个方法。而这里实现单例模式,并没有对类的结构发生改变,只是新增了一个属性__instance,所以使用__init__方法即可,下面看看使用情况

class Foo(metaclass=Singleton):
    pass

a = Foo()
b = Foo()
print(a == b)
>>> True


class Foo():
    pass

a = Foo()
b = Foo()
print(a == b)
>>> False

很明显使用未使用meatclass创建的两个实例不相等