4.15
今日内容
-
反射
-
内置方法
__str__
__del__
不用调,会在满足某种条件时自动触发
-
元类
反射
python:强类型,动态解释型语言,反射是python被视为动态的关键
什么是反射:在程序运行过程中,可以“动态”获取对象的信息
为何用反射:得到一个未知的对象,如在协同工作时得到别人代码中的对象,想查看这个对象的属性,看能够调用其中的什么功能
- 使用
dir(obj)
得到对象的属性列表,但是列表中存放的是字符串,不能直接通过这个列表使用这个属性 - 通过对象的
.__dict__['name']
可以使用到这个属性
内置函数
通过字符串来操作属性值
hasattr(obj,‘name’):看obj中有没有’name’ 这个属性,返回True / False
getattr(obj,‘name’,None):相当于obj.name,若不存在name则返回默认值None,name如果是数据则返回值,是方法则返回绑定方法或函数地址
setattr(t,‘age’,18) :等同于t.age=18
**delattr(t,‘age’) **:等同于del t.age
使用四个内置函数,就可以通过字符串来使用功能,可以判断字符串对应的功能是否存在,加额外的逻辑,而不是没有这个方法就报错了
内置方法
什么是:定义在类内部,以__开头和结尾的方法,不是用来直接调用的,而是满足某种条件后自动执行
为什么用:为了定制化我们的类或对象
以 __str__
和 __del__
为例介绍
如何使用内置方法
打印对象,就会触发对象下 __str__
方法的运行
class A:
def __init__(self,name,age):
self.name = name
self.age = age
def __str__(self):
# 返回要打印的值
return f'{self.name}:{self.age}'
obj = A('deimos',21)
print(obj) # 自动触发 __str__,将返回值当作本次打印的结果输出,返回值必须是字符串类型
清理对象,会先执行对象下的 __del__
方法。清理对象包括函数结束,回收名称空间,del 对象,引用计数为0自动垃圾回收
class A:
def __init__(self,name,age):
self.name = name
self.age = age
def __del__(self):
print('清理了对象')
obj = A('deimos',21)
# 运行结束,回收名称空间自动执行 __del__
在清理名称空间时,释放的是程序占用的内存空间,假如涉及到文件操作,占用了系统资源,此时可以在del内置方法中设置回收系统资源
像int类没有自带的内置函数,可以自定义一个类,继承int的属性,再在自定义类中添加自己的方法
元类介绍
源于一句话:一切皆对象
什么是元类:
实例化类得到对象,类也是对象,通过类实例化产生,这个产生类的类就称作元类
元类经过实例化得到自定义类,自定义类实例化得到对象
使用 type(obj) 可以看到obj的类名,使用type(class) 则可以看到定义类的类,就是元类:type
class关键字造类的步骤
类有三个特征:类名,类的父类(object),通过类体代码拿到类的名称空间
-
先拿到类名
-
拿到基类
-
class机制会运行类体代码,其中产生的名字都放进类的名称空间
-
class机制会调用默认的元类type
type(class_name,class_bases,class_dic)
返回一个类
# 1、类名
class_name="People"
# 2、类的基类
class_bases=(object,)
# 3、执行类体代码拿到类的名称空间
class_dic={}
class_body="""
def __init__(self,name,age):
self.name=name
self.age=age
def say(self):
print('%s:%s' %(self.name,self.name))
"""
exec(class_body,{},class_dic)
# print(class_dic)
# 4、调用元类
People=type(class_name,class_bases,class_dic)
obj = People('deimos',22)
前三步都不是能控制的,第四步使用的是内置的type,有操作的空间,可以自定义元类,来控制类的产生
如何自定义元类控制类的产生
class Mymeta(type):
pass
class People(metalclass = Mymeta):
def __init__(self,name,age):
self.name=name
self.age=age
def say(self):
print('%s:%s' %(self.name,self.name))
自定义一个元类Mymeta,必须继承元类type。此时产生类People,第四步会使用元类Mymeta,相当于:
People = Mymeta('People',(object,),{class_dic...})
# type会给你在基类里加object,所以python3中都是新式类
调用Mymeta发生的三件事
- type.call 执行__new__ 先造一个空对象 People
- type.call 调用 Mymeta类内的 __init__方法,完成初始化对象的操作
- 返回初始化好的对象
手动抛出异常:raise 异常名(‘异常信息’)
自定义init
在第二步,init中,可以在元类里,产生类的时候加逻辑,强制定义类的时候以固定的格式
class Mymeta(type):
def __init__(self,x,y,z):
if not x.istitle():
raise NameError('类名的首字母必须大写')
class people(metaclass=Mymeta):
def __init__(self,name,age):
self.name=name
self.age=age
def say(self):
print('%s:%s' %(self.name,self.name))
# 无需实例化,定义类People的时候就会运行类体代码,自动执行Mymeta里的init,识别到类名是小写,则抛出异常
调用类的时候自动造空对象的方法
只要是调用类,都会依次调用
- new
- init
def __new__(cls,*args,**kwargs):
# 自定义的逻辑,最终得到一个对象
return obj
此时,自定义了new方法覆盖了原来的new方法,无法返回新对象了,所以应该加上父类的new方法,得到一个对象
执行new方法得到的对象,再转交给init
- super().new
- type().new
super().__new__(cls,*args,**kwargs)
# 类一定要传,否则不知道是哪个类造的对象
实际上在Mymeta底层,有一个方法帮我们先调new,再调init,再返回初始化好的对象:__call__
call方法
内置方法 __call__
,在对象后面加括号调用,自动运行call的代码,返回值就是call方法的返回值
如果想把一个对象造成可以调用的,在对象的类里面添加 __call__
方法
类可以加括号调用,是因为元类中有call方法,用于实例化产生对象
总结:
-
调用对象,运行的是对象所属类的call方法
-
调用自定义类,运行的是自定类元类的的call方法
定制元类:可以自订生成对象和调用的方法
属性查找
父类不是元类,找方法只会去父类找,不会找到元类层
类也是对象,通过点找类的属性,找不到就去父类找,在普通类这一层。如果这一层找不到则跳去上一层,去元类层找,先找自订的Mymeta,再找type
神游整理
元类概念
对象由类实例化得到,python中一切皆对象,因此类也是由一个类得到的,这个生成类的类,叫做元类。再往下,元类的生成方法不必再深究了
对一个对象使用type方法,可以看到对象的类名,对一个类使用type方法,可以看到元类名,默认是“type”
print(type(StanfordTeacher))print(type(Mymeta))
print(type(People))
# <class 'type'>
# <class '__main__.Mymeta'>
# 看到People的元类和Mymeta的元类
由此可以知道,我们在创建类的时候,由class关键字,调用了元类,产生了一个自定义的类,类似于 People = type(...)
创建类的流程分析
class 关键字在帮我们创建类的时候,调用了元类 type(...)
并将一些参数传入,得到具有各种属性值和方法的类。
研究一下定义类的过程,看看创建一个类需要哪些参数
class People(object,):
country = 'China'
def __init__(self,name,age):
self.name = name
self.age = age
def show_info(self):
print(self.name)
print(self.age)
# 可以看到,一个类必然有三个组成部分:类名,父类,类体代码。类体代码在没有被执行的时候只是一些字符串
因此调用type会传入同样的三个参数
- 类名 class_name = People
- 父类 class_base = (object,) 元组的形式,可以有很多个父类
- 类的名称空间 class_dic 一个字典,在运行类体代码之后存放产生的名字
由此,产生类的过程可以分为四个步骤
-
拿到类名 class_name = People
-
拿到父类名 class_base = object
-
执行类体代码:在定义类的时候就会执行,一开始就学过
执行类体代码产生一系列名字,放进名称空间 class_dic
-
上面三个参数传入元类,得到一个类People
type('People',(object,),class_dic)
所以到这里,我们可以把class帮我们做的事情展开,详细是这样的
-
exec是一个内置函数,可以帮助我们模拟class的运行机制
exec会把第第二个位置参数上的字典当作全局作用域,第三个位置参数上的字典当作局部作用域,以此来找值
# 1、类名
class_name="People"
# 2、类的基类
class_bases=(object,)
# 3、执行类体代码拿到类的名称空间
class_dic={}
class_body="""
def __init__(self,name,age):
self.name=name
self.age=age
def say(self):
print('%s:%s' %(self.name,self.name))
"""
exec(class_body,{},class_dic)
# print(class_dic)
# 4、调用元类
People=type(class_name,class_bases,class_dic)
obj = People('deimos',22)
自定义元类
调用元类产生类的前三个步骤都没什么可操作性。值得研究的是第四个步骤。如果不指定元类,会使用python默认的元类 type
简单省事,但是对于复杂的需求,元类也可以被自定义
指定元类的方法
-
创建一个自定义元类,自定义元类必须继承 type,因为需要用到type里面基层的方法
class Mymeta(type): pass
-
创建自定义类,在括号里指定使用的元类 metaclass = Mymeta
class People(object,metaclass = Mymeta): def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s:%s' %(self.name,self.name))
这样,我们就完成了类的元类的指定。再生成新类,就相当于
People=Mymeta(class_name,class_bases,class_dic)
类实例化产生对象需要类中的 init 方法,同样地,元类产生类也需要 init 方法,在Mymeta元类的 init 里面加上逻辑和数据,就可以完成比起使用元类来创建类额外多的功能
# 在init里加入逻辑判断,如果类名为小写开头,则抛出异常
class Mymeta(type):
def __init__(self,class_name,class_bases,class_dic):
# 使用super来重用父类type的init
super().__init__(class_name, class_bases, class_dic)
if class_name.islower():
# 判断类名是否为纯小写
raise TypeError('类名%s请修改为驼峰体' %class_name)
if '__doc__' not in class_dic or len(class_dic['__doc__'].strip('
')) == 0:
# 判断是否有文档注释
raise TypeError('类中必须有文档注释,并且文档注释不能为空')
class People(object,metaclass=Mymeta):
"""
文档注释
"""
school='Stanford'
def __init__(self,name,age):
self.name=name
self.age=age
def say(self):
print(self.name,self.age)
raise 自定义的错误名('提示错误信息')
# 使用raise可以自定义抛出异常
# 由于类体代码会在定义的时候就被执行,所以不需要实例化,只要包含了类的代码运行了,就会检查People类是不是驼峰体,有没有文档注释
调用的原理
为什么可以被调用
__call__
是一个内置方法,会在对象后面被加括号调用的时候自动执行。能被执行的前提是对象中有 call 方法
class Foo:
def __call__(self, *args, **kwargs):
print(self)
print(args)
print(kwargs)
obj=Foo()
obj(11,22,33)
# <__main__.Foo object at 0x000001F9A3B91280>
# (11, 22, 33)
# {}
类,函数可以被调用,是因为他们有call方法。默认,类的call方法就是生成一个对象,在类里面可以改写这个call,实现调用类的时候的别的功能
call 的步骤
对应我们之前学的对象的产生过程,类的call方法有下面3步
1. 产生一个空对象obj
2. 调用类的__init__方法初始化obj
3. 返回初始化好的obj
同样在元类里也有 __call__
方法,一样有以上三步,不过返回的是类,而不是对象。在自定义的元类Mymeta中可以改写call方法实现不同的需求
与调用类相似,调用元类生成类的三个步骤,并且用到了元类中的其他内置方法:new,init
1. 调用__new__产生一个空对象obj
2. 调用__init__初始化空对象obj
3. 返回初始化好的obj:return obj
我们把__call__ 展开,写到自定义的元类里,长这个样:
class Mymeta(type):
def __call__(self, *args, **kwargs):
# 1. 调用new,得到一个空对象obj
obj=self.__new__(self)
# 2. 对得到的obj执行init,初始化
self.__init__(obj,*args,**kwargs)
# 3. 返回初始化好的对象
# 这也是在普通类中内置方法call的流程,只不过在类中返回的是对象,在元类中返回的是类
return obj
def __init__(self):
pass
def __new__(self):
pass
上面这个代码块什么都没做,只是按照流程来走了一遍,所以可以往里面加额外的逻辑,在生成类的时候实现额外的功能
例 操作产生的类对象的dict,把所有属性变成私有
class Mymeta(type):
def __call__(self, *args, **kwargs):
# 这里self是People类,包含People类的类名,本身自带的属性等等
obj = self.__new__(self)
self.__init__(obj, *args, **kwargs)
# 对初始化得到的对象字典进行操纵
obj.__dict__ = {'_%s__%s' % (self.__name__, k): v for k, v in obj.__dict__.items()}
return obj
class People(object,metaclass=Mymeta):
school='Stanford'
def __init__(self,name,age):
self.name=name
self.age=age
def say(self):
print(self.name,self.age)
t1 = People('lili', 18)
print(t1.__dict__)
# <class '__main__.People'>
# {'_People__name': 'lili', '_People__age': 18}
# 属性全部变成隐藏的了
属性查找问题
对象与类之间的属性查找是先从自己开始找,接着找自己的类,再找父类,最后找object
父类不是元类,从对象出发找属性不会找到元类去
如果直接 类名.属性名
也可以直接访问类里面的数据或方法,这个时候可以跳出类层:当前类找不到,去父类找,都找不到,去到元类层找Mymeta,最后找不到,去元类type里找