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中用于转换字母为小写的lower()方法使用简介
May 19 Python
Python中的id()函数指的什么
Oct 17 Python
解决pandas无法在pycharm中使用plot()方法显示图像的问题
May 24 Python
Python图像处理之图像的读取、显示与保存操作【测试可用】
Jan 04 Python
在python里协程使用同步锁Lock的实例
Feb 19 Python
Python Django Vue 项目创建过程详解
Jul 29 Python
与Django结合利用模型对上传图片预测的实例详解
Aug 07 Python
python实现监控阿里云账户余额功能
Dec 16 Python
pycharm 2019 最新激活方式(pycharm破解、激活)
Sep 22 Python
浅谈Pytorch中的自动求导函数backward()所需参数的含义
Feb 29 Python
详解anaconda离线安装pytorchGPU版
Sep 08 Python
python树莓派通过队列实现进程交互的程序分析
Jul 04 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
超强分页类2.0发布,支持自定义风格,默认4种显示模式
2007/01/02 PHP
MySQL数据库转移,access,sql server 转 MySQL 的图文教程
2007/09/02 PHP
jquery+php实现导出datatables插件数据到excel的方法
2015/07/06 PHP
Linux+Nginx+MySQL下配置论坛程序Discuz的基本教程
2015/12/23 PHP
浅析hasOwnProperty方法的应用
2013/11/20 Javascript
解析JSON对象与字符串之间的相互转换
2013/12/18 Javascript
利用jquery动画特效和css打造的侧边弹出垂直导航
2014/04/04 Javascript
跟我学习javascript的全局变量
2015/11/16 Javascript
Bootstrap自动适应PC、平板、手机的Bootstrap栅格系统
2016/05/27 Javascript
如何给ss bash 写一个 WEB 端查看流量的页面
2017/03/23 Javascript
Vue.js简易安装和快速入门(第二课)
2017/10/17 Javascript
react 父子组件之间通讯props
2018/09/08 Javascript
Vue表单控件绑定图文详解
2019/02/11 Javascript
nodejs和react实现即时通讯简易聊天室功能
2019/08/21 NodeJs
vue动态路由:路由参数改变,视图不更新问题的解决
2019/11/05 Javascript
vue基于v-charts封装双向条形图的实现代码
2019/12/09 Javascript
解决vant title-active-color与title-inactive-color不生效问题
2020/11/03 Javascript
python判断给定的字符串是否是有效日期的方法
2015/05/13 Python
python中abs&amp;map&amp;reduce简介
2018/02/20 Python
Python3.5内置模块之time与datetime模块用法实例分析
2019/04/27 Python
Python使用sklearn库实现的各种分类算法简单应用小结
2019/07/04 Python
python爬虫 基于requests模块的get请求实现详解
2019/08/20 Python
python获取栅格点和面值的实现
2020/03/10 Python
Python 实现平台类游戏添加跳跃功能
2020/03/27 Python
计算机专业毕业生求职信分享
2013/12/24 职场文书
晚会邀请函范文
2014/01/24 职场文书
自我评价的范文
2014/02/02 职场文书
幼儿园消防演练方案
2014/02/13 职场文书
迎元旦广播稿
2014/02/22 职场文书
2014年公司植树节活动方案
2014/03/04 职场文书
《从现在开始》教学反思
2014/04/15 职场文书
初中教师业务学习材料
2014/05/12 职场文书
甜品蛋糕店创业计划书
2014/09/21 职场文书
单方离婚协议书范本2014
2014/10/28 职场文书
python 制作一个gui界面的翻译工具
2021/05/14 Python
Python 统计序列中元素的出现频度
2022/04/26 Python