元类metaclass

元类metaclass

  什么是元类?简单来说元类就是用来创建类的类。在Python中,一切皆对象,实例是对象,类也是对象。类可以创建实例,实例是对象。类也是对象,用什么能创建类呢?答案就是元类。下面的示意图显示了实例、类和元类之间的关系:




元类的创建


  type是最简单也是最基础的元类。要创建一个元类,需要从type继承。下面是一个最简单的元类:

  class Meta(type):
    pass


元类的使用方式


  创建元类之后,怎么使用元类呢?Python2和Python3使用元类的方式不同,Python2通过类属性__metaclass__指定元类;而Python3通过关键字metaclass指定元类。下面是使用元类Meta的例子:

# Python2
class A(object):
  __metaclass__ = Meta
  pass

# Python3
class A(metaclass=Meta):
  pass


元类的例子


  了解元类的创建和使用方式之后,我们来看看通常什么地方能用到元类,或者说元类能用来做什么。
  使用元类可以用来根据需要修改类的属性,下面是一个将类属性改为全大写的元类的例子:

class UpperMeta(type):
    def __new__(mcls, name, bases, attrs):
        attrs_new = dict()
        for key, value in attrs.items():
            if not key.startswith('__'):
                attrs_new[key.upper()] = value
            else:
                attrs_new[key] = value

        return type.__new__(mcls, name, bases, attrs_new)


class A(metaclass=UpperMeta):
    nAMe = 'Lucy'

    def sAy(self):
        print('Hello!')

print('dir(A): %r' % dir(A))
a = A()
print('dir(a): %r' % dir(a))
print('a.NAME: %s' % a.NAME)
a.SAY()

  程序输出:
dir(A): ['NAME', 'SAY', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
dir(a): ['NAME', 'SAY', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
a.NAME: Lucy
Hello!

  显然,在类A中定义的类属性nAMe和方法sAy(),因为使用了元类对属性进行了处理,变成了NAME和SAY()。实际上的处理流程是这样的:先扫描类UpperMeta的定义,发现其父类为type,得知其是元类。然后扫描类A的定义,其定义了nAMe和sAy()。如果类A未指定元类,则使用默认的type来创建类;而当前其指定了元类UpperMeta,则使用元类UpperMeta来创建类。元类UpperMeta的__new__()会将非以双下划线__开头的属性转成全大写,所以类A的属性nAMe和sAy()就变成了NAME和SAY()。
  上面的例子使用了__new__()修改类的属性,当需要检查类的属性时,可以使用方法__init__()。下面的例子通过__init__()强制类必须拥有类的文档字符串,如果没有,则抛出异常。

class NotDocException(BaseException):
    pass

class DocMeta(type):
    def __init__(cls, name, bases, attrs):
        if not '__doc__' in attrs.keys()\
                or not getattr(attrs, '__doc__'):
            raise NotDocException('Must have doc!')

class A(metaclass=DocMeta):
    """
    This class A.
    """
    pass

class B(metaclass=DocMeta):
    pass

  程序输出:
Traceback (most recent call last):
File "DocMeta.py", line 16, in
    class B(metaclass=DocMeta):
File "DocMeta.py", line 8, in __init__
    raise NotDocException('Must have doc!')
__main__.NotDocException: Must have doc!

  从程序的输出不难看出,类A因为有类的文档字符串,定义成功;而类B因为没有类的文档字符串,抛出了异常。
  元类除了使用__new__()和__init__()外,还可以使用__call__()来拦截(使用元类的)类创建实例的过程。下面的例子通过元类的__call__()实现了单例。

class SingletonMeta(type):
    instance = None
    def __call__(cls, *args, **kw):
        if not cls.instance:
            cls.instance = super().__call__(*args, **kw)
        return cls.instance

class A(metaclass=SingletonMeta):
    pass

class B(metaclass=SingletonMeta):
    pass

a1 = A()
a2 = A()
b1 = B()
b2 = B()

assert a1 is a2
assert b1 is b2

assert a is b

  程序输出:
Traceback (most recent call last):
File "SgingletonMeta.py", line 22, in
    assert a is b
NameError: name 'a' is not defined