Python-面向对象编程进阶-元类
一、基本概念
什么是元类?
对象
→ 通过调用类
实例化产生
类
→ 通过调用元类
实例化产生
元类 :就是用来实例化产生类
的类
。
我们使用
class
关键字来定义一个类,背后发生的事情实质就是调用了元类
。
class test:
pass
print(type(test)) # <class 'type'>
如上所示,type
就是元类,我们用class
关键字定义的所有的类
以及内置的类
都是由元类type
实例化产生的。
至于type的妈是谁,不要去想,反正不可能是 Java 。如果非要想,不如认为它自己是自己的妈。
二、使用class关键字创建类的机制分析
我们使用
class
关键字来定义一个类,背后发生的事情实质就是调用了元类
。
类有三大基本特征:
- 类名
- 类的基类(父类),没有默认为
object
- 类体代码(执行类体代码拿到的
类的名称空间
)
满足了三个基本特征之后,就可以调用元类进行实例化。如下所示:
# 1、类名
name = "test"
# 2、类的基类
bases = () # bases = (object,) 是一个元组
# 3、执行类体代码拿到类的名称空间
dic = {} # 包含属性名称和值的字典
# 4、调用元类
test = type(name, bases, dic)
print(test) # <class '__main__.test'>
print(type(test)) # <class 'type'>
print(test.__name__) # test
print(test.__bases__) # (<class 'object'>,)
我们现在并没有使用class
关键字,而是通过调用元类的方式创建了一个类。
而使用class
关键字造类,class
在底层帮我们做的,也是这四步。
type
是python内置的元类,我们还可以自定义元类,来控制类的产生。
三、如何自定义元类
只有继承了
元类type
的类才可以作为元类
使用
一个类没有声明自己的元类,则默认
它的元类
就是type
。除了使用内置元类type
,我们也可以通过继承type
来自定义元类,然后使用metaclass
关键字参数为一个类指定
元类。
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
pass
class test(metaclass=Mymeta):
pass
自定义元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程。所以上面代码的实质即:
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
pass
test = Mymeta("test", (object,), {})
在上面的代码中,test = Mymeta("test", (object,), {})
就是在调用元类Mymeta
。
而调用Mymeta
发生的三件事:
- 先造一个空对象 => 名叫test
- 调用
Mymeta
这个类内的__init__
方法,完成初始化对象的操作 - 返回初始化好的对象(这个对象就是类test)
我们来测试一下,在自定义元类的__init__
方法中加入输出语句:
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
print('我波澜不惊的运行了')
class test(metaclass=Mymeta):
pass
# 运行结果:
# 我波澜不惊的运行了
利用这一特性,我们可以实现一个检测小功能:检测类体内是否存在注释说明,无注释说明则报错。(使用raise
来主动抛出异常)
代码如下:
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0:
raise TypeError('类中必须有文档注释,并且文档注释不能为空')
else:
print('无事发生')
class test(metaclass=Mymeta):
pass
# 运行结果:抛出异常
# TypeError: 类中必须有文档注释,并且文档注释不能为空
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0:
raise TypeError('类中必须有文档注释,并且文档注释不能为空')
else:
print('无事发生')
class test(metaclass=Mymeta):
'''
铁憨憨到此一游
'''
pass
# 运行结果:
# 无事发生
四、__new__
与__call__
我们知道,调用元类产生类发生了三件事,第一步就是造对象。而造对象,必定对应着某种方法,这个方法就是
__new__
。
让我们来回顾一下这个过程:
⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔分⚔隔⚔线⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔
其实,在第一步(造空对象)时,实质上就是调用了元类Mymeta
下的__new__
方法。
然后,再调用Mymeta
这个类内的__init__
方法,完成初始化对象的操作。
最后,返回初始化好的对象。
我们来用一个案例演示:
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
print('__init__运行了')
def __new__(cls, *args, **kwargs):
print('__new__运行了')
print(cls, args, kwargs)
test = Mymeta("test", (object,), {})
#运行结果:
# __new__运行了
# <class '__main__.Mymeta'> ('test', (<class 'object'>,), {}) {}
我们看到,__new__
方法运行了,但是__init__
方法并没有运行。这是因为我们覆盖重写的__new__
没有返回值,即没有返回空对象,所以__init__
没有执行。
我们看到,给__new__
方法传入的参数,就是当前所在的类,以及调用类时所传入的参数。例中为Mymeta
类、和定义的类名、基类、类体代码字典(名称空间)。
要想实现让__init__
成功执行,有两种解决办法:
-
利用
super()
调用父类(type)的__new__方法
class Mymeta(type): def __init__(self, class_name, class_bases, class_dic): print('__init__运行了') def __new__(cls, *args, **kwargs): print('__new__运行了') return super().__new__(cls, *args, **kwargs) # 返回一个对象 test = Mymeta("test", (object,), {}) # 运行结果: # __new__运行了 # __init__运行了
-
指名道姓调用type的
__new__
方法class Mymeta(type): def __init__(self, class_name, class_bases, class_dic): print('__init__运行了') def __new__(cls, *args, **kwargs): print('__new__运行了') return type.__new__(cls, *args, **kwargs) # 返回一个对象 test = Mymeta("test", (object,), {}) # 运行结果: # __new__运行了 # __init__运行了
其实两种方法的逻辑是一样的,只是形式上的不同。
强调: 只要是调用类,那么会一次调用
- 类内的
__new__
- 类内的
__init__
好了,我们现在已经知道__new__
方法是干什么用的了,而还有一个方法不得不提,就是__call__
方法。
我们说,test = Mymeta("test", (object,), {})
就是在调用元类Mymeta
。造对象(类)是应用了__new__
方法,给对象进行初始化是应用了__init__
方法,那么能让test
通过加括号的方式就进行调用,就是因为__call__
方法在背后疯狂输出。
class Foo:
def __call__(self, *args, **kwargs):
print(self)
return '返回值'
obj = Foo()
# 1、要想让obj这个对象变成一个可调用的对象,需要在该对象的类中定义一个__call__方法,该方法会在调用对象时自动触发
# 2、调用obj的返回值就是Foo中__call__方法的返回值
res = obj() # <__main__.Foo object at 0x000001FFF2E822B0>
print(res) # 返回值
直接一点说,调用对象(x)这件事情的实质,是在调用实例化出对象(x)的那个对象(y)的
__call__
方法。(也就是对象他妈的__call__
方法)
y —实例化—> x(y是x的妈)
那么我们就可以得出一个结论:如果想让一个对象可以加括号调用,就需要在该对象的类中添加一个__call__
方法。
那么对应的:
- 对象() --> 类内的
__call__
- 类() --> 自定义元类内的
__call__
- 自定义元类() --> 内置元类
type
的__call__
我们来梳理一遍整个流程:
我们定义一个类走的四步:
- 定义类名
- 定义类的基类
- 执行类体代码,拿到类的名称空间
- 调用元类,而调用元类也走了四步:
- 调用元类这件事情的本质是调用元类他妈的
__call__
方法,比如调用自定义元类Mymeta
就是在调用元类type的__call__
方法 - 造空对象(类),调用的是元类的
__new__
方法 - 初始化对象(类),调用的是元类的
__init__
方法 - 返回初始化好的对象(类)
- 调用元类这件事情的本质是调用元类他妈的
五、自定义元类控制类的调用 ==》控制类的对象的产生
我们通过自定义一个元类,可以控制类的产生。那我们就可以通过调用产生的这个类,来实现控制对象的产生。
现在我们已经做的事情:定义了Mymeta
元类,并调用Mymeta
实例化出了test
类。
然后我们来调用test
类实例化出一个obj
对象:
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
pass
def __new__(cls, *args, **kwargs):
return type.__new__(cls, *args, **kwargs)
def __call__(self, *args, **kwargs):
print('call哥我运行啦')
test = Mymeta("test", (object,), {})
obj = test()
# 运行结果:
# call哥我运行啦
相对应的,调用类test
来实例化对象obj
也走了四步:
- 调用类
test
就是在调用元类Mymeta
的__call__
方法 Mymeta.__call__
函数内会先调用test
内的__new__
方法Mymeta.__call__
函数内会再调用test
内的__init__
方法Mymeta.__call__
函数内会返回一个初始化好的对象
为了证明,对象obj
是由Mymeta.__call__
函数返回的,我们来检验一下:
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dic):
pass
def __new__(cls, *args, **kwargs):
return type.__new__(cls, *args, **kwargs)
def __call__(self, *args, **kwargs):
return '傻眼了吧'
test = Mymeta("test", (object,), {})
obj = test()
print(obj) # 傻眼了吧
我们来理一下整个流程:
class Mymeta(type):
def __call__(self, *args, **kwargs):
# 1、Mymeta.__call__函数内会先调用test内的__new__
test_obj = self.__new__(self)
# 2、Mymeta.__call__函数内会调用test内的__init__
self.__init__(test_obj, *args, **kwargs)
# 3、Mymeta.__call__函数内会返回一个初始化好的对象
return test_obj
class test(metaclass=Mymeta):
def __new__(cls, *args, **kwargs):
# 要产生真正的对象
return object.__new__(cls) # 调用的是object下的new方法,也就是为什么类会默认继承object类的重要原因。
def __init__(self, name, say):
self.name = name
self.say = say
obj = test('山炮','致敬')
# test类的调用
# 1、obj=test('山炮','致敬')就是在调用Mymeta.__call__
# 2、Mymeta.__call__函数内会先调用test内的__new__创造obj空对象
# 3、Mymeta.__call__函数内会调用test内的__init__进行初始化
# 4、Mymeta.__call__函数内会返回一个初始化好的对象
流程分析:
- 首先我们自定义了一个元类
Mymeta
。 - 我们调用
Mymeta
产生了一个类test
(过程省略) - 我们写下了
obj=test('山炮','致敬')
obj=test('山炮','致敬')
调用了类test
,实质上是调用自定义元类Mymeta
的__call__
函数(方法)Mymeta.__call__
函数内会先调用test
内的__new__
test.__new__
实际上调用的是object类
下的new
方法,并返回给我们一个空对象- 然后
Mymeta.__call__
函数调用test
内的__init__
给对象进行初始化 - 给对象添加
self.name = '山炮'
,self.say = '致敬'
两个属性 Mymeta.__call__
函数返回一个初始化好的对象给obj
其实这个流程所对应的代码就是一个,自定义元类控制类的对象生成,的模板。(例中为Mymeta
控制test
的对象生成)
六、属性查找
我们来将
继承
与元类
的知识结合起来,重新看一看对象的属性查找。(切记父类跟元类是两码事,可以理解为父类与子类是父子关系,元类与类是母子关系)
我们已知的属性查找的顺序为:对象 ——》类 ——》父类
如果加上元类,那么查找顺序是什么呢?
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 test(Foo, metaclass=Mymeta):
n = 111
def __init__(self, name, age):
pass
print(test.n) # 自下而上依次注释各个类中的n=xxx,然后重新运行程序,发现n的查找顺序为test->Foo->Bar->object->Mymeta->type
结构图为:
查找顺序即为:
- 先找继承层:
test -> Foo -> Bar -> object
- 后找元类层:
Mymeta -> type
七、总结
元类定义的目的
- 控制类的产生
__new__
__init__
- 控制类的调用
__call__