Python中属性和描述符的简单使用
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')
因为描述符优先级高并且会改变默认的get、set行为,这样一来,当我们访问或者设置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
这样在获取、设置和删除score和ticket的时候都会进入Integer的__get__ 、 __set__ ,从而减少了重复的逻辑。
现在虽然问题得到了解决,但是你可能会好奇这个描述符到底是如何工作的。具体来说,在__init__函数里访问的是自己的self.score和self.ticket,怎么和类属性score和ticket关联起来的?
描述符如何工作
类调用__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
有下面这三个方法
- descriptor是被__getattribute__方法调用的。
- 重写__getattribute__方法,会阻止自动的descriptor调用,必要时需要你自己加上去。
- __getattribute__方法只在新式类和新式实例中有用。
- object.__getattribute__和class.__getattribute__会用不一样的方式调用__get__
- data descriptors总是覆盖instance dictionary
- non-data descriptors有可能被instance dictionary覆盖
Python中属性和描述符的简单使用的更多相关文章
- 如何正确地使用Python的属性和描述符
关于@property装饰器 在Python中我们使用@property装饰器来把对函数的调用伪装成对属性的访问. 那么为什么要这样做呢?因为@property让我们将自定义的代码同变量的访问/设定联 ...
- JavaScript中的对象描述符(属性特性)
我们先创建一个对象: var person = { name: "Nicholas", _job: "Software Engineer", sayName: ...
- Linux中的文件描述符与打开文件之间的关系
Linux中的文件描述符与打开文件之间的关系 导读 内核(kernel)利用文件描述符(file descriptor)来访问文件.文件描述符是非负整数.打开现存文件或新建文件时,内核会返回一个文件描 ...
- Linux中的文件描述符与打开文件之间的关系------------每天进步一点点系列
http://blog.csdn.net/cywosp/article/details/38965239 1. 概述 在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件.目录文件. ...
- (转)Linux中的文件描述符
本文转自:http://blog.csdn.net/cywosp/article/details/38965239 作者:cywosp 1. 概述 在Linux系统中一切皆可以看成是文件,文件又可分为 ...
- Python中基本的读文件和简单数据处理
Python中基本的读文件和简单数据处理 暂无评论 DataQuest上面的免费课程(本文是Python基础课程部分),里面有些很基础的东西(csv文件读,字符串预处理等),发在这里做记录.涉及下面六 ...
- linux内核中的文件描述符(二)--socket和文件描述符
http://blog.csdn.net/ce123_zhouwei/article/details/8459730 Linux内核中的文件描述符(二)--socket和文件描述符 Kernel ve ...
- [svc]linux中的文件描述符(file descriptor)和文件
linux中的文件描述符(file descriptor)和文件 linux为了实现一切皆文件的设计哲学,不仅将数据抽象成了文件,也将一切操作和资源抽象成了文件,比如说硬件设备,socket,磁盘,进 ...
- (转)Linux中的文件描述符与打开文件之间的关系
转:http://blog.csdn.net/cywosp/article/details/38965239 1. 概述 在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件.目录文 ...
随机推荐
- MAC OS下使用OpenSSL生成私钥和公钥的方法
MAC OS自带了OpenSSL,所以不用去编译那一坨跟SHIT一样的源码.直接在命令行里使用OPENSSL就可以. 打开命令行工具,然后输入 openssl打开openssl,接着只要三句命令就可以 ...
- tensorflow中的参数初始化方法
1. 初始化为常量 tf中使用tf.constant_initializer(value)类生成一个初始值为常量value的tensor对象. constant_initializer类的构造函数定义 ...
- RESTful API 学习
/********************************************************************************* * RESTful API 学习 ...
- Texas Instruments matrix-gui-2.0 hacking -- index.php
<?php /* * Copyright (C) 2011 Texas Instruments Incorporated - http://www.ti.com/ * * * Redistrib ...
- HDU1024 最大M子段和问题 (单调队列优化)
Max Sum Plus Plus Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others ...
- Ubuntu终端及VI 快捷键
Ubuntu终端 快捷键 功能 Tab 自动补全 Ctrl+a 光标移动到开始位置 Ctrl+e 光标移动到最末尾 Ctrl+k 删除此处至末尾的所有内容 Ctrl+u 删除此处至开始的所有内容 Ct ...
- java面试题12
1. jsp与servlet的区分? 答:Servlet和JSP都是基于java语言上的动态网页技术,Servlet程序其实就是java程序,只不过它所使用的类库为JAVA Servlet API, ...
- git server side hook 试用
git 的hook 是一个很方便的功能,我们可以使用hook 做好多处理,比如client side hook 进行 提交格式校验,server side 进行ci/cd 处理 测试使用docker- ...
- java面试笔试题收集
转载过来看看.... J2SE基础 1. 九种基本数据类型的大小,以及他们的封装类. 2. Switch能否用string做参数? 可以 3. equals与==的区别. 4. Object有哪些公用 ...
- binlog cache size设置是否合理判断
二进制日志是写操作是,首先写入二进制日志缓冲(binlog_cache)然后commit,再从binlog_cache写入到binlog文件,默认大小为32K,而binlog_cache是sessio ...