Python中的类与对象之描述符详解


Posted in Python onMarch 27, 2015

描述符(Descriptors)是Python语言中一个深奥但却重要的一部分。它们广泛应用于Python语言的内核,熟练掌握描述符将会为Python程序员的工具箱添加一个额外的技巧。为了给接下来对描述符的讨论做一些铺垫,我将描述一些程序员可能会在日常编程活动中遇到的场景,然后我将解释描述符是什么,以及它们如何为这些场景提供优雅的解决方案。在这篇总结中,我会使用新样式类来指代Python版本。

1、假设一个程序中,我们需要对一个对象属性执行严格的类型检查。然而,Python是一种动态语言,所以并不支持类型检查,但是这并不妨碍我们实现自己版本,且较为初级的类型检查。对象属性类型检查的传统方法可能采用下面的方式:

def __init__(self, name, age):
 if isinstance(str, name):
 self.name = name
 else:
 raise TypeError("Must be a string")
 if isinstance(int, age):
 self.age = age
 else:
 raise TypeError("Must be an int")

上面是执行这种类型检查的一种方法,但是参数数量增加时它将变得比较繁琐。另外,在赋值之前,我们可以创建一个在__init__中调用的type_check(type, val)函数,但是当我们想在其他地方设置属性值时,该如何简单地实现这种检查呢。我想到的一个快速解决方案是Java中的getters和setters,但是这并不符合Python风格,并且比较麻烦。

2、假设在一个程序中,我们想创建一些在运行时立刻初始化然后变成只读的属性。有人也能想到利用Python中的特殊方法来实现,但这种实现方法仍旧是笨拙和繁琐的。

3、最后,设想一个程序中,我们希望以某种方式自定义对象属性的访问。例如需要记录这种属性的访问。同样的,还是可以想到一个解决方法,即使这种解决方案可能比较笨重并且不可复用。

上述问题因都与属性引用相关而全部联系在了一起。下面,我们将尝试自定义属性的访问方法。
Python描述符

针对上面所列的问题,描述符提供了优雅、简洁、健壮和可重用的解决方案。简而言之,一个描述符就是一个对象,该对象代表了一个属性的值。这就意味着如果一个账户对象有一个属性“name”,那么描述符就是另一个能够用来代表属性“name”持有值的对象。描述符协议中“定义了__get__”、“__set__”或”__delete__” 这些特殊方法,描述符是实现其中一个或多个方法的对象。这些方法中每一种方法的签名如下所示:

python descr.get(self,obj,type=None)->value。
 
descr.__set__(self, obj, value) --> None
 
descr.__delete__(self, obj) --> None

实现__get__方法的对象是非数据描述符,意味着在初始化之后它们只能被读取。而同时实现__get__和__set__的对象是数据描述符,意味着这种属性是可写的。

为了更好地理解描述符,我们给出针对上述问题基于描述符的解决方法。使用Python描述符实现对象属性的类型检查将是一个非常简单的任务。装饰器实现这种类型检查的代码如下所示:

class TypedProperty(object):
 
 def __init__(self, name, type, default=None):
 self.name = "_" + name
 self.type = type
 self.default = default if default else type()
 
 def __get__(self, instance, cls):
 return getattr(instance, self.name, self.default)
 
 def __set__(self,instance,value):
 if not isinstance(value,self.type):
 raise TypeError("Must be a %s" % self.type)
 setattr(instance,self.name,value)
 
 def __delete__(self,instance):
 raise AttributeError("Can't delete attribute")
 
class Foo(object):
 name = TypedProperty("name",str)
 num = TypedProperty("num",int,42)
 
>> acct = Foo()
>> acct.name = "obi"
>> acct.num = 1234
>> print acct.num
1234
>> print acct.name
obi
# trying to assign a string to number fails
>> acct.num = '1234'
TypeError: Must be a <type 'int'>

在这个例子中,我们实现了一个描述符TypedProperty,并且这个描述符类会对它所代表的类的任何属性执行类型检查。注意到这一点很重要,即描述符只能在类级别进行合法定义,而不能在实例级别定义。例如,在上面例子中的__init__方法里。

