Python中的屬性管理



(網利寶項目框架是基於python自開發的
,使用了大量的python自有屬性方法,大家感興趣,可以掃碼註冊體驗產品

Python
管理屬性的方法一般有三種:操作符重載(即,__getattr____setattr____delattr____getattribute__,有點類似於C++中的重載操作符)、property內置函數(有時又稱“特性”)和描述符協議(descriptor)。

 

Python中,類和類實例都可以有屬性:Python中類的屬性相當於C++中類的靜態成員,而類實例的屬性相當於C++中類的非靜態成員。可以簡單地這麼理解。

 


1 操作符重載

 

Python中,重載__getattr____setattr____delattr____getattribute__方法可以用來管理一個自定義類中的屬性訪問。其中,__getattr__方法將攔截所有未定義的屬性獲取(即,當要訪問的屬性已經定義時,該方法不會被調用,至於定義不定義,是由Python能否查找到該屬性來決定的);__getattribute__方法將攔截所有屬性的獲取(不管該屬性是否已經定義,只要獲取它的值,該方法都會調用),由於此情況,所以,當一個類中同時重載了__getattr____getattribute__方法,那麼__getattr__永遠不會被調用,另外,__getattribute__方法僅僅存在於Python2.6的新式類和Python3的所有類中;__setattr__方法將攔截所有的屬性賦值;__delattr__方法將攔截所有的屬性刪除。說明:在Python中,一個類或類實例中的屬性是動態的(因爲Python是動態的),也就是說,你可以往一個類或類實例中添加或刪除一個屬性。

 

說明:《Python學習手冊(第四版)》中把這些操作分成兩類:一類是__getattr____setattr____delattr__,另一類是__getattribute__。筆者認爲它們都是操作符重載且與C++中的操作符重載類似,所以在這把它們歸爲一類。

 

如果我們想使用這類方法(即重載操作符)來管理自定義類的屬性,就需要我們在我們的自定義類中重新定義這些方法的實現。由於__getattribute____setattr____delattr__方法對所有的屬性進行攔截,所以,在重載它們時,不能再像往常的編碼,要注意避免遞歸調用(如果出現遞歸,則會引起死循環);然而對__getattr__方法,則沒有這麼多的限制。

 

1.1 重載__setattr__方法

在重載__setattr__方法時,不能使用“self.name = value”格式,否則,它將會導致遞歸調用而陷入死循環。正確的應該是:

 

def  __setattr__(self, name, value):

    # do-something

object.__setattr__(self, name, value)

# do-something

 

注:其中的“object.__setattr__(self, name, value)”一句可以換成“self.__dict__[name] = value”;但前提是,必須保證__getattribute__方法重載正確(如果重載了__getattribute__方法的話),否則,將在賦值時導致錯誤,因爲self.__dict__將要觸發對self所有屬性中的__dict__屬性的獲取,這樣從而就會引發__getattribute__方法的調用,如果__getattribute__方法重載錯誤,__setattr__方法自然而然也就會失敗。

 

1.2 重載__delattr__方法

在重載__delattr__方法時,不能使用del self.name格式,否則,它將會導致遞歸調用而陷入死循環。正確的應該是:

 

def  __delattr__(self, name):

    # do-something

object.__delattr__(self, name)

# do-something

 

注:其中的“object.__delattr__(self, name)”一句可以換成“del self.__dict__[name]”;但前提是,必須保證__getattribute__方法重載正確(如果重載了__getattribute__方法的話),否則,將在刪除屬性時導致錯誤,因爲self.__dict__將要觸發對self所有屬性中的__dict__屬性的獲取,這樣從而就會引發__getattribute__方法的調用,如果__getattribute__方法重載錯誤,__delattr__方法自然而然也就會失敗。

 

1.3 重載__getattribute__方法

def  __getattribute__(self, name):

    # do-something

return object.__getattribute__(self, name)

# do-something

 

注:在__getattribute__方法中不能把“return object.__getattribute__(self, name)”一句替換成“return self.__dict__[name]”來避免循環,因爲它(即其中的self.__dict__)也會觸發屬性獲取,進而還是會導致遞歸調用。

 

1.4 重載__getattr__方法

由於__getattr__方法是攔截未定義的屬性,所以它沒有其他三個操作符方法中那麼多的限制,因此,你可以像正常的代碼一樣編寫它。它的作用就是,當一段代碼(用戶寫的,可能是故意,也可以是無意)獲取了類或類實例中沒有定義的屬性時,程序將做出怎樣的反應,而這個迴應就在__getattr__方法中,由你來定。

 

1..5 說明

這裏是一些關於這幾個操作符重載的額外信息,以及一些其他的內容,比如:屬性存儲位置。

 

1.5.1 默認的屬性訪問攔截

如果上述四個操作符方法中實現了任何一個,那麼,當我們執行屬性訪問時,相應的操作符方法就會被調用。但是當我們沒有實現某個時,那麼相應的屬性訪問由默認實現來攔截。比如:如果我們僅僅實現了__setattr____delattr__兩個操作符方法,而沒有實現__getattribute____getattr__操作符方法,那麼我們對屬性的引用獲取將使用系統默認的方式,而對於所有的屬性賦值則調用__setattr__操作符方法,所有屬性的刪除操作則調用__delattr__操作符方法。

 

1.5.2 返回值

__getattr____getattribute__應該返回屬性name的值,但__setatrr____delattr__就返回NoneNone可以顯示返回,也可以不顯示返回;當不顯示返回時,Python中的函數或方法默認返回None)。

 

