Python中属性和描述符的正确使用


Posted in Python onAugust 23, 2016

关于@property装饰器

在Python中我们使用@property装饰器来把对函数的调用伪装成对属性的访问。

那么为什么要这样做呢?因为@property让我们将自定义的代码同变量的访问/设定联系在了一起,同时为你的类保持一个简单的访问属性的接口。

举个栗子,假如我们有一个需要表示电影的类:

class Movie(object):
 def __init__(self, title, description, score, ticket):
 self.title = title
 self.description = description
 self.score = scroe
 self.ticket = ticket

你开始在项目的其他地方使用这个类,但是之后你意识到:如果不小心给电影打了负分怎么办?你觉得这是错误的行为,希望Movie类可以阻止这个错误。 你首先想到的办法是将Movie类修改为这样:

class Movie(object):
 def __init__(self, title, description, score, ticket):
 self.title = title
 self.description = description

 self.ticket = ticket
 if score < 0:
  raise ValueError("Negative value not allowed:{}".format(score))
 self.score = scroe

但这行不通。因为其他部分的代码都是直接通过Movie.score来赋值的。这个新修改的类只会在__init__方法中捕获错误的数据,但对于已经存在的类实例就无能为力了。如果有人试着运行m.scrore= -100,那么谁也没法阻止。那该怎么办?

Python的property解决了这个问题。

我们可以这样做

class Movie(object):
 def __init__(self, title, description, score):
 self.title = title
 self.description = description
 self.score = score

 self.ticket = ticket
 
 @property
 def score(self):
 return self.__score
 
 
 @score.setter
 def score(self, score):
 if score < 0:
  raise ValueError("Negative value not allowed:{}".format(score))
 self.__score = score
 
 @score.deleter
 def score(self):
 raise AttributeError("Can not delete score")

这样在任何地方修改score都会检测它是否小于0。

property的不足

对property来说,最大的缺点就是它们不能重复使用。举个例子,假设你想为ticket字段也添加非负检查。

下面是修改过的新类:

class Movie(object):
 def __init__(self, title, description, score, ticket):
 self.title = title
 self.description = description
 self.score = score
 self.ticket = ticket
 
 @property
 def score(self):
 return self.__score
 
 
 @score.setter
 def score(self, score):
 if score < 0:
  raise ValueError("Negative value not allowed:{}".format(score))
 self.__score = score
 
 @score.deleter
 def score(self):
 raise AttributeError("Can not delete score")
 
 
 @property
 def ticket(self):
 return self.__ticket
 
 @ticket.setter
 def ticket(self, ticket):
 if ticket < 0:
  raise ValueError("Negative value not allowed:{}".format(ticket))
 self.__ticket = ticket
 
 
 @ticket.deleter
 def ticket(self):
 raise AttributeError("Can not delete ticket")

可以看到代码增加了不少,但重复的逻辑也出现了不少。虽然property可以让类从外部看起来接口整洁漂亮,但是却做不到内部同样整洁漂亮。

描述符登场

什么是描述符?

一般来说,描述符是一个具有绑定行为的对象属性,其属性的访问被描述符协议方法覆写。这些方法是__get__() __set__()__delete__() ,一个对象中只要包含了这三个方法中的至少一个就称它为描述符。

描述符有什么作用?