当访问类Foo实例的任何属性时,描述符会调用它的__get__方法。需要注意的是,__get__方法的第一个参数是描述符代表的属性被引用的源对象。当属性被分配时,描述符会调用它的__set__方法。为了理解为什么可以使用描述符代表对象属性,我们需要理解Python中属性引用解析的执行方式。对于对象来说,属性解析机制在object.__getattribute__()中。该方法将b.x转换成type(b).__dict__['x'].__get__(b, type(b))。然后,解析机制使用优先级链搜索属性,在优先级链中,类字典中发现的数据描述符的优先级高于实例变量,实例变量优先级高于非数据描述符,如果提供了getattr(),优先级链会为getattr()分配最低优先级。对于一个给定的对象类,可以通过自定义__getattribute__方法来重写优先级链。

深刻理解优先级链之后,就很容易想出针对前面提出的第二个和第三个问题的优雅解决方案了。那就是,利用描述符实现一个只读属性将变成实现数据描述符这个简单的情况了,即不带__set__方法的描述符。尽管在本例中不重要,定义访问方式的问题只需要在__get__和__set__方法中增加所需的功能即可。
类属性

每次我们想使用描述符的时候都不得不定义描述符类,这样看起来非常繁琐。Python特性提供了一种简洁的方式用来向属性增加数据描述符。一个属性签名如下所示:
 

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

fget、fset和fdel分别是类的getter、setter和deleter方法。我们通过下面的一个示例来说明如何创建属性:

class Accout(object):
 def __init__(self):
 self._acct_num = None
 
 def get_acct_num(self):
 return self._acct_num
 
 def set_acct_num(self, value):
 self._acct_num = value
 
 def del_acct_num(self):
 del self._acct_num
 
 acct_num = property(get_acct_num, set_acct_num, del_acct_num, "Account number property.")

如果acct是Account的一个实例,acct.acct_num将会调用getter,acct.acct_num = value将调用setter,del acct_num.acct_num将调用deleter。

在Python中,属性对象和功能可以像《描述符指南》中说明的那样使用描述符协议来实现,如下所示:

class Property(object):
 "Emulate PyProperty_Type() in Objects/descrobject.c"
 
 def __init__(self, fget=None, fset=None, fdel=None, doc=None):
 self.fget = fget
 self.fset = fset
 self.fdel = fdel
 if doc is None and fget is not None:
 doc = fget.__doc__
 self.__doc__ = doc
 
 def __get__(self, obj, objtype=None):
 if obj is None:
 return self
 if self.fget is None:
 raise AttributeError("unreadable attribute")
 return self.fget(obj)
 
 def __set__(self, obj, value):
 if self.fset is None:
 raise AttributeError("can't set attribute")
 self.fset(obj, value)
 
 def __delete__(self, obj):
 if self.fdel is None:
 raise AttributeError("can't delete attribute")
 self.fdel(obj)
 
 def getter(self, fget):
 return type(self)(fget, self.fset, self.fdel, self.__doc__)
 
 def setter(self, fset):
 return type(self)(self.fget, fset, self.fdel, self.__doc__)
 
 def deleter(self, fdel):
 return type(self)(self.fget, self.fset, fdel, self.__doc__)

Python也提供了@ property装饰器,可以用它来创建只读属性。一个属性对象拥有getter、setter和deleter装饰器方法,可以使用它们通过对应的被装饰函数的accessor函数创建属性的拷贝。下面的例子最好地解释了这一点:

class C(object):
 def __init__(self):
 self._x = None
 
 @property
 # the x property. the decorator creates a read-only property
 def x(self):
 return self._x
 
 @x.setter
 # the x property setter makes the property writeable
 def x(self, value):
 self._x = value
 
 @x.deleter
 def x(self):
 del self._x

如果我们想让属性只读,那么我们可以去掉setter方法。

在Python语言中,描述符有着广泛的应用。Python函数、类方法、静态方法都是非数据描述符的例子。针对列举的Python对象是如何使用描述符实现的问题,《描述符指南》给出了一个基本的描述。

