利用元类来控制类的初始化

标签:Python

几年前我曾写过一篇《Python 的 metaclass》。最近因为元类用得比较多,发现之前写得不太实用,所以再补一篇。

首先重申一下,元类(metaclass)是类的类,所以它的实例是类。
而类的实例化过程中,会调用它的 __new__ 方法来生成对象,然后再用 __init__ 方法来初始化这个对象。
同理,元类在实例化的过程中,也会调用它的 __new__ 方法来生成类,再用 __init__ 方法来初始化这个类。

下面举个例子:
class MetaClass(type):
    def __new__(cls, name, bases, dct):
        print 'new class "%s"' % name
        print 'attributes: "%s"' % dct
        return super(MetaClass, cls).__new__(cls, name, bases, dct)

    def __init__(cls, name, bases, dct):
        print 'init class "%s"' % name
        print 'attributes: "%s"' % dct
        super(MetaClass, cls).__init__(name, bases, dct)


class SubMetaClass(MetaClass):
    def __new__(cls, name, bases, dct):
        print 'new class "%s"' % name
        print 'attributes: "%s"' % dct
        return super(MetaClass, cls).__new__(cls, name, bases, dct)

    def __init__(cls, name, bases, dct):
        print 'init class "%s"' % name
        print 'attributes: "%s"' % dct
        super(MetaClass, cls).__init__(name, bases, dct)        


print '--- define test class ---'


class TestClass(object):
    print 'enter test class body'

    __metaclass__ = MetaClass

    attr = 1

    def method(self):
        pass

    print 'exit test class body'


print '--- create test object ---'

TestClass()


print '--- define test sub class ---'


class TestSubClass(TestClass):
    print 'enter test sub class body'

    __metaclass__ = SubMetaClass

    attr2 = 2

    def method2(self):
        pass

    print 'exit test sub class body'


print '--- create test sub object ---'

TestSubClass()
结果:
--- define test class ---
enter test class body
exit test class body
new class "TestClass"
attributes: "{'__module__': '__main__', '__metaclass__': <class '__main__.MetaClass'>, 'attr': 1, 'method': <function method at 0x10efeacf8>}"
init class "TestClass"
attributes: "{'__module__': '__main__', '__metaclass__': <class '__main__.MetaClass'>, 'attr': 1, 'method': <function method at 0x10efeacf8>}"
--- create test object ---
--- define test sub class ---
enter test sub class body
exit test sub class body
new class "TestSubClass"
attributes: "{'attr2': 2, '__module__': '__main__', '__metaclass__': <class '__main__.SubMetaClass'>, 'method2': <function method2 at 0x10efead70>}"
init class "TestSubClass"
attributes: "{'attr2': 2, '__module__': '__main__', '__metaclass__': <class '__main__.SubMetaClass'>, 'method2': <function method2 at 0x10efead70>}"
--- create test sub object ---
可以看到,在定义 TestClass 类的时候,会先执行类定义里的语句,然后依次调用其元类的 __new__ 和 __init__ 方法。这种初始化是一次性的,在创建 TestClass 类的对象时,并不会再次调用。
此外,上述的 dct 是这个类本身的类属性,不包含其父类的。

顺便说下 __new__ 和 __init__ 方法的区别:
前者是用来创建对象的,所以它需要返回一个对象;而后者是用来初始这个对象的,例如给一些属性赋值等。
也就是说,__new__ 方法做的是生孩子的事,而 __init__ 方法干的是给孩子起名等事。
不过生孩子这么困难的事,一般不需要我这种男性程序员操心,所以大多数时候只要和 __init__ 方法打交道即可。
而 __new__ 方法由于可以控制对象的创建过程,所以常用功能有:如果不返回对象,便能实现一个抽象类;如果每次都返回同一个实例,就能实现单例模式了。
当然,你也可以在 __new__ 方法里做本该是 __init__ 方法去做的事,只是这样不太符合其用意而已。

再回到元类,看看它的 __init__ 方法能做些什么。
不用猜就知道,当然是给类属性赋值了。
于是再举个例子,类的命名一般是驼峰式(即 ClassName),如果想要获得以下划线分隔的名字(即 class_name),可以这样做:
import re


class NameMixin(object):
    _CAMEL_CASE_PATTERN = re.compile(r'([a-z0-9])([A-Z])')

    @classmethod
    def class_name(cls):
        return cls._CAMEL_CASE_PATTERN.sub(r'\1_\2', cls.__name__).lower()


class TestClass(NameMixin):
    pass


class TestSubClass(TestClass):
    pass


print TestClass.class_name()
print TestSubClass.class_name()
结果:
test_class
test_sub_class

或者再高端点,把它弄得像个属性:
class ClassName(object):
    _CAMEL_CASE_PATTERN = re.compile(r'([a-z0-9])([A-Z])')

    def __get__(self, obj, objtype=None):
        if objtype:
            return self._CAMEL_CASE_PATTERN.sub(r'\1_\2', objtype.__name__).lower()


