Python属性描述符(一)
描述符是对多个属性运用相同存取逻辑的一种方式,,是实现了特性协议的类,这个协议包括了__get__、__set__和__delete__方法。property类实现了完整的描述符协议。通常,可以只实现部分协议,如只实现了__get__或__set__,而不必把__get__、__set__和__delete__全部实现
现在,让我们用描述符协议升级上一个章节Python动态属性和特性(二)的LineItem类
图1-1
我们将定义一个Quantity类,LineItem类会用到两个Quantity实例:一个用于管理 weight属性,另一个用于管理 price属性。weight这个属性出现了两次,但两次都有不同,一个是LineItem的类属性,另一个是各个LineItem 对象的实例属性,同理price
现在,让我们看一些定义:
- 描述符类:实现描述符协议的类,比如__set__、__get__或__delete__方法,如图1-1的Quantity类
- 托管类:把描述符实例声明为类属性的类,如图1-1中的LineItem类中的weight和price都为类属性,都为Quantity描述符类的实例
- 描述符实例:描述符类的各个实例, 声明为托管类的类属性,如LineItem类中的weight和price属性
- 托管实例:托管类的实例,在图1-1中,LineItem类的实例即为托管类实例
- 储存属性:托管实例中存储自身托管属性的属性。在图1-1中,LineItem实例的weight和price属性是储存属性。这种属性与描述符属性不同,描述符属性都是类属性
- 托管属性:托管类中由描述符实例处理的公开属性,值存储在储存属性中。也就是说,描述符实例和储存属性为托管属性建立了基础
下面,让我们来看一个例子
class Quantity: # <3> def __init__(self, storage_name): # <4>
self.storage_name = storage_name def __set__(self, instance, value): # <5>
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError('value must be > 0') class LineItem:
weight = Quantity('weight') # <1>
price = Quantity('price') def __init__(self, description, weight, price): # <2>
self.description = description
self.weight = weight
self.price = price def subtotal(self):
return self.weight * self.price
我们将上面的代码与之前的定义对应起来,首先是Quantity类,我们之前说过,只要实现了__set__、__get__或__delete__方法的类,就是描述符类,所以Quantity毫无疑问的是描述符类,再来是LineItem,根据之前的定义,托管类中的类属性,是描述符类的实例,LineItem类的weight和price两个类属性都是Quantity描述符类的实例,所以LineItem类即为托管类,再来,我们根据代码中的标号分析一下代码:
- LineItem中两个属性weight和price为描述符实例
- 当实例化一个LineItem对象时,需传入weight和price参数,由于这两个属性实现了描述符协议,所以关于weight和price的读值、取值或者删除值都可能关联到对应同名类属性Quantity实例中方法,由于Quantity类中只实现了__set__方法,所以这里读值和删除值不会触发Quantity实例中的方法
- Quantity为描述符类
- Quantity实例有个storage_name属性,这是托管实例中存储值的属性的名称
- 当我们要设置LineItem实例中的weight或者price属性,则会触发__set__方法,这个方法中self为描述符实例,即为LineItem类中的weight或price的Quantity实例,instance为托管类实例,即为LineItem实例,value是我们要设置的值,如果判断value大于0,则将其属性名和属性值设置到instance.__dict__字典里
现在让我们来测试这个类,我们故意将传入的price设为0:
truffle = LineItem('White truffle', 100, 0)
运行结果:
Traceback (most recent call last):
……
ValueError: value must be > 0
可以看到,在设置值得时候确实触发了__set__方法
另外还要重复声明一点:__set__方法中的参数,self和instance分别为描述符实例和托管类实例,instance代表要设置属性的那个对象,而self(描述符实例)则保存了要设置属性的属性名,在上个例子中,如果我们在__set__方法要设置LineItem实例只能用这样的方式:
instance.__dict__[self.storage_name] = value
如果尝试用setattr()方法来赋值
class Quantity: def __init__(self, storage_name):
self.storage_name = storage_name def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0')
测试:
truffle = LineItem('White truffle', 100, 10)
运行结果:
Traceback (most recent call last):
……
RecursionError: maximum recursion depth exceeded
我们会发现,如果用setattr()方法来赋值,会产生堆栈异常,为什么会这样呢?假设obj是LineItem实例,obj.price = 10和setattr(obj, "price", 10)一样,都会调用__set__方法,如果用setattr()方法来设置值,会不断调用__set__方法,最终产生堆栈异常
上面的例子,LineItem有个缺点,在托管类中每次实例化描述符时都要重复输入属性名,现在,让我们再改造一下LineItem类,使得不需要输入属性名。为了避免在描述符实例中重复输入属性名,我们将每个Quantity实例中的storage_name属性生成一个独一无二的字符串,同时为描述符类加上__get__方法
import uuid class Quantity: def __init__(self): # <1>
cls = self.__class__
prefix = cls.__name__
identity = str(uuid.uuid4())[:8]
self.storage_name = '_{}#{}'.format(prefix, identity) def __get__(self, instance, owner): # <2>
return getattr(instance, self.storage_name) def __set__(self, instance, value): # <3>
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0') class LineItem:
weight = 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
测试:
raisins = LineItem('Golden raisins', 10, 6.95)
print(raisins.weight, raisins.description, raisins.price)
运行结果:
10 Golden raisins 6.95
- 这里的Quantity描述符类在实例化时,我们不再要求需要传入一个storage_name了,而是在初始化方法中生成一个storage_name,这个storage_name由类名和uuid生成的随机字符串组成
- 我们知道,如果我们对一个实例中的属性赋值,如果这个属性名在类中定义为描述符实例,在赋值时会自动触发__set__方法,而__get__方法则是在我们读值的时候自动触发,__get__方法除了self(描述符实例)还会传入两个参数,instance和owner,instance是托管类实例,owner是托管类,在我们上面的例子instance即为LineItem的实例,owner即LineItem类,当读取实例中的一个属性,如果这个属性在类中定义为描述符实例,则会触发__get__方法
- 在__set__方法中,我们不再调用instance.__dict__[self.storage_name] = value的方式来赋值,而是直接使用setattr()方法来赋值。上一个例子中,我们测试了如果用setattr()方法来赋值的话会出现堆栈溢出的异常,那为什么我们这里又可以用了呢?是因为,我们真正存储属性值的时候,用的属性名并不是类的描述符名,而是由Python解释器生成一个Quantity_#_{uuid}随机字符串,而这个随机字符串,而这个字符串并未在类中注册为描述符实例,所以我们调用setattr(),不会再像之前那样产生堆栈异常
这里还有一点,当我们尝试打印一下LineItem.weight这个描述符实例
LineItem.weight
运行结果:
Traceback (most recent call last):
……
return getattr(instance, self.storage_name)
AttributeError: 'NoneType' object has no attribute '_Quantity#f9860e73'
我们会发现,访问LineItem.weight会抛出AttributeError异常,因为在访问LineItem.weight属性时,同样会调用__get__方法,这个时候instance传入的是一个None,为了解决这个问题,我们在__get__方法中检测,如果传入的instance为None,则返回当前描述符实例,如果instance不为None,则返回instance中的实例属性
import uuid class Quantity: def __init__(self):
cls = self.__class__
prefix = cls.__name__
identity = str(uuid.uuid4())[:8]
self.storage_name = '_{}#{}'.format(prefix, identity) 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')
这里我们修改另外一个章节Python动态属性和特性(二)中的quantity()特性工厂方法,使之不需要传入storage_name
import uuid def quantity():
storage_name = '_{}:{}'.format('quantity', str(uuid.uuid4())[:8]) def qty_getter(instance):
return instance.__dict__[storage_name] def qty_setter(instance, value):
if value > 0:
instance.__dict__[storage_name] = value
else:
raise ValueError('value must be > 0') return property(qty_getter, qty_setter) class LineItem:
weight = 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 raisins = LineItem('Golden raisins', 10, 6.95)
print(raisins.weight, raisins.description, raisins.price)
运行结果:
10 Golden raisins 6.95
现在,我们对比一下描述符类和特性工厂,两种方法都可以在对属性设值或读取时进行一些额外的操作,哪种更好呢?这里建议使用描述符类的方式,主要有两个原因:
- 描述符类可以使用子类扩展,若想重用工厂函数中的代码,除了复制黏贴,很难有其他的办法
- 使用函数属性和闭包保持状态相比,在类属性和实例属性中保持状态更易于理解
我们通过描述符类Quantity,在访问和设置LineItem托管实例的weight和price时进行额外的操作,现在,让我们更进一步,新增一个description描述符实例,对当要对LineItem实例的description属性进行设置和访问时,也增加一些操作。这里,我们要新增一个描述符类NotBlank,在设计NotBlank的过程中,我们发现它与Quantity描述符类很像,只是验证逻辑不同
回想Quantity的功能,我们注意到它做了两件不同的事,管理托管实例中的存储属性,以及验证用于设置那两个属性的值。由此可见,我们可以通过继承的方式,来复用描述符类,这里,我们创建两个基类:
- AutoStorage:自动管理储存属性的描述符类
- Validated:扩展 AutoStorage 类的抽象子类,覆盖 __set__ 方法,调用必须由子类实现的validate方法
稍后我们会重写Quantity类,并实现NotBlank类,使它继承Validated类,只编写validate方法,类之间的关系如图1-2:
图1-2
图1-2:几个描述符类的层次结构。AutoStorage基类负责自动存储属性;Validated类做验证,把职责委托给抽象方法validate;Quantity和NonBlank是Validated的具体子类。Validated、Quantity和NonBlank 三个类之间的关系体现了模板方法设计模式。
import abc
import uuid class AutoStorage: # <1> def __init__(self):
cls = self.__class__
prefix = cls.__name__
identity = str(uuid.uuid4())[:8]
self.storage_name = '_{}#{}'.format(prefix, identity) 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): # <2> def __set__(self, instance, value): # <3>
value = self.validate(instance, value)
super().__set__(instance, value) @abc.abstractmethod
def validate(self, instance, value): # <4>
"""return validated value or raise ValueError""" class Quantity(Validated):
"""a number greater than zero""" def validate(self, instance, value): # <5>
if value <= 0:
raise ValueError('value must be > 0')
return value class NotBlank(Validated):
"""a string with at least one non-space character""" def validate(self, instance, value): # <6>
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value
- AutoStorage类提供了之前Quantity描述符类的大部分功能
- Validated类是抽象类,不过也同时继承了AutoStorage类
- Validated类中重写__set__方法,先通过校验方法,再调用父类的__set__方法来存储值
- 抽象方法,具体实现由子类完成
- Quantity实现了父类Validated的validate方法,校验设置的值必须大于0
- NotBlank实现了父类Validated的validate方法,校验设置的值不能为空字符串
使用Quantity和NonBlank描述符的LineItem类
class LineItem:
description = NotBlank()
weight = 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
测试新的LineItem类
raisins = LineItem(' ', 10, 6.95)
运行结果:
Traceback (most recent call last):
……
ValueError: value cannot be empty or blank
Python属性描述符(一)的更多相关文章
- Python 属性描述符和属性的查找过程
属性描述符可以用来控制给属性赋值的时候的一些行为 import numbers class IntField: def __get__(self, instance, owner): return s ...
- python 属性描述符
import numbers class IntField: # 一个类只要实现了这个魔法函数,那么它就是属性描述符 #数据描述符 def __get__(self, instance, owner) ...
- Python属性描述符
实现了__get__.set.__delete__中任意一个方法的类,称之为属性描述符. 属性描述符可以控制属性操作时的一些行为. 只要具有__get__方法的类就是描述符类. 如果一个类中具有__g ...
- Python属性描述符(二)
Python存取属性的方式特别不对等,通过实例读取属性时,通常返回的是实例中定义的属性,但如果实例未曾定义过该属性,就会获取类属性,而为实例的属性赋值时,通常会在实例中创建属性,而不会影响到类本身.这 ...
- Python:高级主题之(属性取值和赋值过程、属性描述符、装饰器)
Python:高级主题之(属性取值和赋值过程.属性描述符.装饰器) 背景 学习了Javascript才知道原来属性的取值和赋值操作访问的“位置”可能不同.还有词法作用域这个东西,这也是我学习任何一门语 ...
- python之属性描述符与属性查找规则
描述符 import numbers class IntgerField: def __get__(self, isinstance, owner): print('获取age') return se ...
- python数据描述符
Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过google和阅读源码,现将自己的理解和心得记录下来,也为正在为了该问题苦恼的朋友提 ...
- 深入理解javascript对象系列第三篇——神秘的属性描述符
× 目录 [1]类型 [2]方法 [3]详述[4]状态 前面的话 对于操作系统中的文件,我们可以驾轻就熟将其设置为只读.隐藏.系统文件或普通文件.于对象来说,属性描述符提供类似的功能,用来描述对象的值 ...
- JavaScript 属性描述符
属性描述符(Property Descriptor)是 ES5 之后出现的概念,顾名思义,它用于描述属性应该是什么样,例如是否只读,能否枚举,能否可配置等.所有对象属性均可使用属性描述符来定义. 属性 ...
随机推荐
- 仙人掌(cactus)
题目描述LYK 在冲刺清华集训(THUSC)!于是它开始研究仙人掌,它想来和你一起分享它最近研究的结果.如果在一个无向连通图中任意一条边至多属于一个简单环(简单环的定义为每个点至多经过一次),且不存 ...
- 【转】LINQ to SQL语句(1)之Where
Where操作 适用场景:实现过滤,查询等功能. 说明:与SQL命令中的Where作用相似,都是起到范围限定也就是过滤作用的,而判断条件就是它后面所接的子句. Where操作包括3种形式,分别为简单形 ...
- swift学习笔记3-4
再牛逼的梦想,也抵不住你傻逼似的坚持! 我跑啊跑啊,为的就是赶上那个被寄予厚望的自己. 三.运算符+表达式 swift允许重载运算符,比如 “+”你可以重载它 后续会详细介绍 赋值运算符 pass 算 ...
- iOS 应用架构 (三)
iOS 客户端应用架构看似简单,但实际上要考虑的事情不少.本文作者将以系列文章的形式来回答 iOS 应用架构中的种种问题,本文是其中的第二篇,主要讲 View 层的组织和调用方案.下篇主要讨论做 Vi ...
- Linux环境下mysql的root密码忘记解决方法(2种)
方法一: 1.首先确认服务器出于安全的状态,也就是没有人能够任意地连接MySQL数据库. 因为在重新设置MySQL的root密码的期间,MySQL数据库完全出于没有密码保护的 状态下,其他的用户也可以 ...
- [Git]使用Git上传本地项目,并同步到Github上
第一步:先要在github.com中创建一个仓库(New Repository). 第二步,打开Git Bash ① git init [+仓库名]:初始化仓库,执行之后可以在指定的仓库存放地上面看到 ...
- python基础教程总结12——数据库
1. Python 数据库 API 很多支持SQL标准的数据库在Python中都有对应的客户端模块.为了在提供相同功能(基本相同)的不同模块之间进行切换(兼容),Python 规定了一个标准的 DB ...
- ABAP和Java的单元测试Unit Test
ABAP ABAP class单元测试的执行入口,CLASS_SETUP, 是硬编码在单元测试框架实现CL_AUNIT_TEST_CLASS里的. 待执行的单元测试方法通过CL_AUNIT_TEST_ ...
- [web开发] Vue + spring boot + echart 微博爬虫展示平台
1.微博登录 2.爬取数据 3.mysql存储 4.pyechart本地展示 5.用vue搭建网站web展示 先放图: 1.微博登录 新浪微博的登录不是简单的post就能解决的,他的登录有加密,所以我 ...
- PAT (Basic Level) Practise (中文)-1019. 数字黑洞 (20)
http://www.patest.cn/contests/pat-b-practise/1019 给定任一个各位数字不完全相同的4位正整数,如果我们先把4个数字按非递增排序,再按非递减排序,然后用第 ...