描述符
将一个类属性指向一个类的调用,就是一个描述符.之后这个类属性的各项行为,将由这个类来代理.
描述符本质就是一个新式类,在这个新式类中,至少实现了__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__方法,结果发现,即使p1的字典内已经有了’name’:’cony’键值对,但是不管是外部p1.name还是内部的show方法取到的还是None,这是因为实例属性优先级低于数据描述符,如果不通过字典的形式,则去取p1.name实际上是触发了描述符的__get__函数,因为没有编写内容,自然显示为None,搞清楚这一步,就知道编写__get__方法与__set__方法类似,都要操作instance的字典,所以编写返回值,应该是p1的字典里由刚刚set方法写入的值.
设置好__get__属性之后的取值流程:
顺序 | 操作内容 |
---|---|
1 | p1.name去调用p1的name属性,虽然此时p1的属性字典里有name键,但优先级低于数据描述符,所以依然被描述符代理 |
2 | 描述符的get参数从p1属性字典内取到name对应的值,返回 |
3 | p1.name返回属性字典内的值 |
4 | 结果看起来和直接p1.name相似,但内部却经过了描述符代理 |
__delete__方法也与之前的方法类似,由于del p1.name依然会由描述符代理触发__delete__方法,因此将name从p1的属性字典里去除即可.
最后发现键如果写死,每次就修改同样的字典键,如果可以将其作为一个参数传入,就很方便的可以修改对象的属性名称,所以最后用了初始化函数增加了一个self.key.
从外部看起来,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# 异常类型,exc_type的值 '_io.TextIOWrapper' object has no attribute 'test' # 异常值 exc_cal的值 # 追溯信息 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__方法在一起使用比较方便.