class TestClass(object):
    class_name = ClassName()


class TestSubClass(TestClass):
    pass


print TestClass.class_name
print TestSubClass.class_name
结果是一样的,就不列出了。

这里有个不高效的地方,即每次获取时都会重新计算一遍,而这本该是个常量。
所以尝试这样修改:
import re


class ClassName(object):
    _CAMEL_CASE_PATTERN = re.compile(r'([a-z0-9])([A-Z])')

    def __get__(self, obj, objtype=None):
        print 'get name of class "%s"' % objtype.__name__
        if objtype:
            objtype.class_name = class_name = self._CAMEL_CASE_PATTERN.sub(r'\1_\2', objtype.__name__).lower()
            return class_name


class TestClass(object):
    class_name = ClassName()


class TestSubClass(TestClass):
    pass


class TestClass2(object):
    class_name = ClassName()


class TestSubClass2(TestClass):
    class_name = ClassName()


print TestClass.class_name
print TestClass.class_name
print TestSubClass.class_name
print TestClass2.class_name
print TestClass2.class_name
print TestSubClass2.class_name
结果:
get name of class "TestClass"
test_class
test_class
test_class
get name of class "TestClass2"
test_class2
test_class2
get name of class "TestSubClass2"
test_sub_class2
可以看到,获取类名的方法确实不会被重复调用了,但是子类却可能因为父类已经有了同名属性,而导致 descriptor 失效。而如果让子类都去加上这个属性,也不太优雅。

于是终于可以轮到元类出场了,它的 __init__ 方法正好只在类初始化时执行一次,也能给类属性赋值,所以轻松解决这个问题:
import re


class ClassNameMeta(type):
    _CAMEL_CASE_PATTERN = re.compile(r'([a-z0-9])([A-Z])')
    
    def __init__(cls, name, bases, dct):
        super(ClassNameMeta, cls).__init__(name, bases, dct)
        cls.class_name = ClassNameMeta._CAMEL_CASE_PATTERN.sub(r'\1_\2', cls.__name__).lower()


class TestClass(object):
    __metaclass__ = ClassNameMeta


class TestSubClass(TestClass):
    pass


print TestClass.class_name
print TestSubClass.class_name
结果完全正确,就不再贴一次了。

如果还有更变态的需求,例如 HTMLParser 这种不知道应该怎么分词的类名,需要手动指定咋办?
只要判断一下是否已经定义过即可:
import re


class ClassNameMeta(type):
    _CAMEL_CASE_PATTERN = re.compile(r'([a-z0-9])([A-Z])')
    
    def __init__(cls, name, bases, dct):
        super(ClassNameMeta, cls).__init__(name, bases, dct)
        if 'class_name' not in dct:  # 或者用 cls.__dict__ 来判断
            cls.class_name = ClassNameMeta._CAMEL_CASE_PATTERN.sub(r'\1_\2', cls.__name__).lower()


class TestClass(object):
    __metaclass__ = ClassNameMeta


class TestSUBClass(TestClass):
    class_name = 'test_sub_class'


class TestGrandchildClass(TestSUBClass):
    pass


print TestClass.class_name
print TestSUBClass.class_name
print TestGrandchildClass.class_name
结果:
test_class
test_sub_class
test_grandchild_class

再来尝试实现一个方法,它接收一个字符串,根据这个字符串来调用不同的方法:
class ActionClass(object):
    @classmethod
    def handle(cls, action_name, *args, **kwargs):
        handler = getattr(cls, 'do_' + action_name)
        if handler and callable(handler):
            return handler(*args, **kwargs)


class Calculator(ActionClass):
    @classmethod
    def do_add(cls, x, y):
        return x + y

    @classmethod
    def do_sub(cls, x, y):
        return x - y

print Calculator.handle('add', 1, 2)
print Calculator.handle('sub', 2, 1)

因为 getattr 是个比较慢的方法,所以也可以用元类来提前计算:
class ActionMeta(type):
    def __init__(cls, name, bases, dct):
        super(ActionMeta, cls).__init__(name, bases, dct)

        actions = {}
        for attr_name, attr in dct.iteritems():
            if attr_name[:3] == 'do_':
                action_name = attr_name[3:]
                if action_name:
                    method = getattr(cls, attr_name)
                    if callable(method):
                        actions[action_name] = method
        cls._ACTIONS = actions


class ActionClass(object):
    __metaclass__ = ActionMeta

    @classmethod
    def handle(cls, action_name, *args, **kwargs):
        handler = cls._ACTIONS.get(action_name)
        if handler:
            return handler(*args, **kwargs)
这里需要简单说下 method = getattr(cls, attr_name),为什么不直接用 attr 呢?
因为 attr 不是一个 callable 的 method,而是一个 classmethod 对象。它其实是个 descriptor,需要用 attr.__get__(None, cls) 才能拿到原始的 instancemethod。

大致就这样吧,夜深要睡觉了=。=

0条评论 你不来一发么↓

    想说点什么呢?