描述符
将一个类属性指向一个类的调用,就是一个描述符.之后这个类属性的各项行为,将由这个类来代理.
描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),set(),delete()中的一个,这也被称为描述符协议.(正常使用的类是无须定义这三个方法的,是采取之前的attr和items系列方法来控制行为)
get():调用一个属性时,触发
set():为一个属性赋值时,触发
delete():采用del删除属性时,触发
描述符分两种:
一 数据描述符:至少实现了__get__()和__set__()
二 非数据描述符:没有实现__set__()
描述符的基本操作
# 建立一个满足描述符协议的类
class Foo: #在Python3中Foo是新式类,它实现了三种方法,这个类就被称作一个描述符
def __get__(self, instance, owner):
print('get方法被触发')
def __set__(self, instance, value):
print('set方法被触发')
def __delete__(self, instance):
print('delete方法被触发')
如何使用描述符,只需要将一个类属性指向描述符即可.
class Bar:
x = Foo()
def __init__(self, name):
self.name = name
bar = Bar('test')
print(bar.x) # get方法被触发
bar.x = 4 # set方法被触发
print(bar.__dict__)
del bar.x # delete方法被触发
描述符的优先级
如果运行如下代码:
Bar.x = 30 # 没有触发描述符的动作
这是因为描述符有优先级,数据描述符的优先级小于类属性,所以直接设置类属性的时候,会覆盖原来的类属性,将x设置为常量30.可以理解为描述符是一个伪装的类属性,无法覆盖类属性的直接赋值,但在描述符存在的状态下,对描述符代理的类属性进行取值和删除是可以触发描述符动作的.
描述符的优先级如下:
类属性大于数据描述符 | 给类的属性直接赋值的时候,会覆盖数据描述符 |
数据描述符>实例属性 | 由于数据描述符是类属性,实例的属性没有的时候会去找类属性,因此数据描述符的属性要比实例属性高,即实例里边引用描述符,都会触发描述符操作/td> |
实例属性>非数据描述符 | 其实函数就是一个非数据描述符对象,字符串也是,实例里如果有同名的属性,肯当会优先选择实例的,不管是赋值还是调用 |
非数据描述符>找不到(触发__getattr__) | 如果所有描述符都找不到,实际上就是找不到该属性,会报错或者触发__getattr__ |
描述符的应用
描述符的标准应用
# 基本应用,解释如何使用描述符:
class Check:
def __init__(self, key):
self.key = key
def __get__(self, instance, owner):
print('__get__ is triggered: instance--->{} owner--->{}'.format(instance, owner))
return instance.__dict__[self.key]
def __set__(self, instance, value):
print('set is triggered: instance--->{} owner--->{}'.format(instance, value))
instance.__dict__[self.key] = value
def __delete__(self, instance):
print('__delete__ is triggered: instance --> {}'.format(instance))
instance.__dict__.pop(self.key)
class People:
name = Check('name')
def __init__(self, name, age, salary):
self.name = name # 触发代理,不会把name加入到对象的__dict__中
self.age = age
self.salary = salary
def show(self):
print('My name is {}, my age is {}'.format(self.name, self.age))
p1 = People('cony', 4, 999999)
编写描述符的__set__方法并且使用的思路如下:
编写__set__函数:
顺序 | 操作内容 |
---|---|
1 | 建立People类,定义了类属性name由class类进行代理,init定义了三个初始属性. |
2 | p1实例化传入三个参数,此时self.name 指向的是代理,由于数据类型代理优先于实例属性,因此p1的属性字典不包含name,而是把p1这个对象和name的值转交代理.此时查看p1的属性字典,会发现没有name键 |
3 | 是赋值操作,描述符类触发set方法;instance指的就是传入的对象p1,而value就是赋的值'cony'.拿到值之后,要去操作对象的属性字典(这里一定不能直接写成instance.name = value,因为数据描述符优先于实例属性,又会触发描述符导致无限循环),写入同名name和对应的值value |
4 | 此时查看属性字典,发现三个项目都完备,新增了name键,值就是'cony' |
设置好__get__属性之后的取值流程:
顺序 | 操作内容 |
---|---|
1 | p1.name去调用p1的name属性,虽然此时p1的属性字典里有name键,但优先级低于数据描述符,所以依然被描述符代理 |
2 | 描述符的get参数从p1属性字典内取到name对应的值,返回 |
3 | p1.name返回属性字典内的值 |
4 | 结果看起来和直接p1.name相似,但内部却经过了描述符代理 |
描述符的应用
描述符的应用非常广泛,如果要自行开发框架或者复杂程序,描述符是一定会用到的,下边是描述符作为装饰器的几种应用,顺便回头用面向对象的知识解释一下原来的知识.
类的装饰器
由于装饰器的本质是返回和传入的对象一致,所以装饰器也可以对类来起作用.
所以可以定义一些装饰器来修饰类.
带参的装饰器,可以在类开始的时候做各种工作,比较典型的就是给类添加一系列参数,如果把一系列参数和描述符联系起来,一个限定类的描述符可以这样来写:
# 不加装饰器之前的代码,通过描述符限定了三个属性
class Typed:
def __init__(self, name, target_type):
self.key_name = name
self.t_type = target_type
def __get__(self, instance, owner):
return instance.__dict__[self.key_name]
def __set__(self, instance, value):
if isinstance(value, self.t_type):
instance.__dict__[self.key_name] = value
else:
raise AttributeError
def __delete__(self, instance):
return instance.__dict__.pop(self.key_name)
class People:
name = Typed('name', str)
age = Typed('age', int)
gender = Typed('gender', bool)
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
cony = People('cony', 32, True)
用一个带参装饰器一次性解决类型限定的问题:
class Typed:
def __init__(self, name, target_type):
self.key_name = name
self.t_type = target_type
def __get__(self, instance, owner):
return instance.__dict__[self.key_name]
def __set__(self, instance, value):
if isinstance(value, self.t_type):
instance.__dict__[self.key_name] = value
else:
raise AttributeError
def __delete__(self, instance):
return instance.__dict__.pop(self.key_name)
def type_check(**kwargs):
def wrapper(cls):
for x, y in kwargs.items():
setattr(cls, x, Typed(x, y))
return cls
return wrapper
@type_check(name=str, age=int, gender=bool)
class People:
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
cony = People('cony', 32, True)
可见只要按照被装饰对象的init方法里的参数给出类型限定,直接用一个装饰器就可以达成效果,而且适合于各种类.
描述符实现静态属性
现在有一个类如下:
class Room:
def __init__(self,width,length):
self.width= width
self.length = length
@property # 加上装饰器,实际上就是把area改成了一个属性
def area(self):
return self.width * self.length
来试着用描述符来玩一下静态属性:
装饰器这个时候注意,还可以是一个类:
class Myproperty:
def __init__(self,func):
print('Myproperty初始化运行')
self.func = func
def __get__(self, instance, owner): # 2 知道了加上装饰器,就相当于描述符,所以加上一个__get__方法就可以返回想要的东西了.
return self.func(instance) # 由于init执行之后area方法已经被self.func接收,所以只要传area的运行结果就可以了,这时候要把instance传进去.
class Room:
def __init__(self,name,width,length):
self.width= width
self.length = length
self.name = name
@Myproperty # 1 加上装饰器,就是area = Myproperty(area),结果发现,这就是属性名 = 类对象,所以就是一个描述符.
def area(self):
return self.width * self.length
room = Room('test',1,10)
print(room.area)
这样就自己制作了一个相当于python内置的@property装饰器.
可见装饰器的@,其实就是一个var = func(var),如果func是一个高阶函数,就是装饰器,如果func是一个类,那么就成了描述符.这就是一切皆对象的统一之处.
对象调用描述符OK了,但是用类调用描述符的时候出错,这是因为用类调用的时候,__get__方法的instance是None.最好还是用实例运行.
那么用系统内置的property来测试一下,发现类调用的时候返回的是property对象,所以在get方法里加上一个判断,如果instance == None的时候,就返回self
所以最终自行编写类似@property的Myproperty描述符如下:
class Myproperty:
def __init__(self, func):
print('Myproperty初始化运行')
self.func = func
def __get__(self, instance, owner):
if instance is None:
return self
else:
return self.func(instance) # 由于装饰过以后area方法已经被self.func接受,所以只要传area的运行结果就可以了,这时候要把instance传进去.
class Room:
def __init__(self, name, width, length):
self.width = width
self.length = length
self.name = name
@Myproperty # 加上装饰器,就是area = Myproperty(area),结果发现,这就是属性名 = 类对象,所以就是一个描述符.
def area(self):
return self.width * self.length
r1 = Room('test', 3.7, 4.1)
print(r1.area) # 显示15.17
print(Room.area) # 显示<__main__.Myproperty object at 0x000001B562686AC8>
@property 研究
已经知道了@property
其实就是一个描述符类.但是用@property装饰的静态属性是无法赋值的,如果要给静态属性赋值如何操作呢?
class Room:
def __init__(self, name, width, length):
self.name = name
self.width = width
self.length = length
@property
def area(self):
return self.width * self.length
r1 = Room('alex', 4.3, 3.6)
print(r1.area)
r1.area = 16 # 报错,AttributeError: can't set attribute
这个时候就要在@property修饰的基础上,用@被装饰属性名.getter 和 @被装饰属性名.deleter:
class Room:
def __init__(self, name, width, length):
self.name = name
self.width = width
self.length = length
@property
def area(self):
return self.width * self.length
@area.setter
def area(self,value):
print('setter is triggered')
@area.deleter
def area(self):
print('setter is triggered')
r1 = Room('alex', 4.3, 3.6)
print(r1.area)
r1.area = 16 # 触发了setter 下边修饰的同名函数
del r1.area # 触发了deleter下边的同名函数
当然,这里是操作触发函数,并没有真正的实现赋值,这只是让针对静态属性的赋值和删除成为可能,具体如何操作要编写后续代码.
注意这三段的顺序必须这么摆放,而且必须要有最前边的@property及正常的函数,否则后边的setter和deleter无法生效.
上下文管理协议
在操作文件对象的时候,为了免去每次关闭文件的动作,写成如下:
with open('test.txt','r+') as file:
由于文件句柄是一个对象,那么可以想到,支持with进行上下文管理的对象,起内部一定有支持上下文协议的方法.所以open类内一定有支持with上下文的方法.
__enter__和__exit__方法
__enter__是进入上下文的方法,当有with语句时,触发该方法,该方法的返回值会被赋给 as 之后的变量. __exit__是结束上下文的方法,在with内的语句块执行完毕的时候,会触发__exit__函数,结束上下文管理过程.
# 定义自己的Open类
class Open:
def __init__(self,name, open_type,encoding = 'utf-8'):
self.open_type = open_type
self.name = name
self.encoding = encoding
def __enter__(self):
print('__enter__ is triggered')
self.f = open(self.name,self.open_type,encoding=self.encoding)
return self.f
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close()
print('__exit__ is tirggered')
# 用with来使用该类
with Open('table.xml','r+') as file:
print(file.read())
这里要特别说的是__exit__方法.如果with之后的代码块内发生错误,会直接触发__exit__方法(相当于也退出了上下文).
如果__exit__的返回值设置为True,这个错误信息会被__exit__里的三个参数捕捉到;如果返回值不是True,这错误会报出来
class Open:
def __init__(self,name, open_type,encoding = 'utf-8'):
self.open_type = open_type
self.name = name
self.encoding = encoding
def __enter__(self):
print('__enter__ is triggered')
self.f = open(self.name,self.open_type,encoding=self.encoding)
return self.f
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close()
print(exc_type,exc_val,exc_tb,sep = '\n')
print('__exit__ is tirggered')
return True
with Open('table.xml','r+') as file:
file.read()
file.test # 添加一条引起错误的信息
异常不会抛出,被__exit__捕捉到,记录到三个默认参数里
# 执行结果
__enter__ is triggered
<class 'AttributeError'> # 异常类型,exc_type的值
'_io.TextIOWrapper' object has no attribute 'test' # 异常值 exc_cal的值
<traceback object at 0x000001FA8A1A6248> # 追溯信息 exc_tb
__exit__ is tirggered
with在后边的网络编程中经常会遇到,将一些每次都要做的内容写入__enter__和__exit__方法内,会方便很多.
当一个类定义了上下文协议方法,对对象也是可以使用with的:
class Open:
def __init__(self, name, open_type, encoding='utf-8'):
self.open_type = open_type
self.name = name
self.encoding = encoding
def __enter__(self, name, open_type, encoding='utf-8'):
print('__enter__ is triggered')
self.f = open(self.name, self.open_type, encoding=self.encoding)
return self.f
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close()
print(exc_type, exc_val, exc_tb, sep='\n')
print('__exit__ is tirggered')
return True
fa = Open('table.xml', 'r+')
with fa as f:
print(f.read())
当然这也写不是太好,还是将with把类的初始化和类的__enter__方法在一起使用比较方便.