https://www.cnblogs.com/flashBoxer/p/9771797.html

实现了 __get__、__set__ 或 __delete__ 方法的类是描述符。描述符的用法是,创建一个实例,作为另一个类的类属性。

1 前言

描述符是对多个属性运用相同存取逻辑的一种方式。例如,DjangoORM 和 SQL Alchemy 等 ORM 中的字段类型是描述符,把数据库记录中字段里的数据与 Python 对象的属性对应起来。

描述符是实现了特定协议的类,这个协议包括 __get__、__set__ 和__delete__ 方法。

理解描述符是精通 Python 的关键。

2 描述符示例:验证属性

解决重复编写读值方法和设值的面向对象方式是描述符类。

2.1 LineItem类:一个简单的描述符

实现了 __get__、__set__ 或 __delete__ 方法的类是描述符。描述符的用法是,创建一个实例,作为另一个类的类属性。

我们将定义一个 Quantity 描述符,LineItem 类会用到两个 Quantity实例:一个用于管理 weight 属性,另一个用于管理 price 属性。示意
图有助于理解,如图 20-1 所示

图 20-1:LineItem 类的 UML 示意图,用到了名为 Quantity 的描述符类。UML 示意图中带下划线的属性是类属性。注意,weight 和price 是依附在 LineItem 类上的 Quantity 类的实例,不过LineItem 实例也有自己的 weight 和 price 属性,存储着相应的值。

注意,在图 20-1 中,“weight”这个词出现了两次,因为其实有两个不同的属性都叫 weight:一个是 LineItem 的类属性,另一个是各个LineItem 对象的实例属性。price 也是如此。

从现在开始,我会使用下述定义。
描述符类
  实现描述符协议的类。在图 20-1 中,是 Quantity 类。
托管类
  把描述符实例声明为类属性的类——图 20-1 中的 LineItem 类。
描述符实例
  描述符类的各个实例,声明为托管类的类属性。在图 20-1 中,各个描述符实例使用箭头和带下划线的名称表示(在 UML 中,下划线表示类属性)。与黑色菱形接触的 LineItem 类包含描述符实例。
托管实例
  托管类的实例。在这个示例中,LineItem 实例是托管实例(没在类图中展示)。
储存属性
  托管实例中存储自身托管属性的属性。在图 20-1 中,LineItem 实例的 weight 和 price 属性是储存属性。这种属性与描述符属性不同,描述符属性都是类属性。
托管属性
  托管类中由描述符实例处理的公开属性,值存储在储存属性中。也就是说,描述符实例和储存属性为托管属性建立了基础。
Quantity 实例是 LineItem 类的类属性,这一点一定要理解。图 20-2中的机器和小怪兽强调了这个关键点。

图 20-2:带有 MGN(Mills & Gizmos Notation,机器和小怪兽图示法)注解的 UML 类图:类是机器,用于生产小怪兽(实例)。Quantity 机器生产了两个圆头的小怪兽,依附到 LineItem机器上,即 weight 和 price。LineItem 机器生产方头的小怪兽,有自己的 weight 和 price 属性,存储着相应的值

图 20-3:MGN 简图表示,LineItem 类生产了三个实例,Quantity 类生产了两个实例。其中一个 Quantity 实例从一个 LineItem 实例中获取存储的值

在这个示例中,我把 LineItem 实例画成表格中的行,各有三个单元格,表示三个属性(description、weight 和price)。Quantity 实例是描述符,因此有个放大镜,用于获取
值(__get__),以及一个手抓,用于设置值(__set__)。

示例 20-1

class Quantity:  #描述符基于协议实现,无需创建子类。
def __init__(self, storage_name):
self.storage_name = storage_name #Quantity 实例有个 storage_name 属性, #这是托管实例中存储值的属性的名称。 def __set__(self, instance, value): #尝试为托管属性赋值时,
# 会调用 __set__ 方法。这里,self 是描述符实例
#(即 LineItem.weight 或 LineItem.price),
#instance 是托管实例(LineItem 实例),value 是要设定的值。
if value > 0:
instance.__dict__[self.storage_name] = value #这里,必须直接处理托管实例的 __dict__ 属性;
#如果使用内置的setattr 函数,
#会再次触发 __set__ 方法,导致无限递归
else:
raise ValueError('value must be > 0') class LineItem:
weight = Quantity('weight') #第一个描述符实例绑定给 weight 属性
price = Quantity('price') #第二个描述符实例绑定给 price 属性
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price def subtotal(self):
return self.weight * self.price

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

你可能想把各个托管属性的值直接存在描述符实例中,但是这种做法是错误的。也就是说,在 __set__ 方法中,应该像下面这样写:

instance.__dict__[self.storage_name] = value

而不能试图使用下面这种错误的写法

