python metaclass
概述
metaclass在很多语言里都有,能够动态改变类的行为。
例子:简单模拟了一下Django ORM Model
Model 可以以下面的形式定义Field:
from django.db import models
class Resource(models.Model):
name = models.CharField(max_length=128, unique=True)
host = models.GenericIPAddressField()
description = models.TextField(blank=True)
在Resource Model对象使用:
resource_obj = Resource(name='guset', host='127.0.0.1')
print Resource.name
# Output: <CharField object>
print resource_obj.name
# Output: guest
模拟代码:
import six
class BaseField(object):
__default = None
def __init__(self, *args, **kwargs):
self.__value = kwargs.get('default', self.__default)
def validate(self, value):
raise NotImplemented
class IntegerField(BaseField):
def validate(self, value):
# raise directly if exception occur
self.__value = int(value)
# simply return clean value after validate
return self.__value
class CharField(BaseField):
def validate(self, value):
self.__value = str(value)
return self.__value
class BaseModel(type):
def __new__(mcls, name, bases, attrs):
print [mcls, name, bases, attrs]
fields = {}
for attrname, attr in attrs.items():
if isinstance(attr, BaseField):
fields[attrname] = attr
def __init__(self, *args, **kwargs):
for field, field_obj in fields.items():
cleaned_data = field_obj.validate(kwargs.get(field, None))
setattr(self, field, cleaned_data)
attrs['__init__'] = __init__
return type(name, (object,), attrs)
class MyModel(six.with_metaclass(BaseModel)):
age = IntegerField(default=0)
name = CharField(default='')
m = MyModel(age='a100', name='aaa')
print MyModel.age, MyModel.name
print m.age, m.nam
"""
Output:
[<class '__main__.BaseModel'>, 'MyModel', (), {'__module__': '__main__', 'age': <__main__.Field object at 0x7f2083b4f150>, 'name': <__main__.Field object at 0x7f2083bb5e90>}]
<__main__.Field object at 0x7f2083b4f150> <__main__.Field object at 0x7f2083bb5e90>
100 aaa
"""
Tips
def __init__(self, *args, **kwargs):
for field, field_obj in fields.items():
cleaned_data = field_obj.validate(kwargs.get(field, None))
setattr(self, field, cleaned_data)
attrs['__init__'] = __init__
这段代码重载了类的构造函数__init__
,在构造的时候会调用Field.validate
方法,并且把对象的属性赋值为validated之后的cleaned_data。
所以在调用print MyModel.age
打印的是Field Object,而MyModel()
对象输出的是100。
例子:metaclass hook 实现 mixin
# -*- coding: utf-8 -*-
import six
from collections import OrderedDict
class MetaClass(type):
@classmethod
def _get_hook_funcs(cls, bases, attrs):
fields = [(field_name, attrs.pop(field_name))
for field_name, obj in list(attrs.items())
if field_name.startswith("_do_")]
for base in reversed(bases): #遵循多重继承顺序
if hasattr(base, '_hook_funcs'):
fields += list(base._hook_funcs.items())
# 返回OrderedDict,因此是重名覆盖
return OrderedDict(fields)
def __new__(cls, name, bases, attrs):
attrs['_hook_funcs'] = cls._get_hook_funcs(bases, attrs)
return super(MetaClass, cls).__new__(cls, name, bases, attrs)
class MetaClassAll(type):
@classmethod
def _get_hook_funcs(cls, bases, attrs):
fields = []
for name, _ in attrs.items():
if name.startswith("_do_"):
fields.append(attrs.pop(name))
for base in reversed(bases):
if hasattr(base, '_hook_funcs'):
fields += base._hook_funcs
# 返回list,重名不覆盖
return fields
def __new__(cls, name, bases, attrs):
attrs['_hook_funcs'] = cls._get_hook_funcs(bases, attrs)
return super(MetaClassAll, cls).__new__(cls, name, bases, attrs)
class Mixin1(object):
__metaclass__ = MetaClass
def _do_mixin1(self):
print "foo from Minxin1"
class Mixin2(object):
__metaclass__ = MetaClass
def _do_mixin2(self):
print "foo from Minxin2"
class KlassBase(object):
__metaclass__ = MetaClass
def foo(self):
if getattr(self, "_hook_funcs", None):
for name, func in self._hook_funcs.items():
func(self)
def _do_print1(self):
self.print1()
def print1(self):
print "print 1 from KlassBase"
class MyKlass(KlassBase, Mixin1, Mixin2):
def print1(self):
print "print 1 form KlassA"
MyKlass().foo()
"""
Output:
foo from Minxin2
foo from Minxin1
print 1 form KlassA
"""
Tips:
metaclass通过收集当前类和基类的所有_do_开头的属性,放到当前类的_hook_funcs属性中,在基类调用钩子里的函数。
MetaClass的_hook_funcs是OrderedDict,因此同名hook函数覆盖.MetaClassAll使用list存储所以hook functions,因此不覆盖。