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 10 Python
Python编程中的文件读写及相关的文件对象方法讲解
Jan 19 Python
Python中使用urllib2模块编写爬虫的简单上手示例
Jan 20 Python
请不要重复犯我在学习Python和Linux系统上的错误
Dec 12 Python
详解python脚本自动生成需要文件实例代码
Feb 04 Python
Python 自动刷博客浏览量实例代码
Jun 14 Python
关于Django外键赋值问题详解
Aug 13 Python
pytorch 更改预训练模型网络结构的方法
Aug 19 Python
基于Python解密仿射密码
Oct 21 Python
利用python中集合的唯一性实现去重
Feb 11 Python
解决pyPdf和pyPdf2在合并pdf时出现异常的问题
Apr 03 Python
Pycharm在指定目录下生成文件和删除文件的实现
Dec 28 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图片上传程序
2008/03/27 PHP
PHPWind与Discuz截取字符函数substrs与cutstr性能比较
2011/12/05 PHP
ThinkPHP3.1新特性之命名范围的使用
2014/06/19 PHP
php类常量用法实例分析
2015/07/09 PHP
PHP Socket网络操作类定义与用法示例
2017/08/30 PHP
javascript面向对象包装类Class封装类库剖析
2013/01/24 Javascript
jquery实现输入框动态增减的实例代码
2013/07/14 Javascript
jquery实现将获取的颜色值转换为十六进制形式的方法
2014/12/20 Javascript
javascript排序函数实现数字排序
2015/06/26 Javascript
D3.js实现饼状图的方法详解
2016/09/21 Javascript
jQuery 常见小例汇总
2016/12/14 Javascript
深入理解vue2.0路由如何配置问题
2017/07/18 Javascript
浅谈angularJS的$watch失效问题的解决方案
2017/08/11 Javascript
在页面中引入js的两种方法(推荐)
2017/08/29 Javascript
老生常谈JavaScript获取CSS样式的方法(兼容各浏览器)
2018/09/19 Javascript
jQuery 查找元素操作实例小结
2019/10/02 jQuery
[42:39]老党炸弹人试玩视频
2014/09/03 DOTA
[01:56]无止竞 再出发——中国军团出征2017年DOTA2国际邀请赛
2017/07/05 DOTA
python实现代码行数统计示例分享
2014/02/10 Python
火车票抢票python代码公开揭秘!
2018/03/08 Python
python tkinter实现界面切换的示例代码
2019/06/14 Python
Django实现基于类的分页功能
2019/10/31 Python
python小项目之五子棋游戏
2019/12/26 Python
django实现HttpResponse返回json数据为中文
2020/03/27 Python
西班牙第一的网上药房:PromoFarma.com
2017/04/17 全球购物
3个CCIE对一个工程师的面试题
2012/05/06 面试题
小学美术教学反思
2014/02/01 职场文书
温馨提示标语
2014/06/26 职场文书
行政撤诉申请书
2015/05/18 职场文书
负责培养人意见
2015/06/05 职场文书
奠基仪式致辞
2015/07/30 职场文书
入党宣誓大会后的感想
2015/08/10 职场文书
2016年庆“七一”主题党日活动总结
2016/04/05 职场文书
golang中实现给gif、png、jpeg图片添加文字水印
2021/04/26 Golang
端午节将至,用Python爬取粽子数据并可视化,看看网友喜欢哪种粽子吧!
2021/06/11 Python
Python turtle实现贪吃蛇游戏
2021/06/18 Python