1.5.3 使用object類來避免循環調用

可能有些讀者會對object.__getattribute__(self, name)object.__setattr__(self, name, value)object.__delattr__(self, name)有些疑惑。其實,只要明白了Python中的類型繼承系統和類方法調用就會很容易明白了。下面僅僅簡單的說明一下,有關更詳細的這方面的內容請參見相關的Python書籍(因爲這屬於Python的基本語法範疇)。另外,這裏也與類實例中屬性佈局有點小小的關係,我們將在本章節的末尾討論(這部分不瞭解也行,瞭解將對Python更加清楚)。

 

關於Python中的類型繼承系統,在Python2.6中的新式類中和Python3中的所有類中,所有的類(包括內置類型和我們的自定義類型)都是object的子類,object是類型繼承系統中最頂尖的類,所有的類都直接或間接地繼承自它。另外,補充一句,所有類型(包括內置類型和我們的自定義類型)的類型都是type類型,或者說它們這些類型都是由type類型創建的。這裏有個例外,我們的自定義類型可以通過元類來修飾或改變我們的自定義類,這時我們的自定義類型的類型可能會改變(具體情況要看我們的元類的實現)。

 

關於Python中的類成員方法的調用很特別,與其他傳統的編程語言不同。當我們使用Instance.Method( args …)格式來調用類成員方法時,Python在底層會做一個轉換:Python先到Instance所屬的類中找Method方法,如果找到就調用它,如果沒有找到,就到Instance所屬的類的所有基類中依次去找,直到找到第一個爲止。當找到一個Method方法時,這裏我們假設Method方法所屬的類是Class,那麼Python就會把上述格式的方法調用轉換成Class.Method(Instance, args …)形式,接着在底層完成相應的工作。也就是說,Python在最終完成方法調用的是Class.Method(Instance_object, Instance_args)。那麼,我們是不是可以說,可以直接使用這種形式呢?答案是。可以說,我們在進行方法調用時,可以使用這兩種方式中的任何一種;但是,它們有一點區別:當使用第一種時,Python會向從類實例所屬的類開始,沿着繼承鏈向上(它的基類)搜索成員方法,直到找到第一個或找到達object類也沒有找到才停止;然而,對於第二種,Python只在Class的名字空間(即__dict__字典)中搜索成員方法,而不會搜索其他的類。

 

由於所有的類都是object的子類,所以我們可以通過上面的方式,把屬性的訪問控制遞交給object類,以此來避免遞歸調用。

 

1.5.4 公有、私有控制

由於Python是動態語言,也就是說,你可以動態的給已經定義好的類或類實例添加、刪除某個屬性;而在C++Java等語言中,類一旦定義,其類屬性或類實例屬性就不能添加、刪除。如果你想在Python中,像C++Java那樣對未定義屬性的訪問進行限制,也是可以的,你可以這樣做:凡是對未定義屬性的獲取、賦值、刪除操作,在相應的訪問控制方法(__getattr____getattribute____setaatr____delattr__)中都拋出一個AttributeError異常(這個異常是Python內置的)。

 

