Python中屬性和描述符的正確使用
關(guān)于@property裝飾器
在Python中我們使用@property裝飾器來(lái)把對(duì)函數(shù)的調(diào)用偽裝成對(duì)屬性的訪問(wèn)。
那么為什么要這樣做呢?因?yàn)锧property讓我們將自定義的代碼同變量的訪問(wèn)/設(shè)定聯(lián)系在了一起,同時(shí)為你的類保持一個(gè)簡(jiǎn)單的訪問(wèn)屬性的接口。
舉個(gè)栗子,假如我們有一個(gè)需要表示電影的類:
class Movie(object): def __init__(self, title, description, score, ticket): self.title = title self.description = description self.score = scroe self.ticket = ticket
你開始在項(xiàng)目的其他地方使用這個(gè)類,但是之后你意識(shí)到:如果不小心給電影打了負(fù)分怎么辦?你覺(jué)得這是錯(cuò)誤的行為,希望Movie類可以阻止這個(gè)錯(cuò)誤。 你首先想到的辦法是將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
但這行不通。因?yàn)槠渌糠值拇a都是直接通過(guò)Movie.score
來(lái)賦值的。這個(gè)新修改的類只會(huì)在__init__
方法中捕獲錯(cuò)誤的數(shù)據(jù),但對(duì)于已經(jīng)存在的類實(shí)例就無(wú)能為力了。如果有人試著運(yùn)行m.scrore= -100
,那么誰(shuí)也沒(méi)法阻止。那該怎么辦?
Python的property解決了這個(gè)問(wèn)題。
我們可以這樣做
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
都會(huì)檢測(cè)它是否小于0。
property的不足
對(duì)property來(lái)說(shuō),最大的缺點(diǎn)就是它們不能重復(fù)使用。舉個(gè)例子,假設(shè)你想為ticket
字段也添加非負(fù)檢查。
下面是修改過(guò)的新類:
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")
可以看到代碼增加了不少,但重復(fù)的邏輯也出現(xiàn)了不少。雖然property可以讓類從外部看起來(lái)接口整潔漂亮,但是卻做不到內(nèi)部同樣整潔漂亮。
描述符登場(chǎng)
什么是描述符?
一般來(lái)說(shuō),描述符是一個(gè)具有綁定行為的對(duì)象屬性,其屬性的訪問(wèn)被描述符協(xié)議方法覆寫。這些方法是__get__()
、 __set__()
和__delete__()
,一個(gè)對(duì)象中只要包含了這三個(gè)方法中的至少一個(gè)就稱它為描述符。
描述符有什么作用?
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.—–摘自官方文檔
簡(jiǎn)單的說(shuō)描述符會(huì)改變一個(gè)屬性的基本的獲取、設(shè)置和刪除方式。
先看如何用描述符來(lái)解決上面 property邏輯重復(fù)的問(wèn)題。
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')
因?yàn)槊枋龇麅?yōu)先級(jí)高并且會(huì)改變默認(rèn)的get
、set
行為,這樣一來(lái),當(dāng)我們?cè)L問(wèn)或者設(shè)置Movie().score
的時(shí)候都會(huì)受到描述符Integer
的限制。
不過(guò)我們也總不能用下面這樣的方式來(lái)創(chuàng)建實(shí)例。
a = Movie() a.score = 1 a.ticket = 2 a.title = ‘test' a.descript = ‘…'
這樣太生硬了,所以我們還缺一個(gè)構(gòu)造函數(shù)。
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
這樣在獲取、設(shè)置和刪除score
和ticket
的時(shí)候都會(huì)進(jìn)入Integer
的__get__
、 __set__
,從而減少了重復(fù)的邏輯。
現(xiàn)在雖然問(wèn)題得到了解決,但是你可能會(huì)好奇這個(gè)描述符到底是如何工作的。具體來(lái)說(shuō),在__init__
函數(shù)里訪問(wèn)的是自己的self.score
和self.ticket
,怎么和類屬性score
和ticket
關(guān)聯(lián)起來(lái)的?
描述符如何工作
看官方的說(shuō)明
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.
類調(diào)用__getattribute__()
的時(shí)候大概是下面這樣子:
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
下面是摘自國(guó)外一篇博客上的內(nèi)容。
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__.
我對(duì)上面的理解是,訪問(wèn)一個(gè)實(shí)例的屬性的時(shí)候是先遍歷它和它的父類,尋找它們的__dict__
里是否有同名的data descriptor
如果有,就用這個(gè)data descriptor
代理該屬性,如果沒(méi)有再尋找該實(shí)例自身的__dict__
,如果有就返回。任然沒(méi)有再查找它和它父類里的non-data descriptor
,最后查找是否有__getattr__
描述符的應(yīng)用場(chǎng)景
python的property、classmethod修飾器本身也是一個(gè)描述符,甚至普通的函數(shù)也是描述符(non-data discriptor)
django model和SQLAlchemy里也有描述符的應(yīng)用
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
總結(jié)
只有當(dāng)確實(shí)需要在訪問(wèn)屬性的時(shí)候完成一些額外的處理任務(wù)時(shí),才應(yīng)該使用property。不然代碼反而會(huì)變得更加啰嗦,而且這樣會(huì)讓程序變慢很多。以上就是本文的全部?jī)?nèi)容,由于個(gè)人能力有限,文中如有筆誤、邏輯錯(cuò)誤甚至概念性錯(cuò)誤,還請(qǐng)?zhí)岢霾⒅刚?/p>
- Python descriptor(描述符)的實(shí)現(xiàn)
- Python描述符descriptor使用原理解析
- 通過(guò)實(shí)例解析python描述符原理作用
- python實(shí)現(xiàn)裝飾器、描述符
- Python 描述符(Descriptor)入門
- 詳解Python中的Descriptor描述符類
- Python黑魔法Descriptor描述符的實(shí)例解析
- Python中的Descriptor描述符學(xué)習(xí)教程
- Python 的描述符 descriptor詳解
- 解密Python中的描述符(descriptor)
- Python中的類與對(duì)象之描述符詳解
- python的描述符(descriptor)、裝飾器(property)造成的一個(gè)無(wú)限遞歸問(wèn)題分享
- Python基礎(chǔ)詳解之描述符
相關(guān)文章
基于pytorch實(shí)現(xiàn)對(duì)圖片進(jìn)行數(shù)據(jù)增強(qiáng)
圖像數(shù)據(jù)增強(qiáng)是一種在訓(xùn)練機(jī)器學(xué)習(xí)和深度學(xué)習(xí)模型時(shí)常用的策略,尤其是在計(jì)算機(jī)視覺(jué)領(lǐng)域,具體而言,它通過(guò)創(chuàng)建和原始圖像稍有不同的新圖像來(lái)擴(kuò)大訓(xùn)練集,本文給大家介紹了如何基于pytorch實(shí)現(xiàn)對(duì)圖片進(jìn)行數(shù)據(jù)增強(qiáng),需要的朋友可以參考下2024-01-01python實(shí)現(xiàn)nao機(jī)器人身體軀干和腿部動(dòng)作操作
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)nao機(jī)器人身體軀干和腿部動(dòng)作操作,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04python3 將階乘改成函數(shù)形式進(jìn)行調(diào)用的操作
這篇文章主要介紹了python3 將階乘改成函數(shù)形式進(jìn)行調(diào)用的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03Python數(shù)據(jù)分析之?Pandas?Dataframe合并和去重操作
這篇文章主要介紹了Python數(shù)據(jù)分析之?Pandas?Dataframe合并和去重操作,文章基于python的相關(guān)資料展開詳細(xì)的內(nèi)容介紹,需要的小伙伴可以參考一下2022-05-05Python實(shí)現(xiàn)將MP4視頻轉(zhuǎn)化為GIF圖像
與靜態(tài)圖像相比,動(dòng)態(tài)的?GIF?圖片更能吸引各位讀者的注意力,還可以提供更生動(dòng)、有趣和引人入勝的內(nèi)容,本文為大家介紹了Python將MP4視頻轉(zhuǎn)化為GIF圖像的方法,需要的可以參考下2023-06-06python實(shí)現(xiàn)微信小程序的多種支付方式
這篇文章主要為大家介紹了python實(shí)現(xiàn)微信小程序的多種支付方式的實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04解決jupyter不是內(nèi)部或外部命令,也不是可運(yùn)行程序問(wèn)題
這篇文章主要介紹了解決jupyter不是內(nèi)部或外部命令,也不是可運(yùn)行程序問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06通過(guò)CartPole游戲詳解PPO?優(yōu)化過(guò)程
這篇文章主要為大家介紹了通過(guò)CartPole游戲詳解PPO?優(yōu)化過(guò)程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04