前言

最近使用描述符对自己的催化动力学模拟程序进行了改进,在Python描述符的帮助下实现了更加灵活而且强大有效的属性管理,使得程序各个组件的数据封装更加完善管理也更加有条理。

本文就以自己程序中运用描述符来进行有效的python属性管理为例子,介绍python中描述符的概念以及如何更好的使用描述符这个强有力的工具帮助我们有效管理python程序中的数据访问控制。

正文

在其他语言中我们经常会在类中实现gettersetter等工具方法,这样有助于定义类的接口,也使得开发者能够方便的封装功能,例如验证用法或者进行取值范围的检测。但是在Python中我们一般都是直接从public属性写起,但是当我们对属性有特殊需求,例如进行类型验证(Python是动态类型),数值范围检测,返回深复制(而不是引用)的时候,我们一般会考虑使用:

  1. 内建的@property装饰器

  2. 使用描述符

@property装饰器使用起来方便快捷,例如

class KineticModel(obejct):# ...@propertydef kB(self):return self.__kB@property.setterdef kB(self, kB):#...

但是@property的缺点就在于他无法被复用,同一套逻辑不能在不同的属性之间重复使用,这样除了对波尔兹曼常数进行处理外如果还有普朗克常数需要做同样的处理,难道要重复写一次settergetter函数?这显然已经"Repeat yourself"了。

这时候就要召唤Python的描述符机制了,他的存在是python开发者能够复用与属性相关的逻辑。

描述符协议

Python描述符协议是一种再模型中引用属性时将要发生事件的方法。Python会对属性的访问操作进行一定的转译,这种转译的方式就是由描述符协议确定的。借助Python提供给我们的描述符协议,我们就可以用来以Python的方式实现与私有变量类似的功能。

描述符协议包括几个方法:

- descr.__get__(self, obj, type=None) --> value, 用于访问属性
- descr.__set__(self, obj, value) --> None, 用于设置属性的值
- descr.__delete__(self, obj) --> None, 控制属性的删除操作

任何对象如果定义了上面的任何一个方法便实现了描述符协议,也就变成了一个描述符了。我们通过将之前的gettersetter方法中的逻辑重写到__get____set__方法中,便可以把同一套逻辑运用在不同类中不同的属性上面了。

创建描述符

这里只介绍使用类方法创建描述符。

我的动力学模型中的KineticModel需要很多类属性,例如基本的基元反应式rxn_expression(这里我用了一个包含string的list来表示)、模型反应发生的温度temperature(用一个float类型表示)。

为了能够在对属性进行赋值的时候进行相应的类型检测,我就定义了几个基本类型的描述符,提供了检测数据类型的相应逻辑,下面是个简单的整型描述符(当然这不是最后的使用的版本):

class Float(object):def __init__(self, name):self.name = namedef __get__(self):private_name = "_{}__{}".format(instance.__class__.__name__, self.name)if private_name not in instance.__dict__:instance.__dict__[private_name] = self.defaultdef __set__(self, instance, value):# 检测类型if type(value) is not float:msg = "{} ({}) is not a float number".format(self.name, value)raise ValueError(msg)# 将对象的相应属性进行赋值,注意这里我使用了`mangled name`用来进行私有化处理private_name = "_{}__{}".format(instance.__class__.__name__, self.name)instance.__dict__[private_name] = value

这样我们就可以在我们类中相应的类属性定义成相应的描述符对象,后面我们就可以像使用正常属性一样使用他,但是他却拥有了类型检测功能:

...class KineticModel(obejct):# 设置temperature为类属性temperature = Float("temperature")...

当我试图向其赋值一个字符串时,便会抛出异常:

描述符的原理

上面进行了基本的描述符创建和使用效果,那么描述符是如何工作的才能让我们以这种方式操作属性呢?

一句话总结就是通过将属性访问进行了转译。

描述符触发

当我们进行属性访问时便会触发描述符(如果这个属性具有描述符定义的时候),当我们对对象obj的属性d进行访问时候,obj.d,描述符的触发过程大致:先在对象obj的字典中寻找d,如果d是个含有__get__()的对象,则直接调用d.__get__(obj).

官方文档中对具体的触发细节进行了更详细的描述,具体的触发又分为我们访问的是类属性还是实例属性: 
1. 如果是对实例属性进行访问,则属性访问转译的关键就在于基类object__getattribute__方法,我们知道这个内置方法是在进行属性访问的时候无条件调用的,因此这个方法中将obj.d转译成了type(obj).__dict__['d'].__get__(obj, type(obj)) 
其实现的C代码参见:https://docs.python.org/3/c-api/object.html#c.PyObject_GenericGetAttr

  1. 如果是对类对象的属性进行访问,则属性的访问转译关键在于元类type__getattribute__方法,它将cls.d转译成cls.__dict__['d'].__get__(None, cls),这里__get__()instance没有也就是相应的None了。 
    其实现的C代码参见:https://hg.python.org/cpython/file/3.5/Objects/typeobject.c#l2936

