描述符
描述符介绍与疑惑
描述符本质就是一个新式类,在这个新式类中,至少实现了
__get__
,__set__
,__delete__
中的一个,这也被称为描述符协议。
__get__
:调用一个属性时,触发
__set__
:为一个属性赋值时,触发
__delete__
:采用del
删除属性时,触发
描述符的作用是用来代理另外一个类的属性的(必须把描述符定义成这个类的类属性,不能定义到构造函数中)
换而言之,描述符类应该与其他类做搭配使用而非单独使用。
# ==== 描述符的定义 ==== class Foo(object): def __get__(self, instance, owner): pass def __set__(self, instance, value): pass def __delete__(self, instance): pass
# ==== 描述符类的错误使用 ==== class Foo(object): def __get__(self, instance, owner): print('触发get') def __set__(self, instance, value): print('触发set') def __delete__(self, instance): print('触发delete') # 包含这三个方法的新式类称为描述符,由这个类产生的实例进行属性的调用/赋值/删除,并不会触发这三个方法 f1 = Foo() f1.name = 'egon' print(f1.name) del f1.name # ==== 执行结果 ==== """ egon """
# ==== 描述符类的正确使用以及疑问点 ==== class Foo(object): """描述符类""" def __get__(self, instance, owner): print("执行了__get__") def __set__(self, instance, value): print("执行了__set__") def __delete__(self, instance): print("执行了__delete__") class MyClass(object): name = Foo() # 类属性 name 此时就是一个描述符 Foo 的实例对象... def __init__(self, name): self.name = name def __getattr__(self, item): print("没找到") m1 = MyClass("小花") print(m1.name) del m1.name # === 疑问点 === 为何实例对象 m1 的 __dict__ 字典中没有 name ????? print(m1.__dict__) print(MyClass.__dict__) # === 执行结果 === """ 执行了__set__ 执行了__get__ None 执行了__delete__ {} {'__module__': '__main__', 'name': <__main__.Foo object at 0x000002B8A6193E80>, '__init__': <function MyClass.__init__ at 0x000002B8AE5F7790>, '__getattr__': <function MyClass.__getattr__ at 0x000002B8AE5F7820>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None} """
注意上面的疑问点,记在心里继续向下看。
数据描述符与非数据描述符
数据描述符:至少实现了
__get__
和__set__
非数据描述符:没有实现
__set__
那么这里为什么区分出数据描述符与非数据描述符呢? 因为他们对于属性查找的影响及其重要!
# ==== 数据描述符的定义 ==== class Foo(object): def __set__(self, instance, value): print('set') def __get__(self, instance, owner): print('get')
# ==== 非数据描述符的定义 ==== class Foo(object): def __get__(self, instance, owner): print('get')
数据描述符的set与类的setattr属性设置优先级问题
解答疑问:
注意!
__setattr__
与 描述符类__set__
的设置优先及其解释如下:
当
self.name = name
的时候,会先检查MyClass
是否自定义了__setattr__
方法。如果没有,则检测类属性name
是否是描述符对象。类属性
name
是描述符类Foo
的实例对象,并且还是一个数据描述符对象,故此执行Foo
类 下的__set__
方法,因为我们在Foo
的__set__
中没有做任何设置,故实例对象m1
的__dict__
中没有任何值。
以下两段代码将验证猜想。
# ==== 验证猜想:MyClass 具有 __setattr__ 是否会执行 描述符对象中的 __set__ ==== class Foo(object): """数据描述符类""" def __get__(self, instance, owner): print("执行了__get__") def __set__(self, instance, value): print("执行了__set__") def __delete__(self, instance): print("执行了__delete__") class MyClass(object): name = Foo() # 类属性 name 此时就是一个描述符 Foo 的实例对象... def __init__(self, name): self.name = name def __getattr__(self, item): print("没找到...") def __setattr__(self, key, value): # 当有 print("执行了__setattr__") self.__dict__[key] = value m1 = MyClass("小花") # === 疑问点 === print(m1.__dict__) # ==== 执行方法 ==== """ 执行了__setattr__ {'name': '小花'} """
# ==== 验证猜想:MyClass中没有 __setattr__ 是否执行描述符对象中的 __set__ ==== class Foo(object): """描述符类""" def __get__(self, instance, owner): print("执行了__get__") def __set__(self, instance, value): print("执行了__set__") def __delete__(self, instance): print("执行了__delete__") class MyClass(object): name = Foo() # 类属性 name 此时就是一个描述符 Foo 的实例对象... def __init__(self, name): self.name = name def __getattr__(self, item): print("没找到") m1 = MyClass("小花") # === 疑问点 === print(m1.__dict__) # ==== 执行方法 ==== """ 执行了__set__ {} """
描述符注意事项与属性查找顺序
一 描述符本身应该定义成新式类,被代理的类也应该是新式类
二 必须把描述符定义成这个类的类属性,不能为定义到构造函数中
三 要严格遵循该优先级,优先级由高到底分别是:
1.数据描述符
2.实例属性
3.类属性 (与4同级)
4.非数据描述符 (与3同级)
5.找不到的属性触发
__getattr__
(如果存在的话,不存在抛出AttributeError
异常)
对于实例对象获取某一属性/方法时,也将严格按照顺序进行查找。
首先所有关于属性及方法的查找都必须通过 __getattribute__
进行执行,在 __getattribute__
中的查找顺序如下图:
# ==== 属性查找:数据描述符类 > 实例对象 ==== class Foo(object): """数据描述符类""" def __init__(self,key): self.key = key # self --> Foo的实例对象即 name, key 即传入的参数 "name" def __get__(self, instance, owner): """ self: --> Foo的实例对象,即name instance --> MyClass的实例对象,即m1 owner --> 即MyClass类 """ print("执行了__get__") return instance.__dict__[self.key] def __set__(self, instance, value): """ value --> 设置的值,即 MyClass在实例化时传入的 "小花" """ print("执行了__set__") instance.__dict__[self.key] = value # 注意:我们将 key 放在了 instance 的 __dict__ 中。即 m1的 __dict__ 中。当然也可以放在 self的__dict__中 class MyClass(object): name = Foo("name") # 类属性 name 此时就是一个描述符 Foo 的实例对象... def __init__(self,name): self.name = name # 设置时候会检测,name是一个数据描述符对象,并且MyClass类中没有设置 __setattr__ ,故调用其 Foo.__set__ 方法 def __getattr__(self, item): print("没找到...") m1 = MyClass("小花") print(m1.name) # ==== 执行结果 ==== Ps:可以看见,数据描述符的属性获取是会使用 Foo描述符类中的 __get__ """ 执行了__set__ 执行了__get__ 小花 """
# ==== 属性查找:实例对象 > 类 ==== class MyClass(object): name = "小黄" def __init__(self,name): self.name = name def __getattr__(self, item): print("没找到...") m1 = MyClass("小花") print(m1.name) # ==== 执行结果 ==== """ 小花 """
# ==== 属性查找:实例对象 > 非数据描述符 ==== class Foo(object): """非数据描述符类""" def __init__(self,key): self.key = key # self --> Foo的实例对象即 name, key 即传入的参数 "name" def __get__(self, instance, owner): """ self: --> Foo的实例对象,即name instance --> MyClass的实例对象,即m1 owner --> 即MyClass类 """ print("执行了__get__") return instance.__dict__[self.key] class MyClass(object): name = Foo("name") # 类属性 name 此时就是一个描述符 Foo 的实例对象... def __init__(self,name): self.name = name # 此时会调用 object 中的 __setattr__ 进行设置 def __getattr__(self, item): print("没找到...") m1 = MyClass("小花") print(m1.name) # ==== 执行结果 ==== Ps:可以看见,非数据描述符的属性获取是不会使用 Foo描述符类中的 __get__。故实例属性 > 非数据描述符 """ 小花 """
描述符类的简单应用
众所周知,python是弱类型语言,即参数的赋值没有类型限制,下面我们通过描述符机制来实现类型限制功能
在Django的model中,其实不难发现它的本质也是通过描述符类对参数进行限制。
注意点:如果对传入值进行参数设置,其被描述符代理属性的类千万不要设置
__setattr__
。
# ==== 描述符类的简单应用 ==== class Foo(object): """数据描述符类""" def __init__(self, key, expected_type): self.key = key # self --> Foo的实例对象即 name, key 即传入的参数 "name" self.expected_type = expected_type # 即传入的期望类型 def __get__(self, instance, owner): print("__get__") if instance is None: return self return instance.__dict__[self.key] def __set__(self, instance, value): print("__set__") if not isinstance(value, self.expected_type): # 如果不是期望的类型,则抛出异常 raise TypeError("你所传入的类型不正确!必须是%s" % self.expected_type) instance.__dict__[self.key] = value def __delete__(self, instance): print("__del__") del instance.__dict__[self.key] class Student(object): name = Foo("name", str) # 类属性 name 此时就是一个描述符 Foo 的实例对象... age = Foo("age",int) def __init__(self, name,age): self.name = name # 依照属性查找顺序。检测name是一个数据描述符对象,并且Student类本身没有设置 __setattr__ 。则调用 Foo 下的 __set__ 进行设置。 self.age = age m1 = Student("小花","1fdasfdsa8") print(m1.name) print(m1.age) # ==== 执行结果 ==== """ __set__ __set__ Traceback (most recent call last): File "C:/Users/Administrator/PycharmProjects/learn/描述符.py", line 41, in <module> m1 = Student("小花","1fdasfdsa8") File "C:/Users/Administrator/PycharmProjects/learn/描述符.py", line 36, in __init__ self.age = age File "C:/Users/Administrator/PycharmProjects/learn/描述符.py", line 21, in __set__ raise TypeError("你所传入的类型不正确!必须是%s" % self.expected_type) TypeError: 你所传入的类型不正确!必须是<class 'int'> """
# ==== 描述符类的应用错误示范!不要设置 __setattr__ ==== class Foo(object): """数据描述符类""" def __init__(self, key, expected_type): self.key = key # self --> Foo的实例对象即 name, key 即传入的参数 "name" self.expected_type = expected_type # 即传入的期望类型 def __get__(self, instance, owner): print("__get__") if instance is None: return self return instance.__dict__[self.key] def __set__(self, instance, value): print("__set__") if not isinstance(value, self.expected_type): # 如果不是期望的类型,则抛出异常 raise TypeError("你所传入的类型不正确!必须是%s" % self.expected_type) instance.__dict__[self.key] = value def __delete__(self, instance): print("__del__") del instance.__dict__[self.key] class Student(object): name = Foo("name", str) # 类属性 name 此时就是一个描述符 Foo 的实例对象... age = Foo("age",int) def __init__(self, name,age): self.name = name # 虽然 name 是一个数据描述符对象,但是由于Student本身具有类方法 __setattr__ 。故不会执行其 Foo.__set__ 进行类型限制 self.age = age def __setattr__(self, key, value): print("__setattr__") self.__dict__[key] = value m1 = Student("小花","1fdasfdsa8") print(m1.name) print(m1.age) # ==== 执行结果 ==== """ __setattr__ __setattr__ __get__ 小花 __get__ 1fdasfdsa8 """
类的装饰器
装饰器可以给类装饰
装饰器的本质就是将
@xxx
下面的代码块当做参数传入装饰器函数中。因此我们也可以给类做一个装饰器。
# ==== 类的无参装饰器 ==== def outer(cls): cls.xxx = "随便设的值" # 添加了一个类属性 return cls @outer # --> outer(Foo) --> 调用Foo 实际上就是在调用 cls class Foo(object): def __init__(self,name,age,gender): self.name = name self.age = age self.gender = gender f1 = Foo("Yunya",18,"male") print(f1.xxx) # ==== 执行结果 ==== """ 随便设的值 """
# ==== 类的有参装饰器 ==== def outer(**kwargs): def inner(cls): print("传入的参数-->",kwargs) return cls return inner @outer(name=str,age=int,gender=str) # 执行outer完成后相当于 @inner,然后再把Foo传入cls,最后返回cls class Foo(object): def __init__(self,name,age,gender): self.name = name self.age = age self.gender = gender f1 = Foo("Yunya",18,"male") # ==== 执行结果 ==== """ 传入的参数--> {'name': <class 'str'>, 'age': <class 'int'>, 'gender': <class 'str'>} """
类也可以作为装饰器
一定要理解!装饰器的本质只是将
@xxx
下面的代码块的内存地址传入形参!所以它并不在乎到底是函数还是类!,实际上@property
等等都是基于类的装饰器来完成的。
# ==== 类也可以作为装饰器 ==== class Bar(object): def __init__(self,obj): self.obj = obj("Yunya",18,"male") print(self.obj.__dict__) @Bar # --> 将Foo传入到Bar.__init__的形参obj中去了 class Foo(object): def __init__(self,name,age,gender): self.name = name self.age = age self.gender = gender # ==== 执行结果 ==== """ {'name': 'Yunya', 'age': 18, 'gender': 'male'} """
描述符与类装饰器的综合应用
属性类型限制
我们在学习了类的装饰器后,已经可以将其与描述符类相结合。做出能够大批量的去限制属性传入值的类型的功能
# ==== 单纯通过类的装饰器类做类属性的动态增加 ==== def accept_parameters(**kwargs): """接收参数""" def add_attr(cls): """添加类属性""" for key,value in kwargs.items(): setattr(cls,key,value) return cls return add_attr @accept_parameters(name="Yunya",age="18",gender="male") # 相当于头上挂了一个 @add_attr 然后自动将 Student类传入,再动态设置类属性。 class Student(object): pass if __name__ == '__main__': print(Student.__dict__) # ==== 执行结果 ==== """ {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None, 'name': 'Yunya', 'age': '18', 'gender': 'male'} """
有了这样的框架就可以对已经定义好的类动态添加一下类属性或者类方法 ...
关于使用setattr这一点其实本来是想用 obj.__dict__.update(kwargs)
来做的但是不知何原因并不支持。 (会引发一个异常AttributeError: 'mappingproxy' object has no attribute 'updete'
)
# ==== 类的装饰器与描述符的终极玩法:动态限制属性传入类型 ==== """ 鉴于以下代码对初学者来说理解比较困难,故在此做一下流程分析。 1: @accept_parameters 开始执行,传入参数 name=str,age=int,gender=str 给 accept_parameters的kwargs。并返回 返回值,add_attr.此时class Student头上相当于挂了一个@add_attr,将类Student自动传入继续执行。add_attr函数体内代码。 2: 执行add_attr函数体代码,开始设置类属性。for 循环拿到传入的参数,在setattr(cls,attr_name,TypeRestrictions(attr_name,expected_type)) 发现要实例化描述符类TypeRestrictions,于是执行其实例化方法。这个时候Studetn中的name,age,gender都被描述符类所代理了。 3: 实例化Student的时候,实际上类中的描述符全部设置好了,此时检测Student是否有 __setattr__ 方法,如果没有执行描述符类中的 __set__ 方法进行逻辑判断。 """ def accept_parameters(**kwargs): """接收参数""" def add_attr(cls): """新增类属性""" for attr_name,expected_type in kwargs.items(): setattr(cls,attr_name,TypeRestrictions(attr_name,expected_type)) # 类Student添加描述符属性。 return cls return add_attr class TypeRestrictions(object): """类型限制类 数据描述符""" def __init__(self,attr_name,expected_type): self.attr_name = attr_name self.expected_type = expected_type def __get__(self, instance, owner): """ self: --> TypeRestrictions的实例对象 instance --> Student的实例对象,即s1 owner --> 即Student类 """ return instance.__dict__[self.attr_name] def __set__(self, instance, value): """ self: --> TypeRestrictions的实例对象 instance --> Student的实例对象,即s1 value --> 设置的值。设置的值,即 Student 在实例化时传入的值 """ if isinstance(value,self.expected_type): instance.__dict__[self.attr_name] = value else: raise TypeError("类型错误!{0}的类型必须是{1},而你给的类型是{2}".format(self.attr_name,self.expected_type,type(value))) def __delete__(self, instance): """ self: --> TypeRestrictions的实例对象 """ del instance.__dict__[self.attr_name] @accept_parameters(name=str,age=int,gender=str) class Student(object): def __init__(self,name,age,gender): self.name = name self.age = age self.gender = gender if __name__ == '__main__': s1 = Student("Yunya","18","male") # <--- 传入的是age是str类型,这将引发异常。 # ==== 执行结果 ==== """ Traceback (most recent call last): File "C:/Users/Administrator/PycharmProjects/learn/反射与自省.py", line 37, in <module> s1 = Student("Yunya","18","male") # <--- 传入的是age是str类型,这将引发异常。 File "C:/Users/Administrator/PycharmProjects/learn/反射与自省.py", line 32, in __init__ self.age = age File "C:/Users/Administrator/PycharmProjects/learn/反射与自省.py", line 22, in __set__ raise TypeError("类型错误!{0}的类型必须是{1},而你给的类型是{2}".format(self.attr_name,self.expected_type,type(value))) TypeError: 类型错误!age的类型必须是<class 'int'>,而你给的类型是<class 'str'> """
class Lazyproperty: def __init__(self,func): self.func=func def __get__(self, instance, owner): print('这是我们自己定制的静态属性,r1.area实际是要执行r1.area()') if instance is None: return self else: value=self.func(instance) instance.__dict__[self.func.__name__]=value return value # return self.func(instance) #此时你应该明白,到底是谁在为你做自动传递self的事情 def __set__(self, instance, value): print('hahahahahah') class Room: def __init__(self,name,width,length): self.name=name self.width=width self.length=length @Lazyproperty #area=Lazyproperty(area) 相当于定义了一个类属性,即描述符 def area(self): return self.width * self.length print(Room.__dict__) r1=Room('alex',1,1) print(r1.area) print(r1.area) print(r1.area) print(r1.area) #缓存功能失效,每次都去找描述符了,为何,因为描述符实现了set方法,它由非数据描述符变成了数据描述符,数据描述符比实例属性有更高的优先级,因而所有的属性操作都去找描述符了
摘抄自笔记:
描述符异常强大,它除开可以使用property做静态属性外,还能够去限制类或者实例属性的一些条件。但是这些还远远体现不出描述符的强大之处,更加厉害的是使用描述符我们可以自己来做 @classmethod ,@staticmethod,或者 __slots__ 等等,底层的大多数功能都可以通过描述符来进行还原。
当message被加上了@property后,实际上是通过组合的形式对property类创建了一个名叫message的实例对象,并且只存在于类的属性字典中。
接下来可以通过创建一个装饰器的类来实现这样的操作。
最后来梳理一下运行逻辑,当被@顶到头顶上的时候那么在程序被运行的一瞬间,自动的运行了灰色部注释的代码,将整个message变成了一个被修饰的类变量,那么当Person实例对象调用这个类变量的时候会触发 __get__ 方法。其中的self是My_property的实例对象也就是message,而instance就是我们的Person的实例对象p1,使用了 __get__ 后被隐式的运行了一遍 message 函数,由于传入了instance参数所以并不会抛出self的错误。那么最后message函数的返回值被res获取到再将res返回至调用处print(p1.message)处,看起来就像是一个类变量一样的方法。类并不能够去运行这一个方法,因为当类调用该被描述的属性时,instance就会变成一个None..因此会少一个参数。
由于是None,就会抛出异常,那么紧接着上部分做一做改进。就好了 ...
进行到此处,请高呼NICE!!!property被完美的重做了。实际底层就是这样做的,虽然这在实际工作中没有任何用处... 但你应该知道百分之九十的Python开发者不明白这些东西。
但是这样做并不完美,如果这个求值的过程非常的复杂,我们每一次调用都要去在在描述符实例中的类里面的 __get__ 方法中进行一次计算,那么是非常耗费资源的。如果说,想要去计算一次从今以后都不再计算,则可以将值添加到实例字典中,但是一定要注意,这个描述符必须是非数据描述符!因为实例字典的查找顺序优先级在非数据描述符之上在数据描述符之下!!!
自定义staticmethod
class StaticMethod: def __init__(self,func): self.func=func def __get__(self, instance, owner): #类来调用,instance为None,owner为类本身,实例来调用,instance为实例,owner为类本身, def feedback(*args,**kwargs): print('在这里可以加功能啊...') return self.func(*args,**kwargs) return feedback class People: @StaticMethod# say_hi=StaticMethod(say_hi) def say_hi(x,y,z): print('------>',x,y,z) People.say_hi(1,2,3) p1=People() p1.say_hi(4,5,6)
自定义classmethod
class ClassMethod: def __init__(self,func): self.func=func def __get__(self, instance, owner): #类来调用,instance为None,owner为类本身,实例来调用,instance为实例,owner为类本身, def feedback(): print('在这里可以加功能啊...') return self.func(owner) return feedback class People: name='linhaifeng' @ClassMethod # say_hi=ClassMethod(say_hi) def say_hi(cls): print('你好啊,帅哥 %s' %cls.name) People.say_hi() p1=People() p1.say_hi() #疑问,类方法如果有参数呢,好说,好说 class ClassMethod: def __init__(self,func): self.func=func def __get__(self, instance, owner): #类来调用,instance为None,owner为类本身,实例来调用,instance为实例,owner为类本身, def feedback(*args,**kwargs): print('在这里可以加功能啊...') return self.func(owner,*args,**kwargs) return feedback class People: name='linhaifeng' @ClassMethod # say_hi=ClassMethod(say_hi) def say_hi(cls,msg): print('你好啊,帅哥 %s %s' %(cls.name,msg)) People.say_hi('你是那偷心的贼') p1=People() p1.say_hi('你是那偷心的贼')