问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

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

发布网友 发布时间:2022-04-13 04:46

我来回答

2个回答

懂视网 时间:2022-04-13 09:07

Python 描述符是一种创建托管属性的方法。每当一个属性被查询时,一个动作就会发生。这个动作默认是get,set或者delete。不过,有时候某个应用可能会有

更多的需求,需要你设计一些更复杂的动作。最好的解决方案就是编写一个执行符合需求的动作的函数,然后指定它在属性被访问时运行。一个具有这种功能的对象

称为描述符。描述符是python方法,绑定方法,super,PRoperty,staticmethod和classmethod的实现基础。

1.描述符协议

描述符descriptor就是一个表示属性值的对象,通过实现一个或多个__get__,__set__,__delete__方法,可以将描述符与属性访问机制挂钩,还可以自定义这些操作。

__get__(self,instance,own):用于访问属性,返回属性的值。instance为使用描述符的实例对象,own为实例所属的类。当通过类访问属性时,instance为None。

__set__(self,instance,value):设定属性值。

__delete__(self,instance):删除属性值。

2.描述符如何实现

class Descriptor(object):
def __get__(self, instance, owner):
print 'getting:%s'%self._name
return self._name
def __set__(self, instance, name):
print 'setting:%s'%name
self._name = name
def __delete__(self, instance):
print 'deleting:%s'%self._name
del self._name
class Person(object):
name = Descriptor()

一个很简单的描述符对象就产生了,现在可以对一个Person对象进行属性name的读取,设置和删除:

>>> p=Person()
>>> p.name='john'
setting:john
>>> p.name
getting:john
'john'
>>> del p.name
deleting:john

注意:描述符只能在类级别上进行实例化,不能通过在__init__()和其他方法中创建描述符对象来为每个实例创建描述符。

具有描述符的类使用的属性名称比实例上存储的属性名称具有更高的优先级。为了能让描述符在实例上存储值,描述符必须挑选一个与它本身所用名称不同的名称。

如上例,Person类初始化__init__函数为实例设置属性就不能用name名称了。

data描述符与none-data描述符:

如果实现了__get__和__set__就是一个data描述符,如果只有__get__就是一个non-data描述符。不同的效果在于data描述符总是替代在一个实例中的属性实现,

而non-data描述符由于没有set,在通过实例对属性赋值时,例如上面的p.name = 'hello',不会再调用__set__方法,会直接把实例属性p.name设为'hello'。

当然如果仅仅在__set__中raise AttributeError,仍然得到的是一个non-data的描述符。

描述符调用机制:

当查询一个对象的属性a.attr时,如果python发现attr是个描述符对象,如何读取属性取决于对象a:

直接调用:最简单的调用是直接使用代码调用描述符的方法,attr.__get__(a)

实例绑定:如果a是个实例对象,调用方法:type(a).__dict__['attr'].__get__(a,type(a))

类绑定:如果A是个类对象,调用方法:A.__dict__['attr'].__get__(None,A)

super绑定:如果a是个super实例,那么super(B,obj).m()通过查询obj.__class__.__mro__找到B的基类A,然后执行A.__dict__['m'].__get__(obj,obj.__class__)

3.执行属性类型检查的描述符

class TypedProperty(object):
def __init__(self,name,attr_type,default=None):
self.name='_'+name
self.type=attr_type
self.default=default if default else attr_type()
def __get__(self,instance,own):
return getattr(instance,self.name,self.default)
def __set__(self,instance,value):
if not isinstance(value,self.type):
raise TypeError,'Must be %s'%self.type
setattr(instance,self.name,value)
def __delete__(self,instance):
raise AttributeError('Can not delete attribute')
class Foo(object):
name=TypedProperty('name',str)
num=TypedProperty('num',int,37)

上述描述符可以对属性的类型进行检查,如果name属性不设为str类型或者num不设为int类型,就会报错:

>>> f.name=21
TypeError: Must be <type 'str'>

而且禁止对属性进行删除操作:

>>> del f.name
AttributeError: Can not delete attribute

f.name 隐形的调用type(f).__dict__['name'].__get__(f,Foo),即Foo.name.__get__(f,Foo)。