self.__dict__[self.storage_name] = value

为了理解错误的原因,可以想想 __set__ 方法前两个参数(self 和instance)的意思。这里,self 是描述符实例,它其实是托管类的类属性。

同一时刻,内存中可能有几千个 LineItem 实例,不过只会有两个描述符实例:LineItem.weight 和 LineItem.price。因此,存储
在描述符实例中的数据,其实会变成 LineItem 类的类属性,从而由全部 LineItem 实例共享。

2.2自动获取储存属性的名称

为了避免在描述符声明语句中重复输入属性名,我们将为每个Quantity 实例的 storage_name 属性生成一个独一无二的字符串。图
20-4 是更新后的 Quantity 和 LineItem 类的 UML 类图。

图 20-4:示例 20-2 的 UML 类图。现在,Quantity 类既有 __get__方法,也有 __set__ 方法;LineItem 实例中储存属性的名称是生成
的,_Quantity#0 和 _Quantity#1

为了生成 storage_name,我们以 '_Quantity#' 为前缀,然后在后面拼接一个整数: Quantity.__counter 类属性的当前值,每次把一个
新的 Quantity 描述符实例依附到类上,都会递增这个值。在前缀中使用井号能避免 storage_name 与用户使用点号创建的属性冲突,因为

nutmeg._Quantity#0 是无效的 Python 句法。但是,内置的 getattr和 setattr 函数可以使用这种“无效的”标识符获取和设置属性,此外
也可以直接处理实例属性 __dict__。示例 20-2 是新的实现。

示例 20-2 每个 Quantity 描述符都有独一无二的 storage_name

class Quantity:
__counter = 0 #__counter 是 Quantity 类的类属性,统计Quantity 实例的数量。 def __init__(self):
cls = self.__class__ #cls 是 Quantity 类的引用。
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index) #每个描述符实例的 storage_name 属性都是独一无二的,
#因为其值由描述符类的名称和 __counter 属性的当前值构成(例
                                                            #如,_Quantity#0)。
        cls.__counter += 1  #递增 __counter 属性的值。
    def __get__(self, instance, owner):  #我们要实现 __get__ 方法,因为托管属性的名称与 storage_name
#不同。稍后会说明 owner 参数。
return getattr(instance, self.storage_name) #使用内置的 getattr 函数从
# instance 中获取储存属性的值。
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value) #使用内置的 setattr 函数把值存储在 instance 中。
else:
raise ValueError('value must be > 0') class LineItem:
weight = Quantity() #现在,不用把托管属性的名称传给 Quantity 构造方法。这是这一版的目标。

price = Quantity()

def __init__(self, description, weight, price):
          self.description = description
          self.weight = weight
      self.price = price

    def subtotal(self):
      return self.weight * self.price

 

这里可以使用内置的高阶函数 getattr 和 setattr 存取值,无需使用instance.__dict__,因为托管属性和储存属性的名称不同,所以把
储存属性传给 getattr 函数不会触发描述符,不会像示例 20-1 那样出现无限递归。

注意,__get__ 方法有三个参数:self、instance 和 owner。owner参数是托管类(如 LineItem)的引用,通过描述符从托管类中获取属
性时用得到。如果使用 LineItem.weight 从类中获取托管属性(以weight 为例),描述符的 __get__ 方法接收到的 instance 参数值是
None。因此,下述控制台会话才会抛出 AttributeError 异常:

>>> from bulkfood_v4 import LineItem
>>> LineItem.weight
Traceback (most recent call last):
...
File ".../descriptors/bulkfood_v4.py", line 54, in __get__
return getattr(instance, self.storage_name)
AttributeError: 'NoneType' object has no attribute '_Quantity#0'

抛出 AttributeError 异常是实现 __get__ 方法的方式之一,如果选择这么做,应该修改错误消息,去掉令人困惑的 NoneType 和
_Quantity#0,这是实现细节。把错误消息改成"'LineItem' classhas no such attribute" 更好。最好能给出缺少的属性名,但是在
这个示例中,描述符不知道托管属性的名称,因此目前只能做到这样。此外,为了给用户提供内省和其他元编程技术支持,通过类访问托管属
性时,最好让 __get__ 方法返回描述符实例。示例 20-3 对示例 20-2 做了小幅改动,为 Quantity.__get__ 方法添加了一些逻辑。
示例 20-3

通过托管类调用时,__get__ 方法返回描述符的引用

class Quantity:
__counter = 0 def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index)
cls.__counter += 1 def __get__(self, instance, owner):
if instance is None:
return self #如果不是通过实例调用,返回描述符自身。 else:
return getattr(instance, self.storage_name)
#否则,像之前一样,返回托管属性的值。
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0')

