Python的描述符和属性是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过google和阅读源码,现将自己的理解和心得记录下来,也为正在为了该问题苦恼的朋友提供一个思考问题的参考。

关于@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__() ,一个对象中只要包含了这三个方法中的至少一个就称它为描述符。

描述符有什么作用?

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

先看如何用描述符来解决上面 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关联起来的?

描述符如何工作

类调用__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

我对上面的理解是,访问一个实例的属性的时候是先遍历它和它的父类,寻找它们的__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

__get__,__getattr__,__getattribute__

__get__,__getattr__和__getattribute__都是访问属性的方法,但不太相同。
object.__getattr__(self, name)
当一般位置找不到attribute的时候,会调用getattr,返回一个值或AttributeError异常。

object.__getattribute__(self, name)
无条件被调用,通过实例访问属性。如果class中定义了__getattr__(),则__getattr__()不会被调用(除非显示调用或引发AttributeError异常)

object.__get__(self, instance, owner)
如果class定义了它,则这个class就可以称为descriptor。owner是所有者的类,instance是访问descriptor的实例,如果不是通过实例访问,而是通过类访问的话,instance则为None。(descriptor的实例自己访问自己是不会触发__get__,而会触发call,只有descriptor作为其它类的属性才有意义。)(所以下文的d是作为C2的一个属性被调用)

class C(object):
a = 'abc'
def __getattribute__(self, *args, **kwargs):
print("__getattribute__() is called")
return object.__getattribute__(self, *args, **kwargs)
# return "haha"
def __getattr__(self, name):
print("__getattr__() is called ")
return name + " from getattr" def __get__(self, instance, owner):
print("__get__() is called", instance, owner)
return self def foo(self, x):
print(x) class C2(object):
d = C()
if __name__ == '__main__':
c = C()
c2 = C2()
print(c.a)
print(c.zzzzzzzz)
c2.d
print(c2.d.a) 结果:
__getattribute__() is called
abc
__getattribute__() is called
__getattr__() is called
zzzzzzzz from getattr
__get__() is called <__main__.C2 object at 0x16d2310> <class '__main__.C2'>
__get__() is called <__main__.C2 object at 0x16d2310> <class '__main__.C2'>
__getattribute__() is called
abc

小结:可以看出,每次通过实例访问属性,都会经过__getattribute__函数。而当属性不存在时,仍然需要访问__getattribute__,不过接着要访问__getattr__。这就好像是一个异常处理函数。
每次访问descriptor(即实现了__get__的类),都会先经过__get__函数。

需要注意的是,当使用类访问不存在的变量是,不会经过__getattr__函数。而descriptor不存在此问题,只是把instance标识为none而已。

Descriptor Protocol(协议)

参考链接:http://www.cnblogs.com/btchenguang/archive/2012/09/18/2690802.html

代码示例:

class RevealAccess(object):
"""创建一个Descriptor类,用来打印出访问它的操作信息
""" def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name def __get__(self, obj, objtype):
print('Retrieving', self.name) return self.val def __set__(self, obj, val):
print('Updating', self.name) self.val = val
# 使用Descriptor class MyClass(object):
# 生成一个Descriptor实例,赋值给类MyClass的x属性
x = RevealAccess(10, 'var "x"')
y = 5 # 普通类属性 if __name__ == "__main__":
a = MyClass()
print(a.x)
print()
a.x = 1
print(a.x)
print()
print(a.y)
print()
print(MyClass.x)
结果:
Retrieving var "x"
10 Updating var "x"
Retrieving var "x"
1 5 Retrieving var "x"
1

有下面这三个方法

get__(self, obj, type=None) --> value
set__(self, obj, value) --> None
delete__(self, obj) --> None
 
只要对象重写任何上面的一个方法,对象就被看作是descriptor,就可以不去采用默认的查找属性的顺序。
 
 
如果一个对象同时定义了__get__,__set__方法,被看作是data descriptor;只定义了__get__,被称
为non-data descriptor。如果实例字典中有一个key和data descriptor同名,那么查找时优先采用
data descriptor;如果实例字典中有一个key和non-data descriptor同名,那么优先采用实例字典的
方法。
 
 
创建一个只读data descriptor,只需要在同时定义__get__,__set__方法的同时,让__set__方法抛出异常
AttributeError。
需要注意的几点:
  • descriptor是被__getattribute__方法调用的。
  • 重写__getattribute__方法,会阻止自动的descriptor调用,必要时需要你自己加上去。
  • __getattribute__方法只在新式类和新式实例中有用。
  • object.__getattribute__和class.__getattribute__会用不一样的方式调用__get__
  • data descriptors总是覆盖instance dictionary
  • non-data descriptors有可能被instance dictionary覆盖

