利用元类来控制类的初始化
2014 11 26 03:30 AM 1630次查看
首先重申一下,元类(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 ---可以看到,在定义 TestClass 类的时候,会先执行类定义里的语句,然后依次调用其元类的 __new__ 和 __init__ 方法。这种初始化是一次性的,在创建 TestClass 类的对象时,并不会再次调用。
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 ---
此外,上述的 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"可以看到,获取类名的方法确实不会被重复调用了,但是子类却可能因为父类已经有了同名属性,而导致 descriptor 失效。而如果让子类都去加上这个属性,也不太优雅。
test_class
test_class
test_class
get name of class "TestClass2"
test_class2
test_class2
get name of class "TestSubClass2"
test_sub_class2
于是终于可以轮到元类出场了,它的 __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条评论 你不来一发么↓