Metaclass
1、一切皆对象
一、 类也是对象
在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段,在Python中这一点仍然成立。但是,Python中的类还远不止如此。类同样也是一种对象。只要你使用关键字class,Python解释器在执行的时候就会创建一个对象。下面的代码段:
class MyClass(object):
pass
将在内存中创建一个对象,名字就是MyClass。这个对象(类)自身拥有创建对象(类实例)的能力,而这就是为什么它是一个类的原因。但是,它的本质仍然是一个对象,于是你可以对它做如下的操作:
你可以将它赋值给一个变量, 你可以拷贝它, 你可以为它增加属性, 你可以将它作为函数参数进行传递。
在 python 中有两种对象:
- 类型(类,新的版本中类和类型是一样的)对象:可以被实例化和继承
- 非类型(实例)对象:不可以被实例和继承
python 中一切皆为对象:
在python里,
int
整形是对象,整数2
也是对象,你定义的函数啊,类啊都是对象,你定义的变量也是对象。总之,你在python里能用到的都可以称之为对象。
二、type和object
明白了python中一切皆对象之后,再了解一下python中对象之间的两种关系
面向对象体系中的两种关系:
- 父子关系:通常描述为“子类是一种父类”
- 类型实例关系:这种关系存在于两个对象中,其中一个对象(实例)是另一个对象(类型)的具体实现。
python中万物皆对象,一个python对象可能拥有两个属性,__class__
和 __bases__
,__class__
表示这个对象是谁创建的,__bases__
表示一个类的父类是谁们。__class__和type()函数效果一样。
class MyClass:
pass
MyClass.__class__
#Out: type
MyClass.__bases__
#Out: (object,)
int.__class__
#Out: type
int.__bases__
#Out: (object,)
object.__class__ #object是type的实例,object创建自type
#Out: type
object.__bases__ #object没有超类,它本身是所以对象的超类
#Out: ()
type.__class__ #type创建自本身
#Out: type
type.__bases__ #type继承自object,即object是type的超类
#Out: (object,)
- type为对象的顶点,所有对象都创建自type。
- object为类继承的顶点,所有类都继承自object。
三、元类、类、实例
- object是所有类的超类,而且type也是继承自object;所有对象创建自type,而且object也是type的实例。
- “type是object的类型,同时,object又是type的超类”,那到底是先有object还是先有type呢?这就像“先有鸡还是先有蛋问题”。
- object和type是python中的两个源对象,事实上,它们是互相依赖对方来定义,所以它们不能分开而论。
- 通过这两个源对象可以繁育出一堆对象:list、tuple、dict等。元类就是这些类的类,这些类是元类的实例。
l1=list()
l1.__class__ #l是list的实例
#Out: list
list.__class__ #list是type的实例
#Out: type
l1.__bases__ #实例没有超类
#AttributeError: 'list' object has no attribute '__bases__'
list.__bases__
#Out: (object,)
l2=[1,2,3] #l1是利用"类型名()"的方式创建实例,l2是利用python内置类型创造实例,比l1的创建速度要快
2、metaclass
一、type--“造物的上帝”
就像str是用来创建字符串对象的类,int是用来创建整数对象的类,而type就是创建类对象的类。
类本身不过是一个名为 type 类的实例。在 Python的类型世界里,type这个类就是造物的上帝。
用户自定义类,只不过是type类的__call__运算符的重载。当我们定义一个类的语句结束时,
真正发生的情况,是 Python 调用 type 的__call__运算符。简单来说,当你定义一个类时,写成下面时:
class MyClass:
data = 1
Python 真正执行的是下面这段代码:
class = type(classname, superclasses, attributedict)
这里等号右边的type(classname, superclasses, attributedict),就是 type 的__call__运算符重载,它会进一步调用:
type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)
当然,这一切都可以通过代码验证,比如下面这段代码示例:
class MyClass:
data = 1
instance = MyClass()
MyClass, instance
#Out: (__main__.MyClass, <__main__.MyClass at 0x4eac358>)
MyClass = type('MyClass', (), {'data': 1})
instance = MyClass()
MyClass, instance
#Out: (__main__.MyClass, <__main__.MyClass at 0x4f915c0>)
instance.data
#Out: 1
由此可见,正常的 MyClass 定义,和手工去调用 type运算符的结果是一样的。
二、metaclass属性
metaclass 是 type 的子类,通过替换 type的__call__运算符重载机制,“超越变形”正常的类。
其实,理解了以上几点,我们就会明白,正是 Python 的类创建机制,给了 metaclass 大展身手的机会。
一旦你把一个类型 MyClass 的 metaclass 设置成 MyMeta,MyClass 就不再由原生的 type创建,而是会调用 MyMeta 的__call__运算符重载。
class = type(classname, superclasses, attributedict)
# 变为了
class = MyMeta(classname, superclasses, attributedict)
class MyMeta(type):
def __new__(cls, *args, **kwargs):
print('===>MyMeta.__new__')
print(cls.__name__)
return super().__new__(cls, *args, **kwargs)
def __init__(self, classname, superclasses, attributedict):
super().__init__(classname, superclasses, attributedict)
print('===>MyMeta.__init__')
print(self.__name__)
print(attributedict)
print(self.tag)
def __call__(self, *args, **kwargs):
print('===>MyMeta.__call__')
obj = self.__new__(self, *args, **kwargs)
self.__init__(self, *args, **kwargs)
return obj
class Foo(object, metaclass=MyMeta):
tag = '!Foo'
def __new__(cls, *args, **kwargs):
print('===>Foo.__new__')
return super().__new__(cls)
def __init__(self, name):
print('===>Foo.__init__')
self.name = name
print('test start')
foo = Foo('test')
print('test end')
#输出
#===>MyMeta.__new__
#MyMeta
#===>MyMeta.__init__
#Foo
#{'tag': '!Foo', '__module__': '__main__', '__init__': <function Foo.__init__ at 0x00000000010F7950>, '__new__': <function Foo.__new__ at 0x00000000010F78C8>, '__qualname__': 'Foo'}
#!Foo
#test start
#===>MyMeta.__call__
#===>Foo.__new__
#===>Foo.__init__
#test end
在创建Foo类的时候,python做了如下操作。
- Foo中有metaclass这个属性吗?如果是,Python会在内存中通过metaclass创建一个名字为Foo的类对象(我说的是类对象,请紧跟我的思路)。
- 如果Python没有找到metaclass,它会继续在父类中寻找metaclass属性,并尝试做和前面同样的操作。
- 如果Python在任何父类中都找不到metaclass,它就会在模块层次中去寻找metaclass,并尝试做同样的操作。
- 如果还是找不到metaclass,Python就会用内置的type来创建这个类对象。
现在的问题就是,你可以在metaclass中放置些什么代码呢?
答案就是:可以创建一个类的东西。那么什么可以用来创建一个类呢?type,或者任何使用到type或者子类化type的东西都可以。用类实现可以(比如上面这个例子),用函数实现也可以。但是metaclass必须返回一个类。
def MyMetaFunction(classname, superclasses, attributedict):
attributedict['year'] = 2019
return type(classname, superclasses, attributedict)
class Foo(object, metaclass=MyMetaFunction):
tag = '!Foo'
def __new__(cls, *args, **kwargs):
print('===>Foo.__new__')
return super().__new__(cls)
def __init__(self, name):
print('===>Foo.__init__')
self.name = name
print('test start')
foo = Foo('test')
print('name:%s,tag:%s,year:%s' % (foo.name, foo.tag, foo.year))
print('test end')
#输出
#test start
#===>Foo.__new__
#===>Foo.__init__
#name:test,tag:!Foo,year:2019
#test end
把上面的例子运行完之后就会明白很多了,正常情况下我们在父类中是不能对子类的属性进行操作,但是元类可以。换种方式理解:元类、装饰器、类装饰器都可以归为元编程(引用自 python-cook-book 中的一句话)。
3、应用
一、实现ORM
我们通过创建一个类似Django中的ORM来熟悉一下元类的使用,通过元类用来创建API是非常好的选择,使用元类的编写很复杂但使用者可以非常简洁的调用API
#一、首先定义Field类,它负责保存数据库表的字段名和字段类型
class Field(object):
def __init__(self, name, column_type):
self.name = name
self.colmun_type = column_type
def __str__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name)
class StringField(Field):
def __init__(self, name):
super().__init__(name, 'varchar(100)')
class IntegerField(Field):
def __init__(self, name):
super().__init__(name, 'bigint')
#二、定义元类,控制Model对象的创建
class ModelMetaClass(type):
def __new__(cls, name, bases, attrs):
if name == 'Model':
return super().__new__(cls, name, bases, attrs)
mappings = dict()
for k,v in attrs.items():
#保持类属性和列的映射关系到mappings字典
if isinstance(v,Field):
print('Found mapping:%s==>%s' % (k, v))
mappings[k] = v
for k in mappings.keys(): #将类属性移除,是定义的类字段不污染User类属性,只在实例中可以访问这些key
attrs.pop(k)
attrs['__table__'] = name.lower() #假设表名为类名的小写,创建类时添加一个__table__属性
attrs['__mappings__'] = mappings #保持属性和列的关系映射,创建类时添加一个__mappings__属性
return super().__new__(cls, name, bases, attrs)
#三、Model基类
class Model(dict, metaclass=ModelMetaClass):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError("'Model' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
def save(self):
fields = []
params = []
args = []
for k,v in self.__mappings__.items():
fields.append(v.name)
params.append('?')
args.append(getattr(self,k,None))
sql = 'insert into %s (%s) values (%s)' % (self.__table__,','.join(fields),','.join(params))
print('SQL:%s' % sql)
print('ARGS:%s' % str(args))
#我们想创建类似Django的ORM,只要定义字段就可以实现对数据库表和字段的操作
#最后、我们使用定义好的ORM接口,使用起来非常简单
class User(Model):
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
user = User(id=1,name='Job',email='job@test.com',password='pw')
user.save()
#输出
#Found mapping:password==><StringField:password>
#Found mapping:id==><IntegerField:id>
#Found mapping:email==><StringField:email>
#Found mapping:name==><StringField:username>
#SQL:insert into user (email,id,password,username) values (?,?,?,?)
#ARGS:['job@test.com', 1, 'pw', 'Job']
二、单例
依照Python官方文档的说法,__new__方法主要是当你继承一些不可变的class时(比如int, str, tuple), 提供给你一个自定义这些类的实例化过程的途径。还有就是实现自定义的metaclass。
简单来说,单例模式的原理就是通过在类属性中添加一个单例判定位ins_flag,通过这个flag判断是否已经被实例化过了,如果被实例化过了就返回该实例。
1)、__new__方法实现单例
class Singleton:
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'): #_instance是类(Singleton)对象的一个属性
cls._instance= super().__new__(cls, *args, **kwargs)
return cls._instance #类的__new__方法之后,必须生成本类的实例(注意是“本类”的实例)才能自动调用本类的__init__方法进行初始化,否则不会自动调用__init__
class SubSingleton(Singleton):
pass
s1 = Singleton()
s2 = Singleton()
print(s1 is s2)
ss1 = SubSingleton()
ss2 = SubSingleton()
print(ss1 is ss2)
#输出
#True
#True
因为重写__new__方法,所以继承至Singleton的类,在不重写__new__的情况下都将是单例模式。 _instance(单下划线开头)属性换成__instance(双下划线开头)会得到不一样的结果,将无法实现单例模式,如果__instance(双下划线开头)属性就变成了私有的(其实变成了_Singleton__instance)。
2)、元类实现单例
class SingletonMeta(type):
def __init__(self,*args,**kwargs):
self.__instance = None #这是一个私有属性来保存属性,而不会污染Singleton类(其实还是会污染,只是无法直接通过__instance属性访问)
super().__init__(*args,**kwargs)
def __call__(self, *args, **kwargs):
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
class Singleton(metaclass=SingletonMeta): #在代码执行到这里的时候,元类中的__new__方法和__init__方法其实已经被执行了,而不是在Singleton实例化的时候执行。且仅会执行一次。
pass
class SubSingleton(metaclass=SingletonMeta):
pass
s1 = Singleton()
s2 = Singleton()
print(s1 is s2)
ss1 = SubSingleton()
ss2 = SubSingleton()
print(ss1 is ss2)
#输出
#True
#True
- 我们知道元类(SingletonMeta)生成的实例是一个类(Singleton),而这里我们仅仅需要对这个实例(Singleton)增加一个属性(__instance)来判断和保存生成的单例。想想也知道为一个类添加一个属性当然是在__init__中实现了。
- 关于__call__方法的调用,因为Singleton是SingletonMeta的一个实例。所以Singleton()这样的方式就调用了SingletonMeta的__call__方法。
三、动态加载
YAML是一个家喻户晓的 Python 工具,可以方便地序列化 / 逆序列化结构数据。YAMLObject 的一个超越变形能力,就是它的任意子类支持序列化和反序列化(serialization & deserialization)。比如说下面这段代码(https://pyyaml.org/wiki/PyYAMLDocumentation):
class Monster(yaml.YAMLObject):
yaml_tag = u'!Monster'
def __init__(self, name, hp, ac, attacks):
self.name = name
self.hp = hp
self.ac = ac
self.attacks = attacks
def __repr__(self):
return "%s(name=%r, hp=%r, ac=%r, attacks=%r)" % (
self.__class__.__name__, self.name, self.hp, self.ac,
self.attacks)
yaml.load("""
--- !Monster
name: Cave spider
hp: [2,6] # 2d6
ac: 16
attacks: [BITE, HURT]
""")
Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])
print yaml.dump(Monster(
name='Cave lizard', hp=[3,6], ac=16, attacks=['BITE','HURT']))
# 输出
!Monster
ac: 16
attacks: [BITE, HURT]
hp: [3, 6]
name: Cave lizard
这里 YAMLObject 的特异功能体现在哪里呢?
你看,调用统一的 yaml.load(),就能把任意一个 yaml 序列载入成一个 Python Object;而调用统一的 yaml.dump(),就能把一个 YAMLObject 子类序列化。对于 load() 和 dump() 的使用者来说,他们完全不需要提前知道任何类型信息,这让超动态配置编程成了可能。听大神说在他的实战经验中,许多大型项目都需要应用这种超动态配置的理念。
比方说,在一个智能语音助手的大型项目中,我们有 1 万个语音对话场景,每一个场景都是不同团队开发的。作为智能语音助手的核心团队成员,我不可能去了解每个子场景的实现细节。
在动态配置实验不同场景时,经常是今天我要实验场景 A 和 B 的配置,明天实验 B 和 C 的配置,光配置文件就有几万行量级,工作量不可谓不小。而应用这样的动态配置理念,我就可以让引擎根据我的文本配置文件,动态加载所需要的 Python 类。
对于 YAML 的使用者,这一点也很方便,你只要简单地继承 yaml.YAMLObject,就能让你的 Python Object 具有序列化和逆序列化能力。是不是相比普通 Python 类,有一点“变态”,有一点“超越”?
YAML 的这种动态序列化 / 逆序列化功能正是用metaclass 实现的。
我们这里只看 YAMLObject 的 load() 功能。简单来说,我们需要一个全局的注册器,让 YAML 知道,序列化文本中的 !Monster 需要载入成 Monster 这个 Python 类型。
一个很自然的想法就是,那我们建立一个全局变量叫 registry,把所有需要逆序列化的 YAMLObject,都注册进去。比如下面这样:
registry = {}
def add_constructor(target_class):
registry[target_class.yaml_tag] = target_class
然后,在 Monster 类定义后面加上下面这行代码:
add_constructor(Monster)
但这样的缺点也很明显,对于 YAML 的使用者来说,每一个 YAML 的可逆序列化的类 Foo 定义后,都需要加上一句话,add_constructor(Foo)。这无疑给开发者增加了麻烦,也更容易出错,毕竟开发者很容易忘了这一点。
那么,更优的实现方式是什么样呢?如果你看过 YAML 的源码,就会发现,正是 metaclass 解决了这个问题。
# Python 2/3 相同部分
class YAMLObjectMetaclass(type):
def __init__(cls, name, bases, kwds):
super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds)
if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None:
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
# 省略其余定义
# Python 3
class YAMLObject(metaclass=YAMLObjectMetaclass):
yaml_loader = Loader
# 省略其余定义
# Python 2
class YAMLObject(object):
__metaclass__ = YAMLObjectMetaclass
yaml_loader = Loader
# 省略其余定义
你可以发现,YAMLObject 把 metaclass 都声明成了YAMLObjectMetaclass,利用 YAMLObjectMetaclass 的__init__方法,为所有 YAMLObject 子类偷偷执行add_constructor()。在 YAMLObjectMetaclass 中,下面这行代码就是魔法发生的地方:
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
YAML 应用 metaclass,拦截了所有 YAMLObject 子类的定义。也就说说,在你定义任何 YAMLObject 子类时,Python 会强行插入运行下面这段代码,把我们之前想要的add_constructor(Foo)给自动加上。
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
所以 YAML 的使用者,无需自己去手写add_constructor(Foo) 。怎么样,是不是其实并不复杂?
4、总结
正常情况下我们在父类中是不能对子类的属性进行操作,但是元类可以,这就使得程序代码的维护变得困难。metaclass 是 Python 的黑魔法之一,在掌握它之前不要轻易尝试。感觉上python的规范原则很松,但这也使得python更灵活,对代码的编写理应由程序员自己负责,自己写的代码还是得自己负责啊。
元类、装饰器、类装饰器都可以归为元编程,它们之间有一些相似点,还是在实际的应用中选择比较,使用合适的工具进行编程吧。