描述符优先级

首先,描述符和描述符之间也是有区别的: 
1. 如果一个对象同时定义了__get__()__set__()方法,则这个描述符被称为data descriptor 
2. 如果一个对象只定义了__get__()方法,则这个描述符被称为non-data descriptor

我们对属性进行访问的时候需要几行打交道的基本上包含这几个对象:
1. data descriptor 
2. non-data descriptor 
3. 实例的字典 
4. 内置的__getattr__()函数

他们几个的优先级顺序是:

data descriptor >> instance's dict >> non-data descriptor >> __getattr__()

也就是说如果实例obj重现了同名的data descriptor d 和 实例属性d, 当我们访问d的时候,由于data descriptor具有更高的优先级,python便会调用type(obj).__dict__['d'].__get__(obj, type(obj))而不是返回obj.__dict__['d']

但是如果描述符是个non-data descriptor,则正好相反,python会返回obj.__dict__['d'] 

描述符实现惰性访问(按需访问)

很多时候一个类的属性,我们并不需要在这个类初始化的时候就进行初始化,我们可以在第一次使用这个属性的时候顺便将这个属性初始化,这样在后面重复使用这个属性的时候便直接返回结果就可以了,这样既可以减少计算的次数,也在一定程度上减少了内存的需求。

因此我在定义自己的描述符__get__()的时候进行了判断是否该相应的实例属性已经初始化,若未初始化则进行初始化,若已初始化直接返回,达到了惰性访问的目的:

def __get__(self, instance, owner):private_name = "_{}__{}".format(instance.__class__.__name__, self.name)# 是否实例属性已存在if private_name not in instance.__dict__:instance.__dict__[private_name] = self.defaultreturn instance.__dict__[private_name]

创建只读描述符

当我们想让一个属性(描述符)禁止调用者进行修改的时候,可以通过在__set__()方法中抛出AttributeError异常来实现,例如:

def __set__(self, instance, value):private_name = "_{}__{}".format(instance.__class__.__name__, self.name)# 在第一次赋值后便无法修改属性的值if private_name not in instance.__dict__:instance.__dict__[private_name] = valueelse:msg ="Changing value of {}.{} is not allowed".format(instance.__class__.__name__,self.name)raise AttributeError(msg)

这样便实现了私有变量的效果,可以将数据更安全的进行封装,防止在外部调用的时候意外修改了对象的数据造成不想要的结果。

对于mutable的变量可以使用深复制

如果实例属性是字典或者列表这类的变量,python都会返回对象的引用,因此在获取其值以后也是有可能修改其内部数据的,因此如果真的想要是这个属性不被做任何的修改,可以使用deepcopy直接返回对象的深复制,这样在外部无论怎么蹂躏这个对象,都跟返回他的对象本身没有关系了。

def __get__(self, instance, owner):private_name = "_{}__{}".format(instance.__class__.__name__, self.name)if private_name not in instance.__dict__:instance.__dict__[private_name] = self.defaultif self.deepcopy:return copy.deepcopy(instance.__dict__[private_name])else:return instance.__dict__[private_name]

需要注意的点

描述符都是针对类属性,因此如果把数据存放在描述符对象中的时候,会出现意想不到的结果。 
例如我想针对每个学生类创建对应的身高描述符,而且把身高数据放在描述符中,我可以这样定义描述符:

我们创建了两个学生实例,但是身高属性却是同一个对象,这是因为描述符是类属性,因此每个实例中进行访问的时候都是访问的类属性的

引用。

这个时候我们可以不把数据放在描述符中,而是在相应的实例对象中创建私有变量,这样不同的对象的私有变量是不同的变量,便不会出想上图的问题。

class Height(object):def __init__(self, name):self.name = namedef __get__(self, instance, cls):return getattr(instance, self.name)def __set__(self, instance, value):setattr(instance, self.name, value)

同时也可以将相应的对象和值以字典的键值对存到描述符的字典中,但是这样会造成引用计数无法为0导致无法进行垃圾回收从而导致内存泄漏的风险,因此这个方法就不详细描述了。

总结

本文总结了Python中的描述符相关的概念和使用,描述符可以帮助我们实现强大而灵活的属性管理,通过结合使用描述符可以实现优雅的编程,但是同时也应该保持谨慎的态度,避免由于覆盖普通对象行为而产生不必要的代码复杂性。

原文发布时间为:2016-12-11

本文作者:Pytlab

本文来自云栖社区合作伙伴“Python中文社区”,了解相关信息可以关注“Python中文社区”微信公众号