Python中属性和描述符的简单使用的更多相关文章

  1. 如何正确地使用Python的属性和描述符

    关于@property装饰器 在Python中我们使用@property装饰器来把对函数的调用伪装成对属性的访问. 那么为什么要这样做呢?因为@property让我们将自定义的代码同变量的访问/设定联 ...

  2. JavaScript中的对象描述符(属性特性)

    我们先创建一个对象: var person = { name: "Nicholas", _job: "Software Engineer", sayName: ...

  3. Linux中的文件描述符与打开文件之间的关系

    Linux中的文件描述符与打开文件之间的关系 导读 内核(kernel)利用文件描述符(file descriptor)来访问文件.文件描述符是非负整数.打开现存文件或新建文件时,内核会返回一个文件描 ...

  4. Linux中的文件描述符与打开文件之间的关系------------每天进步一点点系列

    http://blog.csdn.net/cywosp/article/details/38965239 1. 概述     在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件.目录文件. ...

  5. (转)Linux中的文件描述符

    本文转自:http://blog.csdn.net/cywosp/article/details/38965239 作者:cywosp 1. 概述 在Linux系统中一切皆可以看成是文件,文件又可分为 ...

  6. Python中基本的读文件和简单数据处理

    Python中基本的读文件和简单数据处理 暂无评论 DataQuest上面的免费课程(本文是Python基础课程部分),里面有些很基础的东西(csv文件读,字符串预处理等),发在这里做记录.涉及下面六 ...

  7. linux内核中的文件描述符(二)--socket和文件描述符

    http://blog.csdn.net/ce123_zhouwei/article/details/8459730 Linux内核中的文件描述符(二)--socket和文件描述符 Kernel ve ...

  8. [svc]linux中的文件描述符(file descriptor)和文件

    linux中的文件描述符(file descriptor)和文件 linux为了实现一切皆文件的设计哲学,不仅将数据抽象成了文件,也将一切操作和资源抽象成了文件,比如说硬件设备,socket,磁盘,进 ...

  9. (转)Linux中的文件描述符与打开文件之间的关系

    转:http://blog.csdn.net/cywosp/article/details/38965239 1. 概述     在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件.目录文 ...

随机推荐

  1. magento优化

    magento -- SEO优化继续往前走一步 magento的SEO做的实在太好了,让其它的电子商务平台与之相比实在是不是一个重级的,这也许就是magento成功的原因.尽管现在结合apache,实 ...

  2. SVM实现分类识别及参数调优(一)

    前言 项目有一个模块需要将不同类别的图片进行分类,共有三个类别,使用SVM实现分类. 实现步骤: 1.创建训练样本库: 2.训练.测试SVM模型: 3.SVM的数据要求: 实现系统: windows_ ...

  3. TJU Problem 1065 Factorial

    注意数据范围,十位数以上就可以考虑long long 了,断点调试也十分重要. 原题: 1065.   Factorial Time Limit: 1.0 Seconds   Memory Limit ...

  4. WinRAR备份技巧 - imsoft.cnblogs

    RAR控制台日常备份策略 run.batrar a -ep1 -agYYYY{年}MM{月}DD{日} 备份 @list.txt-ep1是忽略原文件路径,rar包里是一堆文件,没有目录结构-ag附加命 ...

  5. HBulider打包

    1. manifest配置 按照Manifest.json文档说明 manifest配置把工程中的manifest.json文件配置好,下面以我的项目为例进行配置. (1).应用信息 (2).图标配置 ...

  6. Oracle集合类型

    Oracle集合类型介绍   集合类型   1. 使用条件:    a. 单行单列的数据,使用标量变量 .     b. 单行多列数据,使用记录    c. 单列多行数据,使用集合        *集 ...

  7. Linux 'XXXXXX' "is not in the sudoers file. This incident will be reported" 解决方法

    添加方法如下: 1.进入root模式su -  注意:su和-之间有空格输入当前用户的密码 2.添加写权限chmod u+w /etc/sudoers 3.将自己加入到sudoers中 gedit / ...

  8. bat删除系统默认共享

    在我们的系统中,有很多默认的共享是开启的,可以设置一个bat文件在每次开机的时候把共享删除. net share c$ /del net share d$ /del net share e$ /del ...

  9. linux 命令:chmod权限设置命令

    Linux系统中的每个文件和目录都有访问许可权限,用它来确定谁可以通过何种方式对文件和目录进行访问和操作. 文件或目录的访问权限分为只读,只写和可执行三种.以文件为例,只读权限表示只允许读其内容,而禁 ...

  10. Kindle一周使用感受

    为何选择Kindle 「Kindle」终于入手,心情十分愉悦^_^,入手的是499块「Kindle国行版」,个人感觉电子墨水屏显示效果很赞,很适合在光线比较充足的环境下阅读,即使在中午的阳光底下使用K ...