python 面向对象专题(八):特殊方法 (一)__get__、__set__、__delete__ 描述符(一)
https://www.cnblogs.com/flashBoxer/p/9771797.html
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__ 描述符(一)的更多相关文章
- __get__ __set__ __delete__描述符
描述符就是一个新式类,这个类至少要实现__get__ __set__ __delete__方法中的一种class Foo: def __get__(self, instance, owner): pr ...
- 元类编程--__get__ __set__属性描述符
from datetime import date, datetime import numbers class IntField: #数据描述符,实现以下任意一个,都会变为属性描述符 def __g ...
- 描述符__get__,__set__,__delete__
描述符__get__,__set__,__delete__ # 描述符:1用来代理另外一个类的属性 # __get__():调用一个属性时,触发 # __set__():为一个属性赋值时触发 # __ ...
- python 面向对象专题(九):特殊方法 (二)__get__、__set__、__delete__ 描述符(二)覆盖型与非覆盖型描述符对比
前言 根据是否定义__set__ 方法,描述符可分为两大类. 实现 __set__ 方法的描述符属于覆盖型描述符,因为虽然描述符是类属性,但是实现 __set__ 方法的话,会覆盖对实例属性的赋值操作 ...
- python 面向对象进阶之内置方法
一 isinstance(obj,cls)和issubclass(sub,super) 1.1,isinstance(obj,cls)检查是否obj是否是类 cls 的对象 class Foo(obj ...
- Python描述符(__get__,__set__,__delete__)简介
先说定义,这里直接翻译官方英文文档: 一般来说,描述符是具有“绑定行为”的对象属性,该对象的属性访问将会被描述符协议中的方法覆盖.这些方法是__get__(),__set__(),和__delete_ ...
- python基础----再看property、描述符(__get__,__set__,__delete__)
一.再看property 一个静态属性property ...
- 描述符__get__,__set__,__delete__和析构方法__del__
描述符__get__,__set__,__delete__ 1.描述符是什么:描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),__set__(),__delete__()中的一 ...
- Python类总结-描述符__get__(),__set__(),__delete__()
1 描述符是什么:描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),set(),delete()中的一个,这也被称为描述符协议 get():调用一个属性时,触发 set():为一 ...
- 【Python】【元编程】【二】【描述符】
""" #描述符实例是托管类的类属性:此外,托管类还有自己实例的同名属性 #20.1.1 LineItem类第三版:一个简单的描述符#栗子20-1 dulkfood_v3 ...
随机推荐
- ado.net Web前端:关于JavaScript知识点的简单梳理
学习js:1.htmml2.cssjs+html+css == html5 js的组成:1).ecamscript ES是js的标准,js 是es 的实现2)文档对象模型(Document Objec ...
- .Net Core服务监控报警指标上报Prometheus+Grafana
前言 简单集成Prometheus+Grafana,指标的上报收集可视化. Prometheus Prometheus是一个监控平台,监控从HTTP端口收集受监控目标的指标.在微服务的架构里Prome ...
- tarjan算法求scc & 缩点
前置知识 图的遍历(dfs) 强连通&强连通分量 对于有向图G中的任意两个顶点u和v存在u->v的一条路径,同时也存在v->u的路径,我们则称这两个顶点强连通.以此类推,强连通分量 ...
- debug PostgreSQL 9.6.18 using Eclipse IDE on CentOS7
目录 debug PostgreSQL 9.6.18 using Eclipse IDE on CentOS7 1.概览 2.建立用户 3.编译postgre 4.启动Eclipse 5.设置环境变量 ...
- Linux下安装MongoDB 4.2数据库--使用tar包方式
(一)基础环境设置 操作系统版本 :centos-7.4 MongoDB版本:MongoDB 4.2 社区版 (1)关闭防火墙 # 关闭防火墙 [root@mongodbenterprise lib ...
- elasticSearch插件的安装以及使用nginx的modles收集nginx的日志
1.首先在windows环境上搭建es的集群 集群的配置如下 #node01的配置: cluster.name: es-itcast-cluster node.name: node01 node.ma ...
- JavaWeb网上图书商城完整项目--day02-15.登录功能流程分析
当用户点击登录界面的登录按钮的时候,将登录的用户名.密码和验证码上传到后台,后台的业务流程如下面所示:
- Spring-AOP:一、注解demo及基本概念
切面:Aspect 切面=切入点+通知.在老的spring版本中通常用xml配置,现在通常是一个类带上@Aspect注解.切面负责将 横切逻辑(通知) 编织 到指定的连接点中. 目标对象:Target ...
- Spring7——开发基于注解形式的spring
开发基于注解形式的spring SpringIOC容器的2种形式: (1)xml配置文件:applicationContext.xml; 存bean:<bean> 取bean: Appli ...
- 【秒懂Java】【第1章_初识Java】02_软件开发
通过上一篇文章<01_编程语言>,我们了解到 Java是众多编程语言中的其中一种 编程语言可以用来开发软件 因此,我们即将要学习的Java技术,是属于软件开发的范畴.那软件开发的前景如何呢 ...