有效的python属性管理:描述符的使用相关推荐

  1. python中文件描述符_Python中的描述符

    python中文件描述符 In Python, a class that implements a get, set or delete methods for an object is called ...

  2. python100例详解-几个小例子给你讲解Python中类的描述符

    原标题:几个小例子给你讲解Python中类的描述符 学习 Python 这么久了,说起 Python 的优雅之处,能让我脱口而出的, Deor(描述符)特性可以排得上号. 描述符是Python 语言独 ...

  3. python 将实例用作属性_将类实例用作类属性、描述符和属性

    首先,你学习新式课程很好.他们有很多优势. 在Python中创建属性的现代方法是:class Collection(object): def __init__(self): self._profile ...

  4. python如何计算分子描述符_Python——描述符(descriptor)解密

    本文由 极客范 - 慕容老匹夫 翻译自 Chris Beaumont.欢迎加入极客翻译小组,同我们一道翻译与分享.转载请参见文章末尾处的要求. Python中包含了许多内建的语言特性,它们使得代码简洁 ...

  5. python属性管理(1):基础

    管理属性的几种方式 在python中访问.设置.删除对象属性的时候,有以下几种方式: 使用内置函数getattr().setattr()和delattr() 自己编写getter().setter() ...

  6. 外网访问arm嵌入式linux_嵌入式Linux系统编程——文件读写访问、属性、描述符、API

    Linux 的文件模型是从 Unix 的继承而来,所以 Linux 继承了 UNIX 本身的大部分特性,然后加以扩展,本章从 UNIX 系统接口来描述 Linux 系统结构的特性. 操作系统是通过一系 ...

  7. python 将文件描述符包装成文件对象

    有一个对应于操作系统上一个已打开的I/O 通道(比如文件.管道.套接字等)的整型文件描述符,你想将它包装成一个更高层的Python 文件对象. 一个文件描述符和一个打开的普通文件是不一样的.文件描述符 ...

  8. 4. linux调用文件计算阶乘前5项和_嵌入式Linux系统编程——文件读写访问、属性、描述符、API

    Linux 的文件模型是从 Unix 的继承而来,所以 Linux 继承了 UNIX 本身的大部分特性,然后加以扩展,本章从 UNIX 系统接口来描述 Linux 系统结构的特性. 操作系统是通过一系 ...

  9. python 属性描述符

    文章目录 1. 描述符示例:验证属性 2. 自动获取储存属性的名称 3. 继承改进 4. 覆盖型与非覆盖型描述符对比 4.1 覆盖型描述符 4.2 没有 `__get__` 方法的覆盖型描述符 4.3 ...

最新文章

  1. 2020ICPC·小米 网络选拔赛第一场(D. Router Mesh)
  2. 秀秀的森林(forest)
  3. 添加虚拟主机 php,给 phpstudy 添加虚拟主机
  4. opencv:图像读取BGR变成RGB
  5. linux模拟网络延迟,使用Nistnet搭建网络延迟模拟设备 (network delay simulator)
  6. fast-rcnn win10 tensorflow部署
  7. C#LeetCode刷题-树状数组
  8. Android学习笔记05---项目的目录结构与安装及启动过程分析
  9. 【网络】HTTPS 怎么保证数据传输的安全性
  10. NA/NP/IE最好的模拟器GNS3 part2
  11. Zookeeper C API 指南五(同步 API 介绍)
  12. DataSetProvider的Option属性
  13. 投简历:求求你给我个机会「小废物招聘全记录①」
  14. R语言:作业二(矩阵:求行列式、逆矩阵、特征值、特征向量)【补充:矩阵相关运算知识】
  15. 操作系统和各类编辑器的豆沙绿设置
  16. Apple(苹果)忘记安全问题答案怎么办?
  17. ESP32和LoRa SX1278通讯
  18. POI实现EXCEL导出(resources配置路径下或者网络图片)
  19. 支付宝h5网页的支付
  20. 卡巴斯基杀毒软件被曝出用户上网痕迹泄露漏洞

热门文章

  1. 卷积神经网络的全连接层转换成卷积层
  2. History命令的显示带时间
  3. win7 linux win7 无法启动,ubuntu 和win7双系统安装后win7无法启动解决办法
  4. 计算机音乐叫什么名字,电脑开机那段美妙的音乐叫什么名字?
  5. android 7和苹果手机,苹果对比安卓,7个iPhone比安卓手机更好的理由
  6. 广日电梯主板灯说明_日立电梯HGP主板说明和电子部件识别
  7. linux 默认组,系统自动创建的默认安全组和自己创建的安全组的默认规则
  8. 计算机专业英语职高 试卷,职高对口高考英语模拟考试题.doc
  9. 怎样在接口地址中添加请求头参数_jmeter5.3做soap接口性能测试配置
  10. java对象的状态由什么表示_持久化层的Java对象可处于哪些状态?这些状态有哪些特征?...