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 04 Python
python生成随机mac地址的方法
Mar 16 Python
python字典DICT类型合并详解
Aug 17 Python
Python 2.x如何设置命令执行的超时时间实例
Oct 19 Python
python使用正则表达式的search()函数实现指定位置搜索功能
Nov 10 Python
让Python更加充分的使用Sqlite3
Dec 11 Python
将TensorFlow的模型网络导出为单个文件的方法
Apr 23 Python
Python基于opencv调用摄像头获取个人图片的实现方法
Feb 21 Python
Python实现基于socket的udp传输与接收功能详解
Nov 15 Python
Python面向对象程序设计之继承、多态原理与用法详解
Mar 23 Python
Jupyter Notebook输出矢量图实例
Apr 14 Python
Python检测端口IP字符串是否合法
Jun 05 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读取xml实例代码
2010/01/28 PHP
使用php方法curl抓取AJAX异步内容思路分析及代码分享
2014/08/25 PHP
轻松掌握php设计模式之访问者模式
2016/09/23 PHP
Zend Framework校验器Zend_Validate用法详解
2016/12/09 PHP
浅谈PHP中new self()和new static()的区别
2017/08/11 PHP
laravel框架使用FormRequest进行表单验证,验证异常返回JSON操作示例
2020/02/18 PHP
完美解决JS中汉字显示乱码问题(已解决)
2006/12/27 Javascript
Mootools 1.2教程 选项卡效果(Tabs)
2009/09/15 Javascript
jQuery操作cookie方法实例教程
2014/11/25 Javascript
jQuery中:submit选择器用法实例
2015/01/03 Javascript
javascript巧用eval函数组装表单输入项为json对象的方法
2015/11/25 Javascript
js格式化时间的方法
2015/12/18 Javascript
jquery点击改变class并toggle的实现代码
2016/05/15 Javascript
利用jQuery实现打字机字幕效果实例代码
2016/09/02 Javascript
prototype与__proto__区别详细介绍
2017/01/09 Javascript
jQuery的中 is(':visible') 解析及用法(必看)
2017/02/12 Javascript
jQuery实现的背景颜色渐变动画效果示例
2017/03/24 jQuery
JS计算距当前时间的时间差实例
2017/12/29 Javascript
浅谈mvvm-simple双向绑定简单实现
2018/04/18 Javascript
JS实现读取xml内容并输出到div中的方法示例
2018/04/19 Javascript
浅谈React碰到v-if
2018/11/04 Javascript
JavaScript键盘事件常见用法实例分析
2019/01/03 Javascript
vue-router懒加载的3种方式汇总
2021/02/28 Vue.js
[04:11]2014DOTA2国际邀请赛 CIS遗憾出局梦想不灭
2014/07/09 DOTA
Python装饰器知识点补充
2018/05/28 Python
python 使用 requests 模块发送http请求 的方法
2018/12/09 Python
Python 使用Numpy对矩阵进行转置的方法
2019/01/28 Python
Django REST framework 分页的实现代码
2019/06/19 Python
python socket 聊天室实例代码详解
2019/11/14 Python
JustFab加拿大:女鞋、靴子、手袋和服装在线
2018/05/18 全球购物
亚马逊巴西站:Amazon.com.br
2019/09/22 全球购物
J2EE中常用的名词进行解释
2015/11/09 面试题
社区志愿者培训方案
2014/06/10 职场文书
乡镇党的群众路线教育实践活动剖析材料
2014/10/09 职场文书
matplotlib之pyplot模块实现添加子图subplot的使用
2021/04/25 Python
教你使用RustDesk 搭建一个自己的远程桌面中继服务器
2022/08/14 Servers