分享

Python学习——面向对象高级之描述符

 看见就非常 2020-10-30

在写上一篇文章的时候遇到了描述符,本来以为很简单,看了一些别人写的博客,结果发现远不如我想的那么简单,一大堆概念向我砸过来,一时间难以接受,不甚理解,需要反反复复的斟酌,才能大致明白其用意与用法。所以决定把面向对象描述符部分单独拿出来写一篇文章,但愿写出来之后,过几天我自己还能看的明白。

什么是描述符

官方说法:python描述符是一个“绑定行为”的对象属性,在描述符协议中,它可以通过方法重写属性的访问。这些方法有 __get__(), __set__(), 和__delete__()。如果这些方法中的任何一个被定义在一个对象中,那么这个对象就是一个描述符。
说啥呢,描述符是一个对象???我觉着吧,描述符应该是一个类,由这个类实例化的对象成为描述符对象
这么说来,描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),__set__(),__delete__()中的一个,这三者也被称为描述符协议。

  • 数据描述符:至少实现了 __get__()__set__()
  • 非数据描述符:没有实现 __set__()

这两者的区别是在访问属性时的搜索顺序上:
搜索链(或者优先链)的顺序:数据描述符>实体属性(存储在实体的dict中)>非数据描述符。解释如下:
获取一个属性的时候:
首先,看这个属性是不是一个数据描述符,如果是,就直接执行描述符的__get__(),并返回值。
其次,如果这个属性不是数据描述符,那就按常规去从__dict__里面取。如果__dict__里面还没有,但这是一个非数据描述符,则执行非数据描述符的__get__()方法,并返回。
最后,找不到的属性触发__getattr__()执行
而设置一个属性的值时,访问的顺序又有所不同,请看以下讲解。
三个方法(协议):
__get__(self, instance, owner):调用一个属性时,触发
__set__(self, instance, value):为一个属性赋值时,触发
__delete__(self, instance):采用del删除属性时,触发

其中,instance是这个描述符属性所在的类的实体,而owner是描述符所在的类。
那么以上的 self, instance owner 到底指的是个什么东西呢?我们先来看一个描述符定义:

class Desc(object): def __get__(self, instance, owner): print('__get__...') print('self : \t\t', self) print('instance : \t', instance) print('owner : \t', owner) print('='*40, '\n') def __set__(self, instance, value): print('__set__...') print('self : \t\t', self) print('instance : \t', instance) print('value : \t', value) print('='*40, '\n')class TestDesc(object): x = Desc()#以下为测试代码t = TestDesc()t.x#以下为输出信息:__get__...self : <__main__.Desc object at 0x0000000002B0B828>instance : <__main__.TestDesc object at 0x0000000002B0BA20>owner : <class '__main__.TestDesc'>========================================
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

可以看到,实例化类TestDesc后,调用对象t访问其属性x,会自动调用类Desc的__get__方法,由输出信息可以看出:
  ① self: Desc的实例对象,其实就是TestDesc的属性x
  ② instance: TestDesc的实例对象,其实就是t
  ③ owner: 即谁拥有这些东西,当然是 TestDesc这个类,它是最高统治者,其他的一些都是包含在它的内部或者由它生出来的

包含这三个方法的新式类称为描述符,由这个类产生的实例进行属性的调用/赋值/删除,并不会触发这三个方法,那何时,何地,会触发这三个方法的执行呢?

描述符有什么用

  • 可以在设置属性时,做些检测等方面的处理
  • 缓存?
  • 设置属性不能被删除?那定义__delete__()方法,并raise 异常。
  • 还可以设置只读属性
  • 把一个描述符作为某个对象的属性。这个属性要更改,比如增加判断,或变得更复杂的时候,所有的处理只要在描述符中操作就行了。
  • 对属性操作提供限制,验证,管理等相关权限的操作

这些都是摘抄的,我确实不知道描述符有什么用,也不知道为什么要用描述符,先记下吧。

描述符触发执行条件以及访问优先级

一 描述符本身应该定义成新式类,owner类也应该是新式类
二 必须把描述符定义成这个类的类属性,不能为定义到构造函数中
三 要严格遵循该优先级,优先级由高到底分别是
1.类属性
2.数据描述符
3.实例属性
4.非数据描述符
5.找不到的属性触发__getattr__()

类属性优先级大于数据描述符

class Str:    def __get__(self, instance, owner):        print('Str调用')    def __set__(self, instance, value):        print('Str设置...')    def __delete__(self, instance):        print('Str删除...')class People:    name=Str()    def __init__(self,name,age): #name被Str类代理        self.name=name        self.age=age#基于上面的演示,我们已经知道,在一个类中定义描述符它就是一个类属性,存在于类的属性字典中,而不是实例的属性字典#那既然描述符被定义成了一个类属性,直接通过类名也一定可以调用吧,没错People.name #恩,调用类属性name,本质就是在调用描述符Str,触发了__get__(),类去操作属性时,会把None传给instancePeople.name='egon'  #那赋值呢,我去,并没有触发__set__()del People.name     #赶紧试试del,我去,也没有触发__delete__()'''原因:描述符在使用时被定义成另外一个类的类属性,因而类属性比二次加工的描述符伪装而来的类属性有更高的优先级People.name #恩,调用类属性name,找不到就去找描述符伪装的类属性name,触发了__get__()People.name='egon'#那赋值呢,直接赋值了一个类属性,它拥有更高的优先级,相当于覆盖了描述符,肯定不会触发描述符的__set__()del People.name #同上'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