Python 相关文章推荐
python实现绘制树枝简单示例
Jul 24 Python
python中self原理实例分析
Apr 30 Python
解决python3在anaconda下安装caffe失败的问题
Jun 15 Python
python入门教程 python入门神图一张
Mar 05 Python
使用Python和xlwt向Excel文件中写入中文的实例
Apr 21 Python
使用pandas模块读取csv文件和excel表格,并用matplotlib画图的方法
Jun 22 Python
Django csrf 两种方法设置form的实例
Feb 03 Python
python变量的存储原理详解
Jul 10 Python
Python 实现自动导入缺失的库
Oct 29 Python
浅谈pytorch中torch.max和F.softmax函数的维度解释
Jun 28 Python
python 爬虫基本使用——统计杭电oj题目正确率并排序
Oct 26 Python
python读取excel数据并且画图的实现示例
Feb 08 Python
深入理解Javascript中的this关键字
Mar 27 #Python
Python运用于数据分析的简单教程
Mar 27 #Python
Python中下划线的使用方法
Mar 27 #Python
利用Python和OpenCV库将URL转换为OpenCV格式的方法
Mar 27 #Python
python根据出生年份简单计算生肖的方法
Mar 27 #Python
python实现根据月份和日期得到星座的方法
Mar 27 #Python
python根据给定文件返回文件名和扩展名的方法
Mar 27 #Python
You might like
PHP常用设计模式之委托设计模式
2016/02/13 PHP
thinkPHP3.x常量整理(预定义常量/路径常量/系统常量)
2016/05/20 PHP
LaravelS通过Swoole加速Laravel/Lumen详解
2018/03/02 PHP
jQuery select控制插件
2009/08/17 Javascript
学习ExtJS 访问容器对象
2009/10/07 Javascript
jQuery 对Select的操作备忘记录
2011/07/04 Javascript
extjs 04_grid 单击事件新发现
2012/11/27 Javascript
jcrop基本参数一览
2013/07/16 Javascript
利用js实现前台动态添加文本框,后台获取文本框内容(示例代码)
2013/11/25 Javascript
JavaScript实现关键字高亮功能
2014/11/12 Javascript
jQuery实现仿QQ在线客服效果的滚动层代码
2015/10/15 Javascript
JQuery validate插件验证用户注册信息
2016/05/11 Javascript
JavaScript实战之菜单特效
2016/08/16 Javascript
AngularJS模糊查询功能实现代码(过滤内容下拉菜单排序过滤敏感字符验证判断后添加表格信息)
2017/10/24 Javascript
详解react-native WebView 返回处理(非回调方法可解决)
2018/02/27 Javascript
p5.js入门教程之平滑过渡(Easing)
2018/03/16 Javascript
Angular5升级RxJS到5.5.3报错:EmptyError: no elements in sequence的解决方法
2018/04/09 Javascript
使用Angular自定义字段校验指令的方法示例
2019/02/01 Javascript
微信小程序复选框实现多选一功能过程解析
2020/02/14 Javascript
vue+openlayers绘制省市边界线
2020/12/24 Vue.js
[01:32]寻找你心中的那团火 DOTA2 TI9火焰传递活动今日开启
2019/05/16 DOTA
[06:07]DOTA2-DPC中国联赛 正赛 Ehome vs VG 选手采访
2021/03/11 DOTA
python连接池实现示例程序
2013/11/26 Python
Python 给屏幕打印信息加上颜色的实现方法
2019/04/24 Python
基于Django框架的权限组件rbac实例讲解
2019/08/31 Python
Python IDLE或shell中切换路径的操作
2020/03/09 Python
使用CSS3的appearance属性改变任何元素的浏览器默认风格
2012/12/24 HTML / CSS
浅析与CSS3的loading动画加载相关的transition优化
2015/05/18 HTML / CSS
css3实现圆锥渐变conic-gradient效果
2020/02/12 HTML / CSS
Canvas 文字碰撞检测并抽稀的方法
2019/05/27 HTML / CSS
学前教育教师求职自荐信
2013/09/22 职场文书
写给保洁员表扬信
2014/01/08 职场文书
人事文员岗位职责
2015/02/04 职场文书
领导视察通讯稿
2015/07/18 职场文书
致运动员加油稿
2015/07/21 职场文书
中学团支部工作总结
2015/08/13 职场文书