[Python学习日记-70] 元类
简介
什么是元类
关键字 class 创建类的流程分析
自定义元类控制类的创建
自定义元类控制类的调用
自定义元类的属性查找
自定义元类的应用与练习
简介
在上一篇章当中我们已经了解了面向对象的各种内置函数了,本篇我们将讲述“元类”,它是 Python 面向对象编程的深层次知识,学会了元类可以做到很多神奇的姿势,这次就带大家一起来探讨一下什么是元类,我们应该如何定制自己的元类,我们应该怎么调用我们自己的元类。
什么是元类
在介绍什么是元类之前我们先定义一个类作为我们的分析对象,如下
class Chinese: # Python3 中默认就是新式类,即 Chinese(object)
country = 'China'
def __init__(self,name,age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
c1 = Chinese('jove',28)
print(type(c1)) # 查看对象c1的类是 ——> <class '__main__.Chinese'>
代码输出如下:
在前面的许多篇博客当中我们都提过一个特性:Python 中的一切皆为对象;而所有的对象都是实例化而得到的,就像上面的 c1 是调用类 Chinese 实例化后得到的,那对象可以怎么用呢?对象有以下四点特性:
- 都可以被引用,即 x = obj
- 都可以当作函数的参数传入
- 都可以作当函数的返回值
- 都可以当作容器类的元素,即 l = [func,time,obj,1,...]
只要是对象就会拥有上面的四点特性,上面的 c1 很明显是一个对象了,其实我们所创建的类 Chinese 的本质也是一个对象,它也拥有以上四点特性。
既然所有的对象都是调用类得到的,那么 Chinese 也必然是调用了一个类得到的,我们把这个被 Chinese 调用的类就称为元类,总的来说产生 Chinese 的过程一定发生了:Chinese = 元类(...),即产生类的类就是元类;我们可以通过以下代码查看 Chinese 的元类是什么,如下
print(type(Chinese))
代码输出如下:
从输出来看 Chinese 的产生是调用了 type 这个元类,即默认的元类为 type,类与对象的产生过程如下图所示
关键字 class 创建类的流程分析
在分析创建类的流程之前我们要先补充一下 exec 方法的用法,exec 方法在创建类时起到了关键的作用,它是用于执行动态生成的代码,并会生成相应的作用域/名称空间,exec 方法的语法如下
exec(code,globals_dic,locals_dic)
- code:一系列python代码的字符串
- globals_dic:全局作用域(字典形式),如果不指定,默认为globals()
- ocals_dic:局部作用域(字典形式),如果不指定,默认为locals()
我们可以把 exec 方法的执行当成是一个函数的执行,会将执行期间产生的名字存放于局部名称空间中,演示代码如下
g = {
'x':1,
'y':2
}
l = {}
exec("""
global x,m
x = 10
m = 100
z = 3
""",g,l)
print(g)
print(l)
代码输出如下:
{'x': 10, 'y': 2, ..., 'm': 100}
{'z': 3}
补充完 exec 方法的使用后我们书接上文,前面我们说到,使用关键字 class 创建的类 Chinese 本身也是一个对象,负责产生该对象的类被我们称之为元类,而在 Python 中内置的元类就是 type。关键字 class 在帮我们创建类的时候必然会帮我们调用元类 type,即 Chinese = type(...),元类 type 进行实例化的时候会依次传入以下三个参数,这三个参数就是类的关键组成部分,分别是
- 类名:class_name = 'Chinese'
- 基类(父类)们:class_bases = (object,)
- 类的名称空间:class_dic,类的名称空间是执行类体代码而得到的
总的来说,关键字 class 帮我们创建一个类分为以下四个过程:
到这里我们知道了,其实我们用关键字 class 创建的类也只是一个用元类 type 创建的对象而已,那也就是说其实我们也可以自己用元类 type 来创建类,并不需要使用关键字 class,总的来说在 Python 中定义类有两种方式:
- 方式一:关键字 class 创建
- 方式二:由元类 type 创建
为了验证我们分析的正确性,我们分别使用两种创建类的方式来创建两个类来对比一下,代码如下
# 定义类的两种方式:
# 方式一: class
class Chinese: # Chinese = type(...)
country = 'China'
def __init__(self,name,age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
obj = Chinese('jove',28)
print(obj,obj.name,obj.age)
# print(type(obj.talk))
# print(Chinese.__bases__)
# 方式二: type
# 定义类的三要素
class_name = 'Chinese' # 类名
class_bases = (object,) # 基类(父类)
# 类里的代码
class_body = """
country = 'China'
def __init__(self,name,age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
"""
class_dic = {}
exec(class_body,globals(),class_dic) # 执行类里的代码,并把类里面的属性(非功能性代码)都放到dict --> locals() 里面
# print(class_dic)
Chinese1 = type(class_name,class_bases,class_dic) # 最后传入所需的要素到type()当中
obj1 = Chinese1('jove',28)
print(obj1,obj1.name,obj1.age)
代码输出如下:
自定义元类控制类的创建
经过前面一大轮的分析,我们已经清楚了 Python 当中默认的元类是 type,而我们能使用 metaclass 关键字参数为一个类指定元类,在默认的情况下如下
class Chinese(object,metaclass=type): # 默认metaclass就等于type
country = 'China'
def __init__(self,name,age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
而我们可以通过继承 type 来自定义元类,然后使用 metaclass 关键字参数为一个类指定自定义元类即可,如下
class Mymeta(type): # 只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
def __init__(self,class_name,class_bases,class_dic):
print(class_name)
print(class_bases)
print(class_dic)
super(Mymeta,self).__init__(class_name,class_bases,class_dic) # 重用父类的功能
class Chinese(object,metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)
country = 'China'
def __init__(self,name,age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
代码输出如下:
从输出可以看出,在创建类 Chinese 的时候同时调用了自定义元类 Mymeta,从这可以看出自定义元类可以控制类的产生过程,而类的产生过程其实就是元类的调用过程,即 Chinese = Mymeta(class_name,class_bases,class_dic),在调用 Mymeta 时会先产生一个空对象 Chinese,然后连同调用 Mymeta 括号内的参数一同传给 Mymeta 下的 __init__ 方法来完成初始化,这样我们可以基于这个调用机制来做一些关于创建类的限制,例如限制类名的书写格式,代码如下
class Mymeta(type):
def __init__(self,class_name,class_bases,class_dic):
if not class_name.istitle(): # 类名开头必须为大写
raise TypeError('类名的首字母必须大写') # 异常处理后面会有专门的篇章介绍
if '__doc__' not in class_dic or not class_dic['__doc__'].strip(): # strip()如果为空则自带布尔值 false
raise TypeError('类必须写注释,且不能为空')
super(Mymeta,self).__init__(class_name,class_bases,class_dic)
class Chinese(object,metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)
'''
中国人的类
'''
country = 'China'
def __init__(self,name,age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
cn = Chinese('jove',28)
cn.talk()
代码输出如下:
当代码没有按照要求类名的首字母大写时
当代码当中没有注释说明时
当所有要求都符合时
自定义元类控制类的调用
在学习自定义元类的调用之前我们需要先掌握 __call__ 方法的使用,这在之前已经介绍过了,可以点击链接查看。了解完 __call__ 方法之后,我们知道调用一个对象,就是触发对象所在类中的 __call__ 方法的执行,如果把 Chinese 也当做一个对象,那么在 Chinese 这个对象的类中也必然存在一个 __call__ 方法,即 Chinese 的元类里面也应该有一个 __call__ 方法,会在 Chinese() 调用时触发,如下
class Mymeta(type):
def __init__(self,class_name,class_bases,class_dic):
if not class_name.istitle(): # 类名开头必须为大写
raise TypeError('类名的首字母必须大写') # 异常处理后面会有专门的篇章介绍
if '__doc__' not in class_dic or not class_dic['__doc__'].strip(): # strip()如果为空则自带布尔值 false
raise TypeError('类必须写注释,且不能为空')
super(Mymeta,self).__init__(class_name,class_bases,class_dic)
def __call__(self, *args, **kwargs): # 如果没有写这个,将会找父类的__call__方法 obj = Chinese('egon', 18)
print(self) # self = Chinese
print(args) # arge = ('jove',)
print(kwargs) # kwarge = {'age': 28}
class Chinese(object,metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)
'''
中国人的类
'''
country = 'China'
def __init__(self,name,age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
cn = Chinese('jove',28) # Chinese.__call__(Chinese,'jove',28)
print(cn)
代码输出如下:
从输出结果来看,调用 Chinese 对象的时候就是在调用 Chinese 类中的 __call__ 方法,然后会把 Chinese 传递给 self,而溢出的位置参数和关键字参数分别传递给 *args 和 **kwargs,最后调用 Chinese 的返回值就是调用的 __call__ 方法的返回值,这里的 __call__ 方法没有指定返回值所以打印 cn 时就是 None。
很明显的看出,我们自定义的 __call__ 还没有实现实例化对象 cn 的功能,那应该怎么做呢?默认地,在调用 cn = Chinese('jove',28) 时 __call__ 应该做以下三件事:
- 产生一个空对象 obj
- 调用 __init__ 方法初始化对象 obj
- 返回初始化好的 obj
实现代码如下
class Mymeta(type):
def __init__(self,class_name,class_bases,class_dic):
if not class_name.istitle(): # 类名开头必须为大写
raise TypeError('类名的首字母必须大写') # 异常处理后面会有专门的篇章介绍
if '__doc__' not in class_dic or not class_dic['__doc__'].strip(): # strip()如果为空则自带布尔值 false
raise TypeError('类必须写注释,且不能为空')
super(Mymeta,self).__init__(class_name,class_bases,class_dic)
def __call__(self, *args, **kwargs): # 如果没有写这个,将会找父类的__call__方法 obj = Chinese('egon', 18)
# 第一件事: 调用__new__造出一个空对象obj
obj = object.__new__(self) # 此处的self是类Chinese,必须传参,代表创建一个Chinese的对象obj
# 第二件事: 调用__init__方法初始化空对象obj
self.__init__(obj, *args, **kwargs)
# 第三件事: 返回初始化好的 obj
return obj
class Chinese(object,metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)
'''
中国人的类
'''
country = 'China'
def __init__(self,name,age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
cn = Chinese('jove',28) # Chinese.__call__(Chinese,'jove',28)
print(cn)
代码输出如下:
从输出结果来看已经看到 cn 已经变成了类 Chinese 的一个对象了,这个时候已经完成了实例化,而上面代码当中的 __call__ 其实只相当于一个模版而已,我们还能在此基础上改写 __call__ 的逻辑从而控制调用 Chinese 的过程,例如把 Chinese 实例化的对象的所有属性都变成私有属性,如下
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
if not class_name.istitle(): # 类名开头必须为大写
raise TypeError('类名的首字母必须大写') # 异常处理后面会有专门的篇章介绍
if '__doc__' not in class_dic or not class_dic['__doc__'].strip(): # strip()如果为空则自带布尔值 false
raise TypeError('类必须写注释,且不能为空')
super(Mymeta, self).__init__(class_name, class_bases, class_dic)
def __call__(self, *args, **kwargs): # 如果没有写这个,将会找父类的__call__方法 obj = Chinese('egon', 18)
# 第一件事: 调用__new__造出一个空对象obj
obj = object.__new__(self) # 此处的self是类Chinese,必须传参,代表创建一个Chinese的对象obj
# 第二件事: 调用__init__方法初始化空对象obj
self.__init__(obj, *args, **kwargs)
# 在初始化之后,obj.__dict__里就有值了
print(obj.__dict__)
obj.__dict__ = {'_%s__%s' % (self.__name__, k): v for k, v in obj.__dict__.items()}
# 第三件事: 返回初始化好的 obj
return obj
class Chinese(object, metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)
'''
中国人的类
'''
country = 'China'
def __init__(self, name, age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
cn = Chinese('jove', 28) # Chinese.__call__(Chinese,'jove',28)
print(cn.__dict__)
代码输出如下:
自定义元类的属性查找
到这里基本就介绍完元类了,在学习完元类之后再来看看结合了继承和元类之后的属性查找应该是怎么样的一个查找顺序呢?我们先来写一段代码,如下
class Mymeta(type):
n=444
def __call__(self, *args, **kwargs):
obj=self.__new__(self)
self.__init__(obj,*args,**kwargs)
return obj
class Bar(object):
n=333
class Foo(Bar):
n=222
class Chinese(Foo,metaclass=Mymeta):
n=111
country = 'China'
def __init__(self, name, age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
print(Chinese.n) # 自下而上依次注释各个类中的n=xxx,然后重新运行程序,发现n的查找顺序为Chinese -> Foo -> Bar -> object -> Mymeta -> type
代码输出如下:
注释掉 Chinese 下的 n=111 后
· 注释掉 Foo 下的 n=222 后
注释掉 Bar 下的 n=333 后
最后注释掉 Mymeta 下的 n=444 后直接报错找不到了
在前面我们学习过继承的实现原理,如果把类当成对象去看,上面代码的继承关系是:对象 Chinese 继承对象 Foo,对象 Foo 继承对象 Bar,对象 Bar 继承对象 object。我们应该把属性的查找分成两层,一层是对象层的查找,即基于 c3 算法的 MRO 方法调用顺序;另一层则是类层的查找,即对元类层的查找,查找顺序如下
当对对象中的属性进行查找时会按以下顺序进行查找:
- 对象层:Chinese -> Foo -> Bar -> object
- 元类层:Mymeta -> type
通过上面的分析,我们现在知道在属性查找的时候元类也会参与其中,我们在之前使用 __call__ 方法实现实例化的时候用到了 self.__new__,下面我们来分析一下这个 __new__ 到底是调用了哪里的,我们先写下一段代码运行看看,如下
class Mymeta(type):
n=444
def __call__(self, *args, **kwargs):
obj=self.__new__(self)
print(self.__new__ is object.__new__) # 当前面的__new__都注释掉之后这个就是True了
class Bar(object):
n=333
# def __new__(cls, *args, **kwargs):
# print('Bar.__new__')
class Foo(Bar):
n=222
# def __new__(cls, *args, **kwargs):
# print('Foo.__new__')
class Chinese(Foo,metaclass=Mymeta):
n=111
country = 'China'
def __init__(self, name, age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
# def __new__(cls, *args, **kwargs):
# print('Chinese.__new__')
Chinese('jove',28)
代码输出如下:
上面代码中注释的 __new__ 方法可以自己运行一下,这样可以更加清楚的看到 self.__new__ 的查找情况 。一番操作后发现 Mymeta 下的 __call__ 里的 self.__new__ 的查找顺序是 Chinese -> Foo -> Bar -> object,而且经过 self.__new__ 和 object.__new__ 的比对之后发现这两个是一样的,那就是说 self.__new__ 的查找并没有找到元类当中,而是会去找 object 里的 __new__,而 object 下默认就有一个 __new__,所以即便是之前的类均未实现 __new__,也一定会在 object 中找到一个,根本不会再去找元类 Mymeta 和 type 中查找 __new__。
那我们是否可以在元类的 __call__ 中用 object.__new__(self) 代替 self.__new__(self) 去造对象呢?原则上是可以的,因为通过属性查找最终还是会找到 object.__new__,但是并不推荐这样做,因为直接使用 object.__new__ 会直接跳过之前的 Chinese、Foo 和 Bar 三个类的检索,如果后期在他们当中想做 __new__ 的自定义的话会造成一定的麻烦。
那什么情况下才会去元类层查找 __new__ 呢?在产生类 Chinese 的过程就是在调用 Mymeta,而 Mymeta 也是 type 类的一个对象,那么 Mymeta 之所以可以调用,一定是在元类 type 中也有一个 __call__ 方法,而这个 __call__ 方法也同样需要做至少三件事,如下
class type:
def __call__(self, *args, **kwargs): # self=<class '__main__.Mymeta'>
obj=self.__new__(self,*args,**kwargs) # 产生Mymeta的一个对象
self.__init__(obj,*args,**kwargs)
return obj
这个时候 type 中的 self.__new__ 进行检索的时候就会先对 Mymeta 进行检索,然后再对 type 进行检索,所以我们可以通过这个逻辑来定制我们的自定义元类 Mymeta,如下
class Mymeta(type):
n=444
def __new__(cls, *args, **kwargs):
obj=type.__new__(cls,*args,**kwargs) # 必须按照这种传值方式
print(obj.__dict__)
return obj # 只有在返回值是type的对象时,才会触发下面的__init__
# return 123
def __init__(self,class_name,class_bases,class_dic):
print('run。。。')
class Chinese(Foo,metaclass=Mymeta):
n=111
country = 'China'
def __init__(self, name, age):
self.name = name
self.age = age
def talk(self):
print('%s is talking' % self.name)
print(type(Mymeta))
代码输出如下:
当返回 type 的对象时
当返回的不是 type 的对象时
自定义元类的应用与练习
一、单例模式
在详细介绍完元类之后我们再来讲讲元类的一些实际应用——单例模式。什么是单例模式呢?在 Python 当中我们定义两个不同的变量,但是值是一样的,如下
obj1 = int(1) # obj1 = 1
obj2 = int(1) # obj2 = 1
print(obj1 is obj2)
代码输出如下:
obj1 和 obj2 两个对象是指向相同的内存地址的,而单例模式要做的事就是把相同特征的对象只产生一个内存地址,从而节约内存空间,下面我们会用两种方式来实现单例模式,如下
配置文件 setting.py:
HOST = '127.0.0.1'
PORT = 1000
实现方式一:不使用元类
import settings
class MySQL:
__instance = None
def __init__(self,host,port):
self.host = host
self.port = port
@classmethod
def singleton(cls):
if not cls.__instance: # 这样的赋值方法只适用于self的特征写死的情况下
obj = cls(settings.HOST,settings.PORT)
cls.__instance = obj
return cls.__instance
def conn(self):
pass
def execute(self):
pass
# 对象的特征(属性)不同,内存地址不同
obj1 = MySQL('1.1.1.2',3306)
obj2 = MySQL('1.1.1.3',3307)
print(obj1 is obj2)
# 对象的特征(属性)相同,内存地址相同
obj1 = MySQL.singleton()
obj2 = MySQL.singleton()
print(obj1 is obj2)
代码输出如下:
实现方式二:使用元类来实现
import settings
class Mymeta(type):
def __init__(self,name,bases,dic): #定义类Mysql时就触发
# 事先先从配置文件中取配置来造一个Mysql的实例出来
self.__instance = object.__new__(self) # 产生对象
self.__init__(self.__instance, settings.HOST, settings.PORT) # 初始化对象
# 上述两步可以合成下面一步
# self.__instance=super().__call__(*args,**kwargs)
super().__init__(name,bases,dic)
def __call__(self, *args, **kwargs): #Mysql(...)时触发
if args or kwargs: # args或kwargs内有值
obj=object.__new__(self)
self.__init__(obj,*args,**kwargs)
return obj
return self.__instance
class Mysql(metaclass=Mymeta):
def __init__(self,host,port):
self.host=host
self.port=port
obj1=Mysql() # 没有传值则默认从配置文件中读配置来实例化,所有的实例应该指向一个内存地址
obj2=Mysql()
obj3=Mysql()
print(obj1 is obj2 is obj3)
obj4=Mysql('1.1.1.4',3307)
print(obj4 is obj1)
代码输出如下:
二、在元类中控制把自定义类的数据属性都变成大写
class Mymeta(type):
def __new__(cls, class_name, class_base, class_dict):
update_class_dict = {}
''.endswith('__')
for k in class_dict:
if not callable(class_dict[k]) and not k.startswith('__') and not k.startswith('_'):
update_class_dict[k.upper()] = class_dict[k]
else:
update_class_dict[k] = class_dict[k]
return type.__new__(cls, class_name, class_base, update_class_dict)
class Chinese(metaclass=Mymeta):
country = 'China'
tag = 'Legend of the Dragon' # 龙的传人
__ismarry = 'yes'
def __init__(self):
self.name = 'Zou'
def walk(self):
print('%s is walking' % self.name)
print(Chinese.__dict__)
代码输出如下:
{'__module__': '__main__', 'COUNTRY': 'China', 'TAG': 'Legend of the Dragon', '_Chinese__ismarry': 'yes', '__init__': <function Chinese.__init__ at 0x000001A082A48B80>, 'walk': <function Chinese.walk at 0x000001A082A499E0>, '__dict__': <attribute '__dict__' of 'Chinese' objects>, '__weakref__': <attribute '__weakref__' of 'Chinese' objects>, '__doc__': None}
三、在元类中控制自定义的类无需 __init__ 方法
- 元类帮其完成创建对象,以及初始化操作
- 要求实例化时传参必须为关键字形式,否则抛出异常 TypeError: must use keyword argument
- key 作为用户自定义类产生对象的属性,且所有属性变成大写
class Mymeta(type):
def __call__(self, *args, **kwargs):
if args:
raise TypeError('must use keyword argument for key function')
obj = object.__new__(self)
for k in kwargs:
obj.__dict__[k.upper()] = kwargs[k]
return obj
class Chinese(metaclass=Mymeta):
country = 'China'
tag = 'Legend of the Dragon' # 龙的传人
__ismarry = 'yes'
def walk(self):
print('%s is walking' % self.name)
obj1 = Chinese(name = 'jove',age = 28)
print(obj1.__dict__)
代码输出如下:
四、在元类中控制自定义的类产生的对象相关的属性全部为隐藏属性
class Mymeta(type):
def __call__(self, *args, **kwargs):
# 控制Chinese对象的创建过程
if args:
raise TypeError('must use keyword argument for key function')
obj = object.__new__(self)
for k in kwargs:
obj.__dict__['_%s__%s' % (self.__name__, k)] = kwargs[k]
return obj
class Chinese(metaclass=Mymeta):
country = 'China'
tag = 'Legend of the Dragon' # 龙的传人
__ismarry = 'yes'
def walk(self):
print('%s is walking' % self.name)
p = Chinese(name = 'jove',age = 28)
print(p.__dict__)
代码输出如下: