流畅的python笔记(二十)属性描述符

目录

前言

一、描述符示例:验证属性

LineItem类第三版:一个简单的描述符

LineItem类第四版:自动获取储存属性的名称

LineItem类第五版:一种新型描述符

二、覆盖型与非覆盖型描述符对比

覆盖型描述符

没有__get__方法的覆盖型描述符

非覆盖型描述符

在类中覆盖描述符

三、方法是描述符

四、描述符用法建议

五、描述符的文档字符串和覆盖删除操作


前言

描述符是对多个属性运用相同存取逻辑的一种方式。

        描述符是实现了特定协议的类,这个协议包括__get__、__set__和__delete__方法。property类实现了完整的描述符协议。描述符是python的独有特征,不仅在应用层使用,在语言的基础设施中也有用到。

一、描述符示例:验证属性

LineItem类第三版:一个简单的描述符

实现了__get__、__set__、__delete__方法的类是描述符。描述符的用法是,创建一个实例,作为另一个类的类属性。下面例子中,我们将定义一个Quantity描述符类,然后创建两个Quantity实例作为LineItem类的类属性,分别用于管理weight实例属性和price实例属性:

以下是本节常用术语定义:

  • 描述符类,实现描述符协议的类。比如Quantity。
  • 托管类,把描述符类的实例声明为类属性的类,比如LineItem类。
  • 描述符实例,描述符类的各个实例,声明为托管类的类属性。
  • 托管实例,托管类的实例,比如LineItem类的实例。
  • 储存属性,托管实例中存储自身托管属性的属性,比如LineItem实例的weight和price两个实例属性都是储存属性。
  • 托管属性,托管类中由描述符实例处理的公开属性,值存储在储存属性中。

下面是Quantity描述符类和新版LineItem类,用到两个Quantity实例:

  1.  描述符基于协议实现,无需继承某抽象基类。
  2.  Quantity实例中有个storage_name属性,这是托管实例中的储存属性的名称。
  3.  尝试为托管属性赋值时,会调用__set__方法。这里self是描述符实例(即LineItem.weight或LineItem.price),instance是托管实例(LineItem实例),value是要设定的值。
  4. 这里必须直接处理托管实例的__dict__属性,如果使用内置的setattr函数,会再次触发__set__方法,导致无限递归。
  5. 第一个描述符实例绑定给weight属性。
  6. 第二个描述符实例绑定给price属性。

该例中各个托管属性的名称和储存属性一样,而且读值方法不需要特殊的逻辑,所以Quantity类不需要定义__get__方法。

LineItem类第四版:自动获取储存属性的名称

  

如上图左边所示,我们在托管类的定义体中实例化描述符时要输入两次属性的名称,如果能像上图右边那样,自动获取储存属性的名称,即只输入一次,可以让代码更简便。

        为了避免在描述符声明语句中重复输入属性名,我们将为每个Quantity实例的storage_name属性生成一个独一无二的字符串。

 

 

  1. __counter是Quantity类的类属性,统计Quantity实例的数量。
  2. cls是Quantity类的引用。
  3. 每个描述符实例的storage_name属性都是独一无二的,因为其值由描述符类的名称和__counter类属性的当前值构成,中间以#号隔开,如_Quantity#0。
  4. 递增__counter类属性的值。
  5. 需要实现__get__方法,因为托管属性的名称与storage_name不同。
  6. 使用内置的getattr函数从instance中获取储存属性的值。
  7. 使用内置的setattr函数把值存储在instance中,此时储存属性的名称是生成的,即Quantity中的self.storage_name属性,与托管属性的名称weight或price不同了,即我们在托管实例中打了猴子补丁,动态生成了新的属性作为储存属性。
  8. 现在不用把托管属性的名称传给Quantity构造方法。

 描述符在类中定义,因此可以通过继承重用部分代码来创建新描述符。

LineItem类第五版:一种新型描述符

通过继承重用代码来创建新描述符。

 

 

  1.  AutoStorage类提供了之前Quantity描述符类的大部分功能。
  2. 但是在设值方法中没有对值做验证。
  3. Validated是抽象类,不过也继承自AutoStorage类。
  4. __set__方法把验证操作委托给validate方法。
  5. 把验证后的value值传给超类的__set__方法,存储值。
  6. 在这个类中,validate是抽象方法。
  7. Quantity和NonBlank都继承自Validate类。
  8. 要求具体的validate方法返回验证后的值,借此可以清理、转换或规范化接收的数据。

  1.  导入model_v5模块。
  2.  使用model.NonBlank描述符。

以上演示了描述符的典型用途------管理数据属性。这种描述符也叫覆盖型描述符,因为描述符的__set__方法使用托管实例中的同名属性覆盖了要设置的属性。也有非覆盖型描述符。

二、覆盖型与非覆盖型描述符对比

python中存取属性的方式不对等,通过实例读取属性时,通常返回的是实例中定义的属性,但是如果没有指定的实例属性,那么会获取类属性。而为实例中的属性赋值时,如果没有指定的实例属性,则通常会在实例中创建属性,不会影响到类。

        这种存取属性方式的不对等,对描述符也有影响。根据是否定义__set__方法,描述符分为两大类:覆盖型描述符与非覆盖型描述符。

  1.  有__get__和__set__方法的典型覆盖型描述符。
  2.  示例中各个描述符的每个方法都调用了print_args函数。
  3.  没有__get__方法的覆盖型描述符。
  4.  没有__set__方法,因此式非覆盖型描述符。
  5.  托管类,其中使用各个描述符类的一个实例。
  6.  spam方法放在这里是为了对比,因为方法也是描述符。

覆盖型描述符

实现__set__方法的描述符属于覆盖型描述符。因为虽然描述符实例是类属性,但是实现__set__方法的话,会覆盖对实例属性的赋值操作。特性也是覆盖型描述符:如果没有提供设值函数,property类中的__set__方法会抛出AttributeError异常,指明那个属性是只读的。

  1.  创建供测试使用的托管类Managed对象。
  2.  obj.over触发描述符的__get__方法,__get__方法第二个参数instance的值是托管实例obj。
  3.  Managed.over触发描述符的__get__方法,__get__方法第二个参数instance的值是None。
  4.  为obj.over赋值,触发描述符的__set__方法,最后一个参数的值是7。这次赋值肯定是失败的。
  5.  读取obj.over,仍会触发描述符的__get__方法。
  6.  跳过描述符,直接通过obj.__dict__属性设值。
  7.  确认值在obj.__dict__属性中,在over键名下。
  8.  然而,即使是有名为over的实例属性,Managed.over描述符仍会覆盖读取obj.over这个操作

没有__get__方法的覆盖型描述符

覆盖型描述符也可以只实现__set__方法,此时,只有写操作由描述符处理。因为没有处理读操作的__get__方法,当读取对应属性的时候,实例属性会遮盖描述符。

  1.  这个覆盖型描述符没有__get__方法,因此obj.over_no_get从类中获取描述符实例。
  2.  直接从托管类中获取类属性over_no_get也是直接获取了描述符实例,而不是对应储存属性的值。
  3.  为obj.over_no_get赋值会触发描述符的__set__方法。
  4.  由于我们实现的__set__方法并不修改属性,只是打印一些信息,因此并没有增加新的名为over_no_get的实例属性,因此读取obj.over_no_get获取的仍是托管类中的描述符实例。
  5.  通过实例的__dict__属性设置名为over_no_get的实例属性。
  6.  现在over_no_get实例属性会遮盖描述符,但是只有读操作是如此。
  7.  为obj.over_no_get赋值,仍然是触发描述符的__set__方法。
  8.  读取obj.over_no_get,只要有同名的实例属性,描述符就会被遮盖。

非覆盖型描述符

没有实现__set__方法的描述符是非覆盖型描述符。如果设置了同名的实例属性,再通过对象给指定属性赋值时,描述符会被遮盖。

  1.  obj.non_over触发描述符的__get__方法,第二个参数的值是obj。
  2.  这里会新增一个non_over实例属性,因为Managed.non_over是非覆盖型描述符,因此没有干涉赋值操作的__set__方法。
  3.  obj的实例属性non_over居然把Managed类的同名描述符属性遮盖了!跟我想的不太一样,我还以为描述符只要实现了__get__方法,那么读值的时候描述符就一定会遮盖实例属性。
  4.  通过类来读non_over才会触发__get__方法。
  5.  删除non_over实例属性。
  6.  此时通过托管实例读取non_over时会触发描述符的__get__方法。

覆盖型描述符也叫数据描述符或强制描述符,非覆盖型描述符也叫非数据描述符或遮盖型描述符。

        上述示例中,如果描述符中有__set__,那么通过实例给属性赋值时就会触发__set__方法,如果没有__set__,则会创建一个新的实例属性。即在赋值的时候描述符可以遮盖实例属性,但是我们可以看到,描述符是无法影响类属性的赋值操作的,这也意味着为类属性赋值能覆盖描述符属性。

在类中覆盖描述符

不管描述符是不是覆盖型,为类属性赋值都能覆盖描述符。这是一种猴子补丁技术。

  1. 为后边测试新建实例。
  2. 覆盖类中的描述符属性。
  3. 可以看到所有描述符都已经被覆盖。

三、方法是描述符

在类中定义的函数属于绑定方法(bound method),因为用户定义的函数都有__get__方法,所以依附到类上时,就相当于描述符。因为只有__get__方法,没有__set__方法,因此方法是非覆盖型描述符。是不是因为函数是一等对象,且所有函数类中都有__get__方法,所以当把函数放到类中时,相当于把一个有__get__方法的类的对象放到另一个类中作为类属性,因此,类中定义的函数相当于描述符。

  1.  spam是Managed类中定义的一个实例方法,这里可以看出来obj.spam其本质是一个bound method,即绑定方法。通过实例来访问一个托管属性,会触发描述符实例的__get__方法。
  2.  但是Managed.spam获取的是函数,且地址与上边obj.spam不一样啊。
  3.  通过obj.spam赋值,会遮盖类属性(因为方法是非覆盖型描述符,没有__set__方法,因为通过obj.spam赋值会新建一个实例属性,而通过obj访问时该实例属性会遮盖描述符),导致无法通过obj实例访问spam方法。

从上例可以看出,obj.spam和Managed.spam获取的是不同的对象:

  • 通过托管类访问时,函数的__get__方法会返回自身的引用。
  • 通过实例访问时,函数的__get__方法会返回绑定方法对象,即一种可调用的对象,里面包装着函数,并把托管实例obj绑定给函数的第一个参数。

下面来分析这种机制:

  1.  Text实例的repr方法返回一个类似Text构造方法调用的字符串。
  2.  reverse方法返回反向拼写的单词。
  3.  在类上调用实例方法相当于调用一个普通函数,需要我们手动传入一个Text对象
  4.  注意类型不同,在类上引用实例方法得到的是function,在实例上引用实例方法得到的是method。
  5.  Text.reverse相当于函数,甚至可以处理Text实例之外的其他对象。
  6.  用类引用实例方法得到的是函数Text.reverse,reverse相当于Text类的类属性,是一个非覆盖型描述符。调用该描述符实例的__get__方法传入Text实例,得到的是绑定到Text实例上的方法。
  7.  调用函数的__get__方法时,如果instance参数的值是None,那么得到的是函数本身。
  8.  word.reverse表达式其实会调用Text.reverse.__get__(word),返回对应的绑定方法。
  9.  绑定方法对象有个__self__属性,其值是调用这个方法的那个实例的引用。
  10.  绑定方法的__func__属性是依附在托管类上那个原始函数的引用。

绑定方法对象还有个__call__方法,这个方法会调用__func__属性引用的原始函数,把函数的第一个参数设为绑定方法对象的__self__属性,这就是形参self的隐式绑定方式

函数会变成绑定方法,这是python语言底层使用描述符的最好例证。

四、描述符用法建议

  • 使用特性以保持简单。内置的property类创建的其实是覆盖型描述符,__set__方法和__get__方法都实现了。特性的__set__方法默认抛出AttributeError:can't set attribute,因此创建只读属性最简单的方式是使用特性。
  • 只读描述符必须有__set__方法。如果使用描述符类实现只读属性,则__get__和__set__两个方法都必须定义(即定义一个覆盖型描述符),否则实例的同名属性会遮盖描述符。只读属性的__set__方法只需抛出AttributeError异常并提供合适的错误消息。
  • 用于验证的描述符可以只有__set__方法。__set__方法应该检查value参数获得的值,如果有效,使用描述符实例的名称为键,直接在托管实例的__dict__属性中设置。
  • 仅有__get__方法的描述符可以实现高效缓存。如果只实现了__get__方法,那么创建的是非覆盖型描述符。这种描述符可用于执行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。同名实例属性会遮盖描述符,因此后续访问会直接从实例的__dict__是修那个中获取值,而不会再触发描述符的__get__方法。
  • 非特殊的方法可以被实例属性遮盖。由于函数和方法只实现了__get__方法(即非覆盖型描述符),他们不会处理同名实例属性的赋值操作。因此像my_obj.the_method = 7 这样简单赋值之后,后续通过该实例访问the_method得到的是数字7,但是不影响类或者其他实例对the_method的访问。但是,特殊方法不受这个问题的影响,repr(x)执行的其实是x.__class__.__repr__(x),即特殊方法都是通过类来调用的。

五、描述符的文档字符串和覆盖删除操作

描述符类的文档字符串用于注解托管类中的各个描述符实例。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值