数据描述符优先级大于实例属性

要验证,需要将实例的属性名与类的数据描述符同名

class Str: def __get__(self, instance, owner): print('Str调用') def __set__(self, instance, value): print('Str设置...') def __delete__(self, instance): print('Str删除...')class People: name=Str() def __init__(self,name,age): self.name=name self.age=agep1=People('egon',18)#如果描述符是一个数据描述符(即有__get__又有__set__),那么p1.name的调用与赋值都是触发描述符的操作,#与p1本身无关了,相当于覆盖了实例的属性p1.name='egonnnnnn'p1.nameprint(p1.__dict__)#实例属性字典中没有name,因为name是数据描述符,优先级高于实例属性,查看/赋值/删除都是跟描述符有关,与实例无关del p1.name
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

实例属性优先级大于非数据描述符

class Foo:    def func(self):        print('我胡汉三又回来了')f1=Foo()f1.func() #调用类的方法,也可以说是调用非数据描述符#函数是一个非数据描述符对象(一切皆对象么)print(dir(Foo.func))print(hasattr(Foo.func,'__set__'))print(hasattr(Foo.func,'__get__'))print(hasattr(Foo.func,'__delete__'))#有人可能会问,描述符不都是类么,函数怎么算也应该是一个对象啊,怎么就是描述符了#笨蛋哥,描述符是类没问题,描述符在应用的时候不都是实例化成一个类属性么#函数就是一个由非描述符类实例化得到的对象#没错,字符串也一样f1.func='这是实例属性啊'print(f1.func)del f1.func #删掉了非数据f1.func()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
class Foo: def __get__(self, instance, owner): print('get')class Room: name=Foo() def __init__(self,name,width,length): self.name=name self.width=width self.length=length#name是一个非数据描述符,因为name=Foo()而Foo没有实现set方法,因而比实例属性有更低的优先级#对实例的属性操作,触发的都是实例自己的r1=Room('厕所',1,1)r1.namer1.name='厨房'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

描述符使用

python是弱类型语言,即参数的赋值没有类型限制,下面我们通过描述符机制来实现类型限制功能

class Typed:    def __init__(self,name,expected_type):        self.name=name        self.expected_type=expected_type    def __get__(self, instance, owner):        print('get--->',instance,owner)        if instance is None:            return self        return instance.__dict__[self.name]    def __set__(self, instance, value):        print('set--->',instance,value)        if not isinstance(value,self.expected_type):            raise TypeError('Expected %s' %str(self.expected_type))        instance.__dict__[self.name]=value    def __delete__(self, instance):        print('delete--->',instance)        instance.__dict__.pop(self.name)def typeassert(**kwargs):    def decorate(cls):        print('类的装饰器开始运行啦------>',kwargs)        for name,expected_type in kwargs.items():            setattr(cls,name,Typed(name,expected_type))        return cls    return decorate#有参:1.运行typeassert(...)返回结果是decorate,此时参数都传给kwargs 2.People=decorate(People)@typeassert(name=str,age=int,salary=float) class People:    def __init__(self,name,age,salary):        self.name=name        self.age=age        self.salary=salaryprint(People.__dict__)p1=People('egon',18,3333.3)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

描述符使用陷阱

为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用__get____set__方法。
访问类层次上的描述符可以自动调用__get__。但是访问实例层次上的描述符只会返回描述符本身,真是魔法一般的存在啊。
确保实例的数据只属于实例本身,否则所有的实例都共享相同的数据

class Desc(object): def __init__(self, name): self.name = name def __get__(self, instance, owner): print('__get__...') print('name = ', self.name) print('=' * 40, '\n')class TestDesc(object): x = Desc('x') def __init__(self): self.y = Desc('y')# 以下为测试代码t = TestDesc()t.xprint(t.__dict__)print(t.y)'''输出结果:__get__...name = x======================================== {'y': <__main__.Desc object at 0x7f514d6ba9e8>}<__main__.Desc object at 0x7f514d6ba9e8>'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

如何检测一个对象是不是描述符