澄清一下,我們可以像C++Java那樣,在Python中控制未定義屬性的訪問,但是我們不能像C++Java那樣控制公有(public)、私有(private)、保護(protected)性的訪問。在Python中,所有的屬性都是公有的,你永遠都可以訪問(獲取、賦值、刪除),我們沒有辦法把它們變成私有的。雖然Python中有一個“私有”成員的概念,但它並不是C++Java中的那個“私有”,它十分的脆弱,你可以把它給忽略掉。

 

筆者上面所述的“我們沒有辦法把它們變成私有的”並不十分準確,筆者在這樣說時,是按照Python中默認的方式,也就是說,你沒有蓄意地改變這種行爲。換句話說,就是我們確實可以通過某種手段,潛在地來把Python中的某些類或類實例的“公有”屬性變成一個“私有”的屬性。在《Python學習手冊(第四版)》中,作者Mark Lutz給出了一個解決方案——他寫了一個裝飾器,通過這個裝飾器可以把任何一類的某些屬性變成“私有”或“公有”的(這裏所說的“公有”和“私有”相當於C++Java中的公有和私有),但是這種“公有”性也不是很強,有點脆弱,能夠被惡意的代碼所擊穿(Mark Lutz也承認了這點)。除此之外,我們還可以通過原始的方法來實現“公有”和“私有”性。這種方法就是,把所有的屬性分爲兩類:公有集合和私有集合;把所有的公有屬性名放在一個公有集合中,把所有的私有屬性放到私有集合當中,當我們訪問一個屬性時,先判斷它是否是存在於私有集合中,如果是,則不能直接訪問,對於成員變量必須通過一個成員方法來間接訪問(在C#中,比較嚴格,爲了訪問私有成員變量,必須要定義getset方法),對於成員方法則不能訪問,否則就拋出一個異常(比如:AttributeError內置異常)或者什麼都不做;如果它不在於私有集合當中,再判斷它是否是存在於公有集合當中,如果是,則可以進行相應的操作,如果不是,則拋出一個異常(比如:AttributeError異常,表示沒有該屬性)。在Python中,我們很難甚至沒辦法來實現像C++Java中的“保護”(protected)機制。

 

1.5.5 Python中類或類實例的屬性的存儲位置

關於這部分內容,其實也很簡單,就像C++中的成員變量一樣。簡單地說,C++中的成員變量怎樣理解,在Python中也怎樣理解。

 

這裏,筆者簡述一下:在類繼承體系中,在不同類中聲明的成員變量如何存儲?

 

Python中,所有的東西都是對象,這些對象是由一個類型實例化而來;那麼說,這些對象就有屬性和方法,屬性可以存儲一些值。而從直觀上來看,任何可以存儲東西的事物,我們都可以說它有一個空間(只有有空間,才能存儲東西嘛),而在編程中,我們一般使用術語“名字空間”或“命名空間”(namespace)來稱呼它。這樣,我們就得出,每個對象都有一個名字空間。而在Python中,我們使用對象的__dict__屬性來保存該對象的名字空間中的東西,__dict__是一個字典(“鍵-值”對,一般“鍵”就是屬性名或方法名,“值”就是屬性的值或方法名所指向的真正的方法實體對象)。

 

因此,我們看出,類有它自己的名字空間,它的名字空間中保存的是它的屬性和方法;而類實例也有它自己的名字空間,它的名字空間中保存的是它自己的屬性(不包含類的屬性)。但是我們要知道,Python在對類實例的屬性查找時,可以向類中查找,也就是說,可能通過類實例來引用類的屬性;反過來,卻是不行。我們雖然能夠通過類實例來引用類的屬性,但是卻不能通過類實例來給類的屬性賦值或刪除類的屬性:當我們通過類實例來給某個屬性賦值或刪除某個屬性時,Python只認爲該屬性是該類實例的,而不會到類的名字空間中查找它。當通過類實例給個屬性賦值時,如果該類實例中已經有該屬性,則Python將把它的值修改成新值;如果該類實例中沒有該屬性,那麼,Python將會在該類實例的名字空間(即__dict__)中創建該屬性,並給它賦值成新值,這時,如果該類實例所屬的類及其基類中也有同名的屬性,那麼這些同名屬性在查找時將會被該類實例的屬性所覆蓋,也就是,Python將找到該類實例中的屬性,而不會找到它所屬的類及其基類中的那些同名屬性。

 

那麼,我們如何才能修改或刪除類中類屬性呢?其實很簡單,我們不能通過類實例,而是要通過類本身(類本身也是一個對象)。比如:類A中有一個類屬性a,如果要想修改a的值,就可以使用“A.a = 123”。如果我們非要使用類實例來操作類屬性的賦值和刪除怎麼辦?其實也很簡單,我們可以像在C++Java中訪問私有成員變量一樣,定義一個成員方法,用這個成員方法來修改或刪除該類的類屬性,然後類實例去調用即可,而且這種方式的控制可以用於繼承體系當中。

 

1.5.6 最後一點事實的澄清

前面我們已經陳述,__getattr__會攔截所有未定義的屬性獲取,__getattribute__會攔截所有的屬性獲取,__setattr__會攔截所有的屬性賦值,__delattr__會攔截所有的屬性刪除。在這裏,筆者承認自己“說了慌”,其實它們並沒有這麼大的能力。前面的陳述是有一個限制的:它不適用於隱式地使用內置操作獲取的方法名屬性。這意味着操作符重載方法調用不能委託給被包裝的對象,除非包裝類自己重新定義這些方法(附:這些方法非常適合用於基於委託的編碼模式)。

 

 

 

 


2 特性

 

Python中,除了重載操作符外來管理類實例屬性的訪問控制外,也可以使用特性(property)。

 

2.1 property

在沒有講解使用特性來管理類實例的屬性訪問控制時,我們先來探討一下“什麼是特性”。

 

其實,在Python中,隱式的存在一種類型,它就是property類,可以把它看成是int類型,並可以使用它來定義變量(在面向對象裏,一般稱爲“對象”,也就是類實例)。而用來管理類實例的屬性訪問控制的“特性”正是使用這個property類。該類可以管理自定義類的實例的屬性訪問控制。

 

property類中,有三個成員方法和三個裝飾器函數。三個成員方法分別是:fgetfsetfdel,它們分別用來管理屬性訪問;三個裝飾器函數分別是:gettersetterdeleter,它們分別用來把三個同名的類方法裝飾成property。其中,fget方法用來管理類實例屬性的獲取,fset方法用來管理類實例屬性的賦值,fdel方法用來管理類實例屬性的刪除;getter裝飾器把一個自定義類方法裝飾成fget操作,setter裝飾器把一個自定義類方法裝飾成fset操作,deleter裝飾器把一個自定義類方法裝飾成fdel操作

 

以上的說明,筆者有點把fgetfsetfdel的功能說死了,其它這三個函數中不僅僅可以分別管理自定義類實例屬性的獲取、賦值、刪除,它們還可以做其它的任何事情,就像普通函數一樣(普通函數能完成什麼樣的功能,它們也都可以),甚至不做它們的本職工作也行(不過,我們一般都會完成自定義類實例屬性的獲取、賦值、刪除等操作,有時,可能還會完成一些其它額外的工作)。其實,不管這三個函數完成什麼的功能,只要在獲取自定義類實例的屬性時就會自動調用fget成員方法,給自定義類實例的屬性賦值時就會自動調用fset成員方法,在刪除自定義類實例的屬性時就會自動調用fdel成員方法。

 

2.2 property類的使用

根據property類中的成員類別,筆者把有關“特性”的使用分成兩部分:一般成員方法和裝飾器方法。

 

2.2.1 使用一般成員方法

我們上面提到“property類是隱式的”,所以,我們不能直接使用這個property類。那怎麼辦呢?沒關係,Python爲我們提供了一個標準的內置函數property,該函數通過我們傳遞給它的參數自動幫我們創建一個property類實例,並將該類實例返回。所以,我要想創建property類實例,就需要調用標準的內置函數property

 

2.2.1.1 property函數

property函數的原型:property(fget=None,  fset=None,  fdel=None,  doc=None)

其中,前三個參數分別是自定義類中的方法名,property函數會根據這三個參數自動創建property類中的三個相應的方法。第四個參數文檔(也就是字符串),把它作爲property類的說明文檔;我們知道,在Python中,可以爲一個類或函數添加一個文檔說明;當doc參數爲None時,property函數會提取第一個參數fget的說明文檔,如果fget參數的說明文檔也爲None,那麼doc參數的值就爲Noneproperty函數返回一個property類型的實例。

 

2.2.1.2 property的使用方法

關於“特性”的使用方法,是在自定義類中通過定義一個類屬性而完成的;雖然property類實例是自定義類的類屬性,但對property類實例的操作將作用於自定義類的實例的屬性上。其實,自定義類的實例的屬性的信息仍然是存儲於自定義類的實例中,而“特性”(即property類實例)只不過在管理如何對它進行訪問(獲取、賦值、刪除)。

 

有些讀者可能會對上述的“自定義類的實例的屬性”有所疑惑。就像開頭所述的,其實它就相當於C++中類的實例的成員。

 

property創建的屬性(也就是管理自定義類實例的“特性”)是屬於自定義類的,而不是屬於自定義類實例的;但是,它卻是管理自定義類實例的屬性的,而不管理自定義類的類屬性。下面,我們具體地再把“特性”的執行流程闡述一下。

 

由以上,我們可以知道,要用“特性”來管理自定義類的實例的屬性訪問控制,必須有兩個屬性,一個是自定義類的類屬性(也就是“特性”,porperty類的實例),另一個自定義類的實例的屬性。既然“特性”是自定義類的屬性,那麼可以我們可以通過自定義類對象(因爲類型也是一個對象,所以自定義類自然也是一個對象,我們稱爲自定義類對象)來訪問“特性”;又由於“特性”是property類的實例,因此,此時我們得到的是一個實例或者說是對象,而它有fgetfsetfdel三個成員方法,那麼我們可以來間接的訪問這些方法,而我們知道這三個成員方法是用來管理自定義類的實例的屬性訪問控制,此時調用這些方法將會間接地管理自定義類的實例的屬性訪問控制。因此,這種通過自定義類對象來調用其成員方法來控制自定義類的實例的屬性訪問控制是一種間接性的,我們可以這麼做,但實際應用中,我們是不會這麼用,這裏只是說明一下,讓讀者對“特性”有個更深層次的瞭解;在實際應用中使用的是下面的一種方法。

 

對於面向對象,我們知道,類的實例可以訪問類的屬性(在C++中就是類中的靜態成員)。所以,在Python中,我們也可以通過自定義類的實例來訪問“特性”。但是,此時的訪問卻與一般的類實例訪問類屬性的行爲有些不同:自定義類實例訪問到的“特性”(這裏筆者沒有說“類屬性”,就是爲了從字面上與此區別)不是property類實例本身,而是進行自動的調用轉,也就是說,Python會根據屬性的訪問類別(獲取、賦值、刪除)自動調用相應的fgetfsetfdel成員方法並得出結果,這也正是“特性”的本質所在——“特性”能夠通過自定義類實例去自動管理自定義類實例的屬性訪問控制(其實還可以做些其它的工作,甚至不管理自定義類實例的屬性訪問控制——請參見2.1中對fgetfsetfdel的描述說明),我們一般稱此爲“特性攔截了屬性的訪問”。

 

我們介紹完了“特性”(即自定義類的類屬性——property類實例)的訪問,我們再看看自定義類實例的屬性訪問控制。既然是自定義類實例的屬性,那麼就只能通過自定義類實例來訪問。

 

當我們通過自定義類實例來訪問其自身的屬性時,其訪問控制是由__getattr____getattribute____setattr____delattr__四個重載操作符來完成的,與“特性”(以及後面講到的“描述符”)無關,並且其訪問方式和正常的訪問一樣。

 

至此,我們已經基本上講完通過成員方法來使用“特性”了。最後,我們要注意兩點:第一,一個“特性”只能控制自定義類的一個屬性的訪問控制,如果想控制多個,就必須定義多個“特性”;第二,在通過“特性”來控制自定義類實例的屬性訪問控制時,“特性”的名字不能和它所控制的自定義類實例的屬性的名字相同。爲什麼不能一樣呢?我們試想,當一樣時,如果我們在“特性”的成員方法中再次訪問了該屬性,它就會觸發屬性的獲取、賦值或刪除操作,那麼由於“特性”會攔截該訪問控制,所以就會再次引發該成員方法的調用,這樣,就會導致遞歸調用而進入死循環,直到內存耗盡。當然,如果你不會在“特性”的成員方法中再次訪問該屬性,就不會導致遞歸調用。

 

2.2.1.3 例子

現在,我們已講解完“特性”的理論知識,再看個例子或許就更清楚了。

注:此例適用於Python2.6中的新式類和Python3中的所有類(因爲Python3中的類都是新式類),不適用於Python2.6舊式類。在下面例子中,“#”後面的註釋是相應的語言的輸出結果,除了說明不是註釋外。

class  A:

def  __init__(self, value):

     print(“init …”)

     self._x = 0

def  getx(self):

    print(“getx …”)

return self._x

def  setx(self, value):

    print(“setx …”)

self._x = value

def  delx(self):

    print(“delx …”)

del self._x

x = property(getx, setx, delx)

 

a = A()             # init ...

 

a.x                # getx ...

                  # 0

a.x = 123           # setx ...

a.x                # getx ...

                  # 123

 

a._x               # 123

a._x = 456          # 這個註釋不是輸出,它不會輸出任何東西

a.x                # getx ...

                  # 456

 

del  a.x           # delx ...

del  a._x          # 這個註釋不是輸出,這句語句將拋出異常

 

a.x = 789          # setx ...

del a.x            # delx ...

 

a._x = 100         # 這個註釋不是輸出,它不會輸出任何東西

a.x               # 100

 

上述代碼講解:

我們定義了一個自定義類A,然後定義了三個成員方法getxsetxdelx,這三個成員方法分別作爲參數傳遞給property內置函數,通過property內置函數隱式地創建了一個property類實例,並把該實例作爲返回值傳遞給了自定義類A的成員x(即類屬性),換句話說,就是x是一個property類實例,其中getxsetxdelx三個成員方法將成爲property類的三個成員方法(fgetfsetfdel)。我們就是通過這個x類屬性來管理_x實例屬性的訪問控制。

 

然後我們通過“a = A( )”語句定義了一個A類的實例a,它將調用構造函數__init__a.x將調用property類的fget成員方法(即A類中的getx方法,下面直接簡單地說“setx”和“delx”,而不再提“fset”和“fdel”);“a.x = 123”語句將調用“setx”方法;“a.x”語句將調用“getx”方法;“a._x”語句將調用A類中的默認的屬性獲取方法(你可以重載__getattr____getattribute__操作符來重載默認的屬性獲取方法,參見上一章節的“重載操作符”);“a._x = 456”語句將調用A類中的默認的屬性賦值方法(你可以重載__setattr__操作符);“del a.x”語句將調用“delx”方法;“del a._x”語句將調用A類中的默認的屬性刪除方法(你可以重載__delattr__操作符)。當執行了“del a.x”語句後,再執行“del a._x”語句時,將引發異常,原因是:“del a.x”語句調用“delx”方法後,a的屬性_x已經被刪除、不存在了,如果再執行“del a._x”語句,就要引發“與屬性不存在性有關的”異常了(刪除一個不存在的屬性)。當接着執行語句“a.x = 789”,將會調用“setx”方法;“del a.x”語句將調用“delx”方法方法;“a._x = 100”語句將調用A類中默認的屬性賦值方法;“a.x”語句將調用“getx”方法。

 

2.2.2 使用裝飾器方法

我們可以使用裝飾器來使自定義類的方法成爲“特性”,裝飾器的接口比較簡潔。

 

property類有三個裝飾器方法(gettersetterdeleter),按照裝飾器的一般使用方法,如果我們把一個方法成爲property類的fget方法,就需要調用propertygetter裝飾器方法。但是,上面我們已經知道,property類是隱式的,所以,我們不能直接使用getter裝飾器(就是想使用也沒有辦法使用——因爲我們不能直接獲取到property類),所以,我們還需要藉助property內置函數。

 

2.2.2.1 使用方法

property不僅是可以作爲內置函數來使用,而且還可以作爲裝飾器來使用。當我們把property內置函數當作裝飾器來使用時,它將會隱式的創建一個property類實例,並調用該實例的getter裝飾器,使得property裝飾器所裝飾的方法成爲property類的fget成員方法。最終,由property將重新綁定它所裝飾的方法,使它所裝飾的方法名重新綁定到新創建的property類實例,而它所裝飾的方法成爲該property類實例的fget方法。

 

假設property所裝飾的方法的名字爲name,那麼,從此以後,我們可以使用name.settername.deleter來裝飾其它的方法,使其成爲fsetfdel成員方法,因爲,此時的nameproperty裝飾器重新綁定到新創建的property類實例,也就是說,name是一個property類實例。注:在用name.settername.deleter裝飾其它方法時,其它方法必須是name。總之,用setterdeleter裝飾器修飾的方法的名字必須和property裝飾器修飾的方法的名字相同。

 

2.2.2.2 例子

注:此例適用於Python2.6中的新式類和Python3中的所有類(因爲Python3中的類都是新式類),不適用於Python2.6舊式類。

 

class Person:

def __init__(self, a_name):

    self._name = a_name

@property

def  name(self):

    return self._name

@name.setter

def  name(self, value):

    self._name = value

@name.deleter

def  name(self):

    del self._name

 

person = Person(“student”)

person.name

person.name = “teacher”

del person.name

 

person._name                # 會拋出異常

person._name = “123”

del  person._name

 

在類Person中,property裝飾器將隱式地創建一個property類實例,然後把它所裝飾的方法(第一個name方法)重新綁定到該property類實例,並把第一個name方法變成該property類實例的fget方法。接着分別用name.settername.deleter裝飾器分別第二個、第三個name方法變成namefsetfdel方法。在類Person定義結束後,我們定義了一個變量person,然後對person對象中name屬性的訪問控制就分別調用相應的三個name方法。實際上,我們對name屬性的操作,最終會轉移到類Person實例的_name屬性身上。

 

2.3 小結

特性是在自定義類的成員中創建一個property類的實例(該property類實例是自定義類的屬性,不是自定義類實例的屬性),然後通過該property類實例來管理自定義類實例中的某個特定的屬性。該property類實例是自定義類中的一個屬性(我們在這稱之爲“類屬性”),當通過自定義類對象訪問它時,是直接訪問該類屬性;當通過自定義類實例訪問它時,該類屬性的三個相應的屬性管理方法(fgetfsetfdel)將會自動調用。在這三個屬性管理方法中,我們可以像其他普通函數或類方法一樣做任何事情,不過,我們一般是把對該類屬性的訪問操作(獲取、賦值、刪除)轉移到自定義類實例的屬性身上。

 

在訪問通過特性創建的類的屬性時,裝飾特性的三個方法會自動調用。

 

特性是在類的屬性(注:該類屬性也是一個對象,因此也是可以有屬性的)中創建一個屬性,對該屬性的所有操作(獲取、賦值、刪除)都將作用在類實例的屬性身上。特性是用來管理類實例的屬性訪問的,而不是管理類本身的屬性訪問的(即,特性不會作用在類屬性上,只會作用在類實例的屬性上)。當然,我們也可以跳過特性而直接訪問類實例的屬性,此時,訪問操作(獲取、賦值、刪除)與特性無關,只受__getattr____getattribute____setattr____delattr__四個重載操作符的影響。當使特性時,其屬性管理只受特性(的三個方法)所影響,不受那些重載操作符的影響。

 

一旦一個自定義類使用了“特性”來管理自定義類實例某個屬性,那麼凡是通過“特性”來對該屬性的所有訪問(獲取、賦值、刪除)都有“特性”的三個成員方法(fgetfsetfdel)來控制。這裏隱含着一個事實,前面沒有說明,這裏必須說明一下:如果“特性”中的三個成員方法有任何一個沒有被定義,那麼,就不能通過“特性”來進行相應的操作;如果進行了這樣的操作,那麼將會引發AttributeError異常(Python找不到相應的方法來調用)。比如:如果沒有定義fset成員方法,那麼就不能通過“特性”來對被該“特性”所管理的屬性進行賦值操作;否則,將引發AttributeError異常。

 

 

 


3 描述符

 

特性只是創建一個特定類型的描述符的一種簡化方式,即:可以把特性看成是簡化了的、受限的描述符;換句話,你可以這樣理解,“特性”是Python內部已經實現好的一個描述符,但它被固定了,也就是說,它沒有你自己定義的描述符的功能那麼強大。因此,筆者不打算再過於詳細的介紹描述符。這看起來可能有點反傳統——按照傳統的,我們應該是先介紹的描述符,再介紹特性,這裏,我們反一傳統,因爲我們已經介紹過“特性”了,如果再詳細的介紹描述符,就有點重複了,而只介紹描述符與“特性”不同的地方。

 

3.1 理解

描述符是一個類,類中定義了三個成員方法:__get____set____delete__。換句話說,只要一個類中定義了這三個方法中任何一個,那麼,這個類就自動的成爲一個描述符。

 

我們已經知道,特性可以看成是一個簡化的描述符。

 

用於”特性“的property類就相當於一個描述符(類),你就可以把它看成一個描述符;property中的三個成員方法就相當於描述符的三個成員方法:fget相當於__get__fset相當於__set__fdel相當於__delete__。由於特性是一個簡化了的描述符,所以,描述符的原理和特性的原理差不多,可以把特性的原理應用於描述符身上。

 

雖然理解描述符可以用特性的原理,但描述符本身沒有內置裝飾器功能。正所謂“禍兮福之所倚”,描述符反而比特性有更多的自由——描述符可以使用所有的OOP功能,因爲“特性”的property類是隱式的(你不能控制它),而描述符類是顯示的,可以由你來控制。因此,我們可以把描述符所管理的自定義類實例屬性的值存儲在描述符類實例中,而不是自定義類實例中(當然,也可以存儲在自定義類實例中,甚至可以同時在兩者中都存儲);而“特性”所管理的自定義類實例屬性的值只能存儲在自定義類實例中。

 

3.2 例子

注:此例適用於Python2.6中的新式類和Python3中的所有類(因爲Python3中的類都是新式類),不適用於Python2.6舊式類。

 

class Name:

def  __init__(self, value):

    self._name = value

def  __get__(self, instance, owner):

    print(‘fetch ...’)

    print(self._name)

    return instance._name

def  __set__(self, instance, value):

    print(‘change ...’)

    instance._name = value

    self._name    = value

def  __delete__(self, instance):

    print(‘remove ...’)

    del instance._name

    del self._name

 

class Person:

def  __init__(self, name):

    self._name = name

name = Name(“” )

 

說明:同“特性”的限制一樣,Person類中的類屬性namePerson類實例的屬性_name不能同樣,因爲這將導致遞歸調用而進入死循環。但是,不像“特性”,描述符也可以把它所管理的屬性放在自身身上,因此,在此例中,爲了展現這一點,筆者把它所管理的屬性同時存儲在了它自身和Person類實例身上。

 

3.3 小結

總之,和特性一樣,描述符類就相當於一個轉接類,把對一個變量(自定義類的類屬性)的訪問控制轉接(或嫁接)到另一個變量(自定義類的實例屬性)身上。

 

特性和描述符一次只能用來管理一個單個的、特定的屬性,既一個特性或描述符對應一個屬性;如果想要管理多個屬性,就必須定義多個特性和描述符。

 


4 三者之間的關係

 

1)特性充當個特定角色,而描述符更爲通用。特性定義特定屬性的獲取、設置和刪除功能。描述符也提供了一個類,帶有完成這些操作的方式,但是,它們提供了額外的靈活性以支持更多任意行爲。實際上,特性真的的只是創建特定描述符的一種簡單方法——即在屬性訪問上運行的一個描述符。編碼上也有區別:特性通過一個內置函數創建,而描述符用一個類來編碼;同樣,描述符可以利用類的所有常用OOP功能,例如:繼承。此外,除了實例的狀態信息,描述符有它們自己的本地狀態,因此,它們可以避免在實例中的名稱衝突。

 

2__getattr____getattribute____setattr____delattr__方法更爲通用:它們用來捕獲任意多的屬性。相反,每個特性或描述符只針對一個特定屬性提供訪問攔截——我們不能用一個單個的特性或描述符捕獲每個屬性獲取。其實現也不同:__getattr____getattribute____setattr____delattr__是操作符重載方法,而特性和描述符是手動賦給類屬性的對象。

<script>window._bd_share_config={"common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"0","bdSize":"16"},"share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];</script>
閱讀(10359) | 評論(0) | 轉發(0) |
給主人留下些什麼吧!~~
評論熱議
發佈了194 篇原創文章 · 獲贊 57 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章