上述描述符实际是存储在实例上的,name通过setattr(f,_name,value)存储在f._name上,num存储在f._num上,这也是加下划线的原因,

否则描述符名称name会和实例属性name发生冲突,描述符属性f.name会覆盖掉实例属性f.name。

热心网友 时间:2022-04-13 06:15

关于@property装饰器
在Python中我们使用@property装饰器来把对函数的调用伪装成对属性的访问。
那么为什么要这样做呢?因为@property让我们将自定义的代码同变量的访问/设定联系在了一起,同时为你的类保持一个简单的访问属性的接口。
举个栗子,假如我们有一个需要表示电影的类:

1
2
3
4
5
6
7
8

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

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

Python

1
2
3
4
5
6
7
8

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解决了这个问题。
我们可以这样做

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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字段也添加非负检查。下面是修改过的新类:

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

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逻辑重复的问题。

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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 = ‘…’
这样太生硬了,所以我们还缺一个构造函数。

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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关联起来的?
描述符如何工作
看官方的说明
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__()的时候大概是下面这样子:

1
2
3
4
5
6
7

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里也有描述符的应用

Python

1
2
3
4
5
6
7
8
9
10
11
12

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。不然代码反而会变得更加啰嗦,而且这样会让程序变慢很多。
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
临沂比较有名的男装品牌 呼伦贝尔市悦动网络科技有限公司怎么样? 呼伦贝尔中汇实业有限公司怎么样? 呼伦贝尔油玉不绝电子商务有限公司怎么样? 如何避免wps卡顿? 属鼠的男人找对象是属什么,属鼠的人和什么属相合 96年鼠的姻缘在哪年 属相相合年份运势提升 2024属鼠找对象属什么最佳 黑客攻击网站能报案吗 黑客攻击报案有用吗 三星s20手机如何外接音箱? 征服s20手机怎么样 英语 雷ber滴 和 biu乐滴 是什么意思??? 英语biut fou什么意思 英语单词bu怎么读谢谢了,大神帮忙啊 我想要知道一个 英文单词 土音 BIU 得 否 是 什么 英文单词?给我翻译下 英文BIub什么意思 三星s20这款手机都有那些使用的小技巧啊? 这个英语怎么读 bureau 能避开拥堵路线的手机导航有么? 今年运势不好总破小财怎么办,谢谢大家了, 每个人换大运都要有点灾或破点财吗? 今年换乙未大运,破了点小财,呵呵! 大师瞅一眼 越忙越出错,我把防盗门钥匙拧折了!下午就忙这事了!还破了210元小财!就当破财免灾吧! 我养的孔雀鱼突然死了有什么寓意吗 鸡窝屎在枕头上什么寓意!! 解签:春去夏来日夜忙,秋冬收利好时光,佛祖保佑人康乐,破点小财免灾障 最近总是破财,求大师指点88年龙阴历2月2日生。时间是下午7点左右。谢谢各位大师。 破财消灾真的有这一说法吗? 今天我摔了一跤别人对我说是个小小的惩罚我怎么回复他呢? 结婚钻戒丢了有什么说法吗 品牌直通车空调遥控器怎么输入代码 如何练成ana cheri身材 请问有哪些有名的哑铃健身大神 男生是不是不喜欢腿粗的女生? 怎样辨别乳清蛋白真假? 求奏音69 revolver罗马音 电脑ip是什么啊 干嘛用的啊?谢谢了,大神帮忙啊 乳清蛋白是什么味道?尝起来怎么样? 胖人减脂,乳清蛋白怎么样? 蛋白质粉和乳清蛋白粉是同一个东西么?效果一样么? 蛋白粉选海德力纯享乳清蛋白怎么样?增肌效果好吗? 汤臣倍健乳清蛋白怎么样 乳清蛋白适合什么样的人?有什么好处?什么牌子的乳清蛋白比较好? 玉鹤鸣乳清蛋白粉怎么样? 解放j6f的空调滤芯在什么位置? j6空滤器多大流量1109010 刷单要电脑ip地址干嘛 六年级学生作息时间表 求六年级学生作息时间表 急需六年级的学习计划表(追加悬赏)