如果把问题换成——一个对象要满足什么条件,它才是描述符呢——那是不是回答就非常简单了?
答:只要定义了(set,get,delete)方法中的任意一种或几种,它就是个描述符。
那么,继续问:怎么判断一个对象定义了这三种方法呢?
立马有人可能就会回答:你是不是傻啊?看一下不就看出来了。。。
问题是,你看不到的时候呢?python内置的staticmethod,classmethod怎么看?
正确的回答应该是:看这个对象的dict
写到这里,我得先停一下,来解释一个问题。不知道读者有没有发现,上面我一直再说“对象”,“对象”,而实际上指的明明是一个类啊?在python中,这样称呼又是妥当的。因为,“一切皆对象”,类,不过也是元类的一种对象而已。
要看对象的dict好办,直接dir(对象)就行了。现在可以写出检测对象是不是描述符的方法了:

def has_descriptor_attrs(obj):    return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))def is_descriptor(obj):    '''obj can be instance of descriptor or the descriptor class'''    return bool(has_descriptor_attrs(obj))def has_data_descriptor_attrs(obj):    return set(['__set__', '__delete__']) & set(dir(obj))def is_data_descriptor(obj):    return bool(has_data_descriptor_attrs(obj))print(is_descriptor(classmethod), is_data_descriptor(classmethod))print(is_descriptor(staticmethod), is_data_descriptor(staticmethod))print(is_data_descriptor(property))'''输出:(True, False)(True, False)True看来,特性(property)是数据描述符'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

@property把函数调用伪装成对属性的访问

class Foo: @property def attr(self): print('getting attr') return 'attr value' def bar(self): passfoo = Foo()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上面这个例子中, attr 是类 Foo 的一个成员函数,可通过语句 foo.attr() 被调用。 但当它被 @property 修饰后,这个成员函数将不再是一个函数,而变为一个描述符。 bar 是一个未被修饰的成员函数。 type(Foo.attr) 与 type(Foo.bar) 的结果分别为:

class Foo:    @property    def AAA(self):        print('get的时候运行我啊')    @AAA.setter    def AAA(self,value):        print('set的时候运行我啊')    @AAA.deleter    def AAA(self):        print('delete的时候运行我啊')#只有在属性AAA定义property后才能定义AAA.setter,AAA.deleterf1=Foo()f1.AAAf1.AAA='aaa'del f1.AAA#方式2class Foo:    def get_AAA(self):        print('get的时候运行我啊')    def set_AAA(self,value):        print('set的时候运行我啊')    def delete_AAA(self):        print('delete的时候运行我啊')    AAA=property(get_AAA,set_AAA,delete_AAA) #内置property三个参数与get,set,delete一一对应f1=Foo()f1.AAAf1.AAA='aaa'del f1.AAA
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

property将一个函数变成了类似于属性的使用,无非只是省略了一个括号而已,可是这有什么意义?以属性的方式来调用函数,换句话说,我以为我调用的是属性,但是其实是函数,这样就完成了一个封装,不需要setter和getter,而直接将setter和getter内嵌进去,大大减少了代码量,使代码简洁美观
案例一:

class Goods: def __init__(self): # 原价 self.original_price = 100 # 折扣 self.discount = 0.8 @property def price(self): # 实际价格 = 原价 * 折扣 new_price = self.original_price * self.discount return new_price @price.setter def price(self, value): self.original_price = value @price.deleter def price(self): del self.original_priceobj = Goods()obj.price # 获取商品价格obj.price = 200 # 修改商品原价print(obj.price)del obj.price # 删除商品原价
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

案例二:

#实现类型检测功能#第一关:class People:    def __init__(self,name):        self.name=name    @property    def name(self):        return self.name# p1=People('alex') #property自动实现了set和get方法属于数据描述符,比实例属性优先级高,#所以你这面写会触发property内置的set,抛出异常#第二关:修订版class People:    def __init__(self,name):        self.name=name #实例化就触发property    @property    def name(self):        # return self.name #无限递归        print('get------>')        return self.DouNiWan    @name.setter    def name(self,value):        print('set------>')        self.DouNiWan=value    @name.deleter    def name(self):        print('delete------>')        del self.DouNiWanp1=People('alex') #self.name实际是存放到self.DouNiWan里print(p1.name)print(p1.name)print(p1.name)print(p1.__dict__)p1.name='egon'print(p1.__dict__)del p1.nameprint(p1.__dict__)#第三关:加上类型检查class People:    def __init__(self,name):        self.name=name #实例化就触发property    @property    def name(self):        # return self.name #无限递归        print('get------>')        return self.DouNiWan    @name.setter    def name(self,value):        print('set------>')        if not isinstance(value,str):            raise TypeError('必须是字符串类型')        self.DouNiWan=value    @name.deleter    def name(self):        print('delete------>')        del self.DouNiWanp1=People('alex') #self.name实际是存放到self.DouNiWan里p1.name=1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

以上就是关于描述符的记录,边写边思考,貌似对描述符理解到了一定的程度,至少我能说明白他是怎么回事了,只是自己还没有在实际中用过,也不知道应该在哪些场景下去使用,毋庸置疑,我会忘记它的,待到忘记的时候再来看吧。
好了,下一篇就是元类和苦大仇深的ORM了。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多