什么是Metaclass

关于Python的Metaclass,个人所见最好的介绍见于Stack Overflow上What is a metaclass in python?这一问题,下面有不少非常精彩的回答。为了便于查找,这里也作下相应的记录。

Metaclass语法

class Metaclass(type):

    def __new__(cls, name, parents, attrs):
        return super(Metaclass, cls).__new__(cls, name, parents, attrs)

    def __init__(cls, name, parents, attrs):
        super(Metaclass, cls).__init__(name, parents, attrs)

    def __call__(cls, *args, **kwargs):
        return super(Metaclass, cls).__call__(*args, **kwargs)


class Foobar(object):

    __metaclass__ = Metaclass

    def __init__(self):
        super(Foobar, self).__init__()

自定义Metaclass需要继承自type,type是Python的预定义函数,用于创建Class。在需要应用Metaclass的类中,以 __metaclass__指定所需的Metaclass。

__new__, __init__, __call__

Class如果存在自定义的Metaclass,在其被创建时会依次调用Metaclass上的__new____init__方法。在这两个函数中可以去动态修改类的定义,比如增删成员变量、成员函数等等。这两个函数有什么区别呢?前者是在Class构造时调用的,后者是在创建完成之后用于初始化的。在自定义Metaclass的时候,个人觉得使用任何一个都可以,在其它场景下,__new__主要用于实现继承immutable types(str,int,etc)。

class UpperStr(str):

    def __new__(cls, value):
        return super(UpperStr, cls).__new__(cls, value.upper())


print UpperStr('a')

另一方面,__call__则是每次在Class实例创建时调用,调用顺序如下,

  1. Metaclass的__call__
  2. Class的__new__
  3. Class的__init__

Metaclass查找顺序

Class在查找应用Metaclass时,遵循如下顺序,

  • 在当前Class中定义的Metaclass
  • 父类中定义的Metaclass
  • 当前module中定义的Metaclass
  • 对于old-style Class来说,会应用classic metaclass (types.ClassType)

Metaclass用例

什么场景比较适合使用Metaclass呢?需要对不同Class进行相同的操作时。其实就是Class层面上的代码复用以既定语法来进行表现。

Singleton

在实际开发中单例是一种强需求。在Python中单例有几种不同实现方式。Stack Overflow上的问题Creating a singleton in python中介绍了很多种可能的写法,个人觉得还是Metaclass的方式看着最简洁。

# singleton.py
class Singleton(type):

    def __call__(cls, *args, **kwargs):
        if not hasattr(cls, '_instance'):
            cls._instance = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instance

# foobar.py
class Foobar(object):

    __metaclass__ = Singleton

    def __init__(self):
        super(Foobar, self).__init__()

foobar = Foobar()

函数替换

在过往的开发中,还遇到了进行函数替换的场景。

__init__调用之后重载__setattr__

class SetAttrMeta(type):
	def __call__(cls, *args, **kwargs):
		real_setattr = cls.__setattr__
		cls.__setattr__ = object.__setattr__
		self = super(SetAttrMeta, cls).__call__(*args, **kwargs)
		cls.__setattr__ = real_setattr
		return self

如果不进行特定处理那么在__init__函数调用之前__setattr__就已经被替换的话,很容易导致递归操作,或者未定义字段访问。这里借用了Metaclass中__call__的调用顺序来手动替换类的__setattr__方法。在Stack Overflow上回答过Overriding setattr at runtime这个相关问题。

替换Class的__getattribute__

在类定义中定义的__getattribute__只会想要实例的属性获取,但对于Class上定义的字段是无能为力的。这时候就需要在Metaclass上做文章了。曾经实现过一个Empty类,具体的功能就是无限支持对象上的级联调用,

class EmptyMeta(type):

	def __getattribute__(self, name):
		try:
			return object.__getattribute__(self, name)
		except:
			return empty


class Empty(object):
	"""
	e.g.
	foo.bar
	foo.bar.hello.world()
	"""

	__metaclass__ = EmptyMeta

	def __init__(self, *args, **kwargs):
		super(Empty, self).__init__()

	def __call__(self, *args, **kwargs):
		return empty

	def __getattribute__(self, item):
		return empty

	def __nonzero__(self):
		return False

	def __iter__(self):
		return self

	def next(self):
		raise StopIteration


empty = Empty()

特定语法

一个比较广泛应用的例子就是Django中的model定义。Python Essential Reference里给出了一个TypedProperty例子,其用法接近,这里直接引用下,

class TypedProperty(object):

    def __init__(self, type, default=None):
        self.name = None
        self.type = type
        self.default = default if default else type()

    def __get__(self, instance, cls=None):
        return getattr(instance, self.name, self.default)

    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError
        setattr(instance, self.name, value)


class TypedMeta(type):
    def __new__(cls, name, parents, attrs):
        slots = []
        for key, value in attrs.iteritems():
            if isinstance(value, TypedProperty):
                value.name = '_' + key
                slots.append(value.name)
        attrs['__slots__'] = slots
        return type.__new__(cls, name, parents, attrs)


class Foobar(object):

    __metaclass__ = TypedMeta

    name = TypedProperty(str)
    num = TypedProperty(int, 42)

总结

Metaclass是Python提供的一种动态编程的特性,然而其用法并不那么简明直观。在官方文档上也没有很好的文档进行说明,在多次反复遗忘之后,在这里进行记录与总结。

一切都是约定。针对对象的特殊操作,在对应的Class里修改,针对Class的特殊操作,在Metaclass里修改。

参考