The default behavior for attribute access is to get, set, or delete the attribute from an object's dictionary. For instance, a.x has a lookup chain starting witha.__dict__[‘x'], then type(a).__dict__[‘x'], and continuing through the base classes of type(a) excluding metaclasses. If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.—?摘自官方文档

简单的说描述符会改变一个属性的基本的获取、设置和删除方式

先看如何用描述符来解决上面 property逻辑重复的问题。

class Integer(object):
 def __init__(self, name):
 self.name = name
 
 def __get__(self, instance, owner):
 return instance.__dict__[self.name]
 
 def __set__(self, instance, value):
 if value < 0:
  raise ValueError("Negative value not allowed")
 instance.__dict__[self.name] = value
 
class Movie(object):
 score = Integer('score')
 ticket = Integer('ticket')

因为描述符优先级高并且会改变默认的getset行为,这样一来,当我们访问或者设置Movie().score的时候都会受到描述符Integer的限制。

不过我们也总不能用下面这样的方式来创建实例。

a = Movie()
a.score = 1
a.ticket = 2
a.title = ‘test'
a.descript = ‘…'

这样太生硬了,所以我们还缺一个构造函数。

class Integer(object):
 def __init__(self, name):
 self.name = name
 
 def __get__(self, instance, owner):
 if instance is None:
  return self
 return instance.__dict__[self.name]
 
 def __set__(self, instance, value):
 if value < 0:
  raise ValueError('Negative value not allowed')
 instance.__dict__[self.name] = value
 
 
class Movie(object):
 score = Integer('score')
 ticket = Integer('ticket')
 
 def __init__(self, title, description, score, ticket):
 self.title = title
 self.description = description
 self.score = score
 self.ticket = ticket

这样在获取、设置和删除scoreticket的时候都会进入Integer__get__ __set__ ,从而减少了重复的逻辑。

现在虽然问题得到了解决,但是你可能会好奇这个描述符到底是如何工作的。具体来说,在__init__函数里访问的是自己的self.scoreself.ticket,怎么和类属性scoreticket关联起来的?

描述符如何工作

看官方的说明

If an object defines both __get__() and __set__(), it is considered a data descriptor. Descriptors that only define __get__() are called non-data descriptors (they are typically used for methods but other uses are possible).

Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance's dictionary. If an instance's dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence. If an instance's dictionary has an entry with the same name as a non-data descriptor, the dictionary entry takes precedence.

The important points to remember are:

descriptors are invoked by the __getattribute__() method
overriding __getattribute__() prevents automatic descriptor calls
object.__getattribute__() and type.__getattribute__() make different calls to __get__().
data descriptors always override instance dictionaries.
non-data descriptors may be overridden by instance dictionaries.

类调用__getattribute__()的时候大概是下面这样子:

def __getattribute__(self, key):
 "Emulate type_getattro() in Objects/typeobject.c"
 v = object.__getattribute__(self, key)
 if hasattr(v, '__get__'):
 return v.__get__(None, self)
 return v

下面是摘自国外一篇博客上的内容。

Given a Class “C” and an Instance “c” where “c = C(…)”, calling “c.name” means looking up an Attribute “name” on the Instance “c” like this:

Get the Class from Instance
Call the Class's special method getattribute__. All objects have a default __getattribute
Inside getattribute

Get the Class's mro as ClassParents
For each ClassParent in ClassParents
If the Attribute is in the ClassParent's dict
If is a data descriptor
Return the result from calling the data descriptor's special method __get__()
Break the for each (do not continue searching the same Attribute any further)
If the Attribute is in Instance's dict
Return the value as it is (even if the value is a data descriptor)
For each ClassParent in ClassParents
If the Attribute is in the ClassParent's dict
If is a non-data descriptor
Return the result from calling the non-data descriptor's special method __get__()
If it is NOT a descriptor
Return the value
If Class has the special method getattr
Return the result from calling the Class's special method__getattr__.

我对上面的理解是,访问一个实例的属性的时候是先遍历它和它的父类,寻找它们的__dict__里是否有同名的data descriptor如果有,就用这个data descriptor代理该属性,如果没有再寻找该实例自身的__dict__ ,如果有就返回。任然没有再查找它和它父类里的non-data descriptor,最后查找是否有__getattr__

描述符的应用场景

python的property、classmethod修饰器本身也是一个描述符,甚至普通的函数也是描述符(non-data discriptor)

django model和SQLAlchemy里也有描述符的应用

class User(db.Model):
 id = db.Column(db.Integer, primary_key=True)
 username = db.Column(db.String(80), unique=True)
 email = db.Column(db.String(120), unique=True)
 
 def __init__(self, username, email):
 self.username = username
 self.email = email
 
 def __repr__(self):
 return '<User %r>' % self.username

总结

只有当确实需要在访问属性的时候完成一些额外的处理任务时,才应该使用property。不然代码反而会变得更加??拢??艺庋?崛贸绦虮渎?芏唷R陨暇褪潜疚牡娜?磕谌荩?捎诟鋈四芰τ邢蓿?闹腥缬斜饰蟆⒙呒?砦笊踔粮拍钚源砦螅?骨胩岢霾⒅刚??/p>

Python 相关文章推荐
为Python的web框架编写前端模版的教程
Apr 30 Python
使用Python多线程爬虫爬取电影天堂资源
Sep 23 Python
Python中字符串的修改及传参详解
Nov 30 Python
Anaconda多环境多版本python配置操作方法
Sep 12 Python
python 保存float类型的小数的位数方法
Oct 17 Python
Python 实现子类获取父类的类成员方法
Jan 11 Python
Python-Tkinter Text输入内容在界面显示的实例
Jul 12 Python
Python3从零开始搭建一个语音对话机器人的实现
Aug 23 Python
Pytorch GPU显存充足却显示out of memory的解决方式
Jan 13 Python
TensorFlow2.0使用keras训练模型的实现
Feb 20 Python
python numpy中setdiff1d的用法说明
Apr 22 Python
Python制作表白爱心合集
Jan 22 Python
Python实现基本线性数据结构
Aug 22 #Python
Python进行数据提取的方法总结
Aug 22 #Python
详解Python实现按任意键继续/退出的功能
Aug 19 #Python
利用Python开发微信支付的注意事项
Aug 19 #Python
Python用模块pytz来转换时区
Aug 19 #Python
教你用python3根据关键词爬取百度百科的内容
Aug 18 #Python
利用Python爬取可用的代理IP
Aug 18 #Python
You might like
PHP+SQL 注入攻击的技术实现以及预防办法
2010/12/29 PHP
PHP中使用cURL实现Get和Post请求的方法
2013/03/13 PHP
解析PHP中的内存管理,PHP动态分配和释放内存
2013/06/28 PHP
JavaScript 程序编码规范
2010/11/23 Javascript
js 数据类型转换总结笔记
2011/01/17 Javascript
JavaScript中闭包的写法和作用详解
2016/06/29 Javascript
javascript 判断是否是微信浏览器的方法
2016/10/09 Javascript
Node.js开发第三方微信公众平台
2017/06/05 Javascript
基于vue.js 2.x的虚拟滚动条的示例代码
2018/01/23 Javascript
微信小程序实现手势图案锁屏功能
2018/01/30 Javascript
JS实现将二维数组转为json格式字符串操作示例
2018/07/12 Javascript
示例vue 的keep-alive缓存功能的实现
2018/12/13 Javascript
如何使用VuePress搭建一个类型element ui文档
2019/02/14 Javascript
vue插件mescroll.js实现移动端上拉加载和下拉刷新
2019/03/07 Javascript
vue-路由精讲 二级路由和三级路由的作用
2020/08/06 Javascript
解决Element中el-date-picker组件不回填的情况
2020/11/07 Javascript
在Python中关于中文编码问题的处理建议
2015/04/08 Python
Python编程中用close()方法关闭文件的教程
2015/05/24 Python
Python中的super()方法使用简介
2015/08/14 Python
深入理解Django中内置的用户认证
2017/10/06 Python
Sanic框架Cookies操作示例
2018/07/17 Python
Python小进度条显示代码
2019/03/05 Python
python requests证书问题解决
2019/09/05 Python
python画图常规设置方式
2020/03/05 Python
Django中文件上传和文件访问微项目的方法
2020/04/27 Python
MxNet预训练模型到Pytorch模型的转换方式
2020/05/25 Python
html5使用canvas绘制一张图片
2014/12/15 HTML / CSS
意大利奢侈品购物网站:Deliberti
2019/10/08 全球购物
农行实习自我鉴定
2013/09/22 职场文书
计算机专业大学生的自我评价
2013/11/14 职场文书
司机职责范本
2014/03/08 职场文书
大学新生军训方案
2014/05/03 职场文书
股份合作协议书
2014/09/10 职场文书
2014年党支部书记工作总结
2014/12/04 职场文书
大学生自我推荐信范文
2015/03/24 职场文书
CSS基础详解
2021/10/16 HTML / CSS