看着示例 20-2,你可能觉得就为了管理几个属性而编写这么多代码不值得,但是要知道,描述符逻辑现在被抽象到单独的代码单元
(Quantity 类)中了。通常,我们不会在使用描述符的模块中定义描述符,而是在一个单独的实用工具模块中定义,以便在整个应用中使用
——如果开发的是框架,甚至会在多个应用中使用。了解这一点之后就可推知,示例 20-4 是描述符的常规用法。

示例 20-4 bulkfood_v4c.py:整洁的 LineItem 类;Quantity 描述符类现在位于导入的 model_v4c 模块中

import model_v4c as model  ➊

class LineItem:
weight = model.Quantity() ➋
price = model.Quantity() def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price def subtotal(self):
return self.weight * self.price

❶ 导入 model_v4c 模块,指定一个更友好的名称。
❷ 使用 model.Quantity 描述符。

2.3一种新型描述符

我们虚构的有机食物网店遇到一个问题:不知怎么回事儿,有个商品的描述信息为空,导致无法下订单。为了避免出现这个问题,我们要再创
建一个描述符,NonBlank。在设计 NonBlank 的过程中,我们发现,它与 Quantity 描述符很像,只是验证逻辑不同。
回想 Quantity 的功能,我们注意到它做了两件不同的事:管理托管实例中的储存属性,以及验证用于设置那两个属性的值。由此可知,我们
可以重构,并创建两个基类。

AutoStorage
  自动管理储存属性的描述符类。

Validated
  扩展 AutoStorage 类的抽象子类,覆盖 __set__ 方法,调用必须由子类实现的 validate 方法。
我们稍后会重写 Quantity 类,并实现 NonBlank,让它继承Validated 类,只编写 validate 方法。类之间的关系见图 20-5。

图 20-5:几个描述符类的层次结构。AutoStorage 基类负责自动存储属性;Validated 类做验证,把职责委托给抽象方法
validate;Quantity 和 NonBlank 是 Validated 的具体子类Validated、Quantity 和 NonBlank 三个类之间的关系体现了模板方
法设计模式。具体而言,Validated.__set__ 方法正是 Gamma 等四人所描述的模板方法的例证:
一个模板方法用一些抽象的操作定义一个算法,而子类将重定义这些操作以提供具体的行为。

示例 20-6 model_v5.py:重构后的描述符类

import abc

class AutoStorage:  #AutoStorage 类提供了之前 Quantity 描述符的大部分功能……
__counter = 0 def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index)
cls.__counter += 1 def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.storage_name) def __set__(self, instance, value):
setattr(instance, self.storage_name, value) #……验证除外 class Validated(abc.ABC, AutoStorage): #Validated 是抽象类,不过也继承自 AutoStorage 类。
def __set__(self, instance, value):
value = self.validate(instance, value) #__set__ 方法把验证操作委托给 validate 方法……
super().__set__(instance, value) #……然后把返回的 value 传给超类的 __set__ 方法,存储值。
@abc.abstractmethod
def validate(self, instance, value): #在这个类中,validate 是抽象方法
"""return validated value or raise ValueError""" class Quantity(Validated): #Quantity 和 NonBlank 都继承自
# Validated 类。
"""a number greater than zero""" def validate(self, instance, value):
if value <= 0:
raise ValueError('value must be > 0')
return value class NonBlank(Validated):
"""a string with at least one non-space character""" def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value #要求具体的 validate 方法返回验证后的值,
#借机可以清理、转换或
#规范化接收的数据。
#这里,我们把 value 首尾的空白去掉,然后将其返回。

model_v5.py 脚本的用户不需要知道全部细节。用户只需知道,他们可以使用 Quantity 和 NonBlank 自动验证实例属性。参见示例 20-7 中的最新版 LineItem 类。

示例 20-7 bulkfood_v5.py:使用 Quantity 和 NonBlank 描述符的LineItem 类

import model_v5 as model  #导入 model_v5 模块,
#指定一个更友好的名称。 class LineItem:
description = model.NonBlank() #使用 model.NonBlank 描述符。
#其余的代码没变。
weight = model.Quantity()
price = model.Quantity() def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price def subtotal(self):
return self.weight * self.price

本章所举的几个 LineItem 示例演示了描述符的典型用途——管理数据属性。这种描述符也叫覆盖型描述符,因为描述符的 __set__ 方法使
用托管实例中的同名属性覆盖(即插手接管)了要设置的属性。不过,也有非覆盖型描述符。

python 面向对象专题(八):特殊方法 (一)__get__、__set__、__delete__ 描述符(一)的更多相关文章

  1. __get__ __set__ __delete__描述符

    描述符就是一个新式类,这个类至少要实现__get__ __set__ __delete__方法中的一种class Foo: def __get__(self, instance, owner): pr ...

  2. 元类编程--__get__ __set__属性描述符

    from datetime import date, datetime import numbers class IntField: #数据描述符,实现以下任意一个,都会变为属性描述符 def __g ...

  3. 描述符__get__,__set__,__delete__

    描述符__get__,__set__,__delete__ # 描述符:1用来代理另外一个类的属性 # __get__():调用一个属性时,触发 # __set__():为一个属性赋值时触发 # __ ...

  4. python 面向对象专题(九):特殊方法 (二)__get__、__set__、__delete__ 描述符(二)覆盖型与非覆盖型描述符对比

    前言 根据是否定义__set__ 方法,描述符可分为两大类. 实现 __set__ 方法的描述符属于覆盖型描述符,因为虽然描述符是类属性,但是实现 __set__ 方法的话,会覆盖对实例属性的赋值操作 ...

  5. python 面向对象进阶之内置方法

    一 isinstance(obj,cls)和issubclass(sub,super) 1.1,isinstance(obj,cls)检查是否obj是否是类 cls 的对象 class Foo(obj ...

  6. Python描述符(__get__,__set__,__delete__)简介

    先说定义,这里直接翻译官方英文文档: 一般来说,描述符是具有“绑定行为”的对象属性,该对象的属性访问将会被描述符协议中的方法覆盖.这些方法是__get__(),__set__(),和__delete_ ...

  7. python基础----再看property、描述符(__get__,__set__,__delete__)

    一.再看property                                                                          一个静态属性property ...

  8. 描述符__get__,__set__,__delete__和析构方法__del__

    描述符__get__,__set__,__delete__ 1.描述符是什么:描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),__set__(),__delete__()中的一 ...

  9. Python类总结-描述符__get__(),__set__(),__delete__()

    1 描述符是什么:描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),set(),delete()中的一个,这也被称为描述符协议 get():调用一个属性时,触发 set():为一 ...

  10. 【Python】【元编程】【二】【描述符】

    """ #描述符实例是托管类的类属性:此外,托管类还有自己实例的同名属性 #20.1.1 LineItem类第三版:一个简单的描述符#栗子20-1 dulkfood_v3 ...

随机推荐

  1. @topcoder - SRM603D1L3@ SumOfArrays

    目录 @desription@ @solution@ @accepted code@ @details@ @desription@ 给定两个长度为 n 的数列 A, B.现你可以将两数列重排列,然后对 ...

  2. Divisors (求解组合数因子个数)【唯一分解定理】

    Divisors 题目链接(点击) Your task in this problem is to determine the number of divisors of Cnk. Just for ...

  3. elasticsearch unassigned shards 导致RED解决

    先通过命令查看节点的shard分配整体情况 curl -X GET "ip:9200/_cat/allocation?v" 说明:有16个索引未分片 2.查看未分片的索引 curl ...

  4. loads和dumps的用法

    import json s='{"name":"wuxie","sex":"m","data":nu ...

  5. C# 什么是泛型 ?以及对泛型各方面的一些知识点的整理

    1.1 理解什么是泛型 在.NET 2.0,可以成为革命性壮举的, 就是引入了激动人心的特性——泛型..NET泛型是CLR和高级语言共同支持的一种全新的结构,实现了一种将类型抽象化的通用处理方式.在泛 ...

  6. oracle不足位数补零的实现sql语句

    select rpad('AAA',5,'0') from dual; 这样就可以了 [注意] 1.'AAA'为待补字符:5表示补齐后的总字符长度:0表示不足时补什么字符 2.rpad是右侧补0,左侧 ...

  7. Python3-hashlib模块-加密算法之安全哈希

    Python3中的hashlib模块提供了多个不同的安全哈希算法的通用接口 hashlib模块代替了Python2中的md5和sham模块,使用这个模块一般分为3步 1.创建一个哈希对象,使用哈希算法 ...

  8. robot framework使用小结(一)

    项目组要用到robot framework验收web,因此花了两天时间了解了一下这个框架.我把网上各位大侠分享的内容整理成一个小小demo,参考的出处没有列出来,在此一并感谢各位. demo仍旧是打开 ...

  9. 深入理解JVM(③)虚拟机的类加载过程

    前言 上一篇我们介绍到一个类的生命周期大概分7个阶段:加载.验证.准备.解析.初始化.使用.卸载.并且也介绍了类的加载时机,下面我们将介绍一下虚拟机中类的加载的全过程.主要是类生命周期的,加载.验证. ...

  10. Format中的转换说明符

    %a(%A) 浮点数.十六进制数字和p-(P-)记数法(C99)%c 单个字符%d 有符号十进制整数%f 浮点数(包括float和doulbe)%e(%E) 指数形式的浮点数[e-(E-)记数法]%g ...