1. 程式人生 > >【Python筆記】裝飾器語法糖(@staticmethod/@classmethod/@property)原理剖析及使用場景說明

【Python筆記】裝飾器語法糖(@staticmethod/@classmethod/@property)原理剖析及使用場景說明

在閱讀一些開源Python庫的原始碼時,經常會看到在某個類的成員函式前,有類似於@staticmethod或@classmethod或@property的語法糖。本質上,它們都是函式裝飾器,只不過通常被用來修飾類成員函式而已。

本筆記旨在說明這些語法糖的用途,關於普通函式裝飾器語法的解釋,可以參考這篇筆記

在解釋這些裝飾器函式前,先來分析下普通成員函式。

1. 類的普通成員函式
對於Python的類,其普通類成員函式的第一個引數預設為當前的類例項,通常的定義形式示例:

class C:
   def __init__(self):  ## NOTICE: 不傳入引數時建立例項會報錯;"self"只是約定俗成的引數名而已,非語法的硬性規定
       pass
我們可以通過print C.__init__檢視函式__init__():
>>> class C():
...     def __init__(self):
...         pass
... 
>>> print C.__init__
<unbound method C.__init__>  ## __init__()是類C的unbound method,即它此時未bound到任何類例項上
>>> c = C()
>>> print c.__init__
<bound method C.__init__ of <__main__.C instance at 0x7fb4346d38c0>>  ## 當類例項建立後,__init__()變成例項的bound method
>>> print C.__init__.__get__ 
<method-wrapper '__get__' of instancemethod object at 0x7fb434772c80> ## 類成員函式預設實現了__get__()方法
總之,對於類的普通成員函式來說,在建立類的具體例項前,它們是unbound method,通過類名呼叫時(如 C.fn()),會報錯;類例項建立後,它們都是例項的bound method,只能通過例項來呼叫(inst = C(); inst.fn())。
關於unbound method函式呼叫報錯的原因,與Python底層實現時對函式名的查詢規則有關。從上面示例程式碼可看到,__init__()函式默認實現了__get__()方法,而根據Python底層規則,當某個物件(Python中萬物皆物件)實現了_get__()/__set__()/__delete__()這3個method中任何一個時,它就成了一個支援descriptor protocal的descriptor。當呼叫c.__init__時(當然,這個是Python直譯器幫我們調
用的,但這並不改變__init__實際上是個普通類函式的事實),根據descriptor invoking規則,其將被轉化為type(c).__dict__['__init__'].__get__(c, type(c))的形式,可見,實際上發生的呼叫類似於C.__init__(c),即類C的例項c會被當作第1個引數傳給普通類成員函式。所以,在定義類普通成員函式時,至少需要有self引數,且類的普通成員函式必須通過類例項來呼叫,而不能通過類名直接呼叫。
關於上面提到的Descriptor Protocol及其對obj attribute查詢規則的影響,強烈建議讀懂這篇文件Descriptor HowTo Guide。不誇張的說,理解Descriptor對我們理解Python程式碼的底層行為有巨大幫助。 2. @classmethod
根據Python文件的說明,classmethod(fn)表明函式fn是類的函式而非類例項的函式,在語法上,它要求fn的函式簽名至少要有1個引數,函式被呼叫時,直譯器會將類作為第1個引數傳給fn。示例如下:
>>> class C():
...     def fn_classmethod(x):
...         print x
...     fn = classmethod(fn_classmethod)
... 
>>> C.fn
<bound method classobj.fn_classmethod of <class __main__.C at 0x7fb4346b6bb0>>
>>> C.fn()
__main__.C
可見,當呼叫classmethod()將fn_classmethod轉換為class method後,我們可以直接通過C.fn來呼叫它,當然,通過C().fn()呼叫也可以。
更需要注意的是,在呼叫時C.fn()時,fn_classmethod()唯一引數x的實參確實是類C(即示例中print出來的__main__.C),該引數是直譯器自動傳入的。
在Python語法中,@classmethod是一種實現自動呼叫classsmethod(fn_classmethod)的語法糖,它實現的功能與上述示例程式碼一致,只是看起來更精簡且更pythonic而已:
>>> class C():
...     @classmethod ## Python的decorator語法會保證classmethod(fn)的自動呼叫
...     def fn(x):
...         print x
... 
>>> C.fn
<bound method classobj.fn of <class __main__.C at 0x7fb4346b6808>>
>>> C.fn()
__main__.C

classmethod的典型使用場合:
1) 直接用類來呼叫函式,而不用藉助類例項
2) 更優雅地實現某個類的例項的構造(類似於Factory Pattern)
通常情況下,類例項是直譯器自動呼叫類的__init__()來構造的,但藉助classmethod可以在直譯器呼叫__init__前實現一些預處理邏輯,然後將預處理後的引數傳入類的建構函式來建立類例項。如dict型別支援的fromkeys()方法就是用classmethod實現的,它用dict例項當前的keysPython pseudo-code,感興趣的話可以去研究一下。
關於實現類例項構造的另一個典型case,可以參考StackOverflow上的這篇問答帖。帖中Best Answer作者給出了一個典型場景,這個場景用非classmethod的方法也可以實現類例項的構造,但藉助classmethod語法,可以實現的更優雅(a. 與__init__構造例項相比,classmethod方法也保證了構造邏輯程式碼複用度而且實現的更精簡,如解析date_as_string為(year, month, day)的程式碼也可以被複用;b. 它不用通過類的例項呼叫,直接用類來呼叫即可構造新的例項;c. 與定義實現相同功能的全域性函式相比,更符合OOP思想;d. 基類被繼承時,基類中定義的classmethod也會被繼承到繼承類中)。

3. @staticmethod
根據Python文件的說明,staticmethod(fn)表明函式fn是類的靜態方法。具體到類定義體內某個函式的定義上,如果該函式想宣告稱靜態成員,則只需在其定義體前加上"@staticmethod"這行,利用裝飾器語法糖來實現staticmethod(cls.fun)的目的。示例如下:
class C(object):
    @staticmethod
    def f(arg1, arg2, ...):
        ...
與classmethod的裝飾器語法糖類似,@staticmethod會自動呼叫staticmethod(f)。
Python中類靜態方法的語義跟C++/Java類似,即類的靜態成員屬於類本身,不屬於類的例項,它無法訪問例項的屬性(資料成員或成員函式)。定義為staticmethod的函式被呼叫時,直譯器不會自動為其隱式傳入類或類例項的引數,它的實際引數列表與呼叫時顯式傳入的引數列表保持一致。
staticmethod的典型應用場景:
若類的某個函式確認不會涉及到與類例項有關的操作時,可以考慮將該函式定義為類的staticmethod。比如,根據業務邏輯,可將全域性函式封裝到一個類中並宣告為staticmethod,這樣看起來更符合OOP思想,具體的例子可以參考這篇Blog 。當然,這只是一種符合OOP的封裝思路,並非意味著碰到全域性函式就一定要這樣做,需要看個人習慣或業務需求。
再次強調:定義為staticmethod型別的函式,其函式體中最好不要涉及與類例項有關的操作(包括建立類例項或訪問例項的屬性),因為一旦涉及到類例項就意味著這些例項名是硬編碼的,在類被繼承的場景下,呼叫這些staticmethod型別的函式會建立基類或訪問基類屬性,而這通常不是業務預期的行為。具體的case可以參考StackOverflow這篇問答帖的第2個高票答案。

4. @property
根據Python文件的說明,property([fget[, fset[, fdel[, doc]]]])為new-style類建立並返回property物件,該物件是根據傳入的引數(fget/fset/fdel)建立的,它可以決定外部呼叫者對new-style類的某些屬性是否具有讀/寫/刪除許可權。以官網文件給出的demo為例:

class C(object): ## NOTICE: property只對new style classes有效
    def __init__(self):
        self._x = None

    def getx(self):
        return self._x

    def setx(self, value):
        self._x = value

    def delx(self):
        del self._x

    x = property(getx, setx, delx, "I'm the 'x' property.")
上述示例中,x是類C的property物件,由於建立時傳入了3個函式物件,故通過訪問該屬性可以實現對self._x的讀/寫/刪除操作。具體而言,呼叫C().x時,直譯器最終會呼叫getx;呼叫C().x = value時,直譯器後最終呼叫setx;呼叫del C().x時,直譯器會最終呼叫delx。
由於property()的第1個引數是fget,利用這一點,可以很容易實現一個只有read-only許可權的類屬性:
>>> class C(object):
...     def __init__(self):
...         self._name = 'name'
...     @property
...     def get_name(self):
...         return self._name
...     
... 
>>> c = C()
>>> c.get_name
'name'
>>> c.get_name = 'new name'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

當然,這裡所說的"read-only",只是指這個屬性名不會被賦值操作重新繫結新物件而已。如果這個屬性名初始繫結的是個可變物件(如list或dict),則即使通過@property裝飾,其繫結的物件的內容也可以通過屬性名來修改。
如果想通過類的例項物件來修改或刪除類例項的屬性,則需用下面的程式碼來實現:

>>> class C(object):
...     def __init__(self):
...         self._name = '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
... 
>>> c = C()
>>> c.name
'name'
>>> c.__dict__
{'_name': 'name'}
>>> c.name = 'new name'
>>> c.__dict__
{'_name': 'new name'}
>>> c.name
'new name'
>>> del c.name
>>> c.__dict__
{}
上述程式碼中,@property、@name.setter及@name.deleter均是裝飾器語法糖,其中:@name.setter中的name指代的是經@property裝飾後的對象(即property(name)返回的名為name但型別為property object的物件),setter是這個名為name的property物件的built-in函式,其目的是通過name.setter(name)為這個property物件提供修改其所屬類的屬性的功能。@name.deleter同理。
至於property物件支援的函式setter()和deleter()的來歷,文件Descriptor HowTo Guide在介紹property原理時給出了property類底層實現的Python偽碼,值得精讀。
從偽碼還
可以看到,property類實現了__get__()、__set__()和__delete__()方法,這意味著property類是個遵循descriptor protocol的data descriptor,根據文件Descriptor HowTo Guide關於Invoking Descriptors的說明,data descriptor會影響直譯器對屬性名的查詢鏈,具體而言,當上面的程式碼中呼叫c.name時,直譯器會將其轉化成type(c).__dict__['name'].__get__(c, type(c))(備註:通過print type(c).__dict__可以驗證,name確實存在於dict中),故這個轉化後的呼叫鏈會呼叫到property物件的__get__()方法,而根據property的實現偽碼,在__get__()中最終會呼叫到類C對應的name()函式。
上面這段話所描述的流程正是property魔法背後的原理。
當然,要想真正理解還需要仔細研究property的python偽碼邏輯。
@property除可以實現屬性的只讀許可權功能外,還可以用在這種場景下:
類屬性已經暴露給外部呼叫者,但由於業務需求,需要針對這個屬性進行業務邏輯的修改(如增加邊界判定或修改屬性計算方法,等等),則此時引入property()或@property語法糖可以在修改邏輯的同時保證程式碼的後向相容,外部呼叫者無需修改呼叫方式。具體的case可以參考這篇Blog中提到的場景。
====================== EOF =====================

相關推薦

Python筆記裝飾語法@staticmethod/@classmethod/@property原理剖析使用場景說明

在閱讀一些開源Python庫的原始碼時,經常會看到在某個類的成員函式前,有類似於@staticmethod或@classmethod或@property的語法糖。本質上,它們都是函式裝飾器,只不過通常被用來修飾類成員函式而已。 本筆記旨在說明這些語法糖的用途,關於普通函式裝

Python筆記1、格式化輸出%用法和format用法

一、格式化輸出1、整數的輸出%o —— oct 八進位制%d —— dec 十進位制%x —— hex 十六進位制1 >>> print('%o' % 20) 2 24 3 >>> print('%d' % 20) 4 20 5 >&

演算法筆記二分法的使用使用目的+模板

今天看完《演算法筆記》裡二分法這個章節,稍微總結一下。 二分法的思想主要就是折半查詢,達到O(logn)的查詢速度。 使用目的或者說使用情景主要有如下三個,下面將依次介紹。 查詢有序序列中是否存在滿足條件的元素 查詢有序序列中滿足條件的第一個元素 對一些函式進行求根

python學習day07 高階函數 裝飾 語法

int lee 場景 return UNC alt image style ima      語法糖對於計算機的運行並沒有任何的好處,但是對於程序員的好處是很大的,方便我們寫代碼,所以稱為糖 #******************************裝飾器******

Python裝飾語法

####裝飾器的固定格式 ##普通版本 def timer(func):     def inner(*args,**kwargs):         '''執行函式之前要做的'''        

設計模式學習筆記裝飾模式

裝飾器模式,顧名思義就是裝。人靠衣裝,馬靠鞍。天生一副臭皮囊,穿金戴銀之後,就顯得與眾不同於。裝飾器模式在不改變原來類結構的基礎上,對原來的類進行了擴充套件。並且這是一種弱耦合形式。 文章目錄            

Python筆記文件常見用法

off 習慣 size readline 追加 run 寫到 文件 內部 關於文件的函數w 寫方式a 追加模式打開(從EOF開始,必要時創建新文件)r+ 以讀寫模式打開w+ 以讀寫模式打開a+ 以讀寫模式打開rb 以二進制讀模式打開wb 以二進制寫模式打開 (參見 w )a

裝飾語法運用

裝飾器語法糖運用 前言:函式名是一個特性的變數,可以作為容器的元素,也可以作為函式的引數,也可以當做返回值。 閉包定義: 內層函式對外層函式(非全域性)變數的引用,這個內層函式就可以成為閉包 在Python中我們用__closure__來檢查函式是否是閉包 def

2、設計模式裝飾模式

前言  IO 包中是用了大量的裝飾器模式   為了弄明白裝飾器模式的本質,我查看了很多資料,發現有很多文章要麼說的很苦澀,要麼舉的例子不恰當。 其實我們可以這樣理解裝飾器模式, 就拿自己舉例子,你把自己裸體的樣子,想象成被裝飾的物件。你的鞋子,你的寸衣,你的外套,你的手錶

Python筆記操作讀取Excel檔案、文字檔案

需求:讀取Excel檔案、替換文字檔案中得指定某個字串並生成新的檔案 原始碼: #encoding:utf-8 # -*- coding: utf-8 -*- #!/usr/bin/env python # -*- coding=utf-8 -*- #Using GPL v2 #Author:

python筆記函式_03_函式的引數

實參:鑑於函式定義中可能包含多個實參,因此函式呼叫可能包含多個實參。 向函式傳遞實參函式的方式很多,可使用位置實參,這就要求實參的順序與形參的順序相同 也可使用關鍵字實參,其中每個實參都有變數名和值組成; 還可使用列表和字典 1.位置實參 定義:你呼叫函式

python筆記使用matplotlib,pylab進行python繪圖

 一提到python繪圖,matplotlib是不得不提的python最著名的繪相簿,它裡面包含了類似matlab的一整套繪圖的API。因此,作為想要學習python繪圖的童鞋們就得在自己的python環境中安裝matplotlib庫了,安裝方式這裡就不多講,方法有很多。

python筆記騰訊動漫爬取更新

目前騰訊動漫閱讀介面的滑動需要切換到指定視窗,即漫畫照片頁所在div 'var q=document.getElementById("mainView").scrollTop ='+str(i*3000) 才可以滑動 import requests import u

Python筆記:nlp作業

1、numpy中函式nditer的作用 https://blog.csdn.net/jiangjiang_jian/article/details/77540599 2、三分鐘帶你對 Softmax 劃重點 https://blog.csdn.net/red_stone1/a

Python筆記Python多執行緒程序如何正確響應Ctrl-C以實現優雅退出

相信用C/C++寫過服務的同學對通過響應Ctrl-C(訊號量SIG_TERM)實現多執行緒C程序的優雅退出都不會陌生,典型的實現偽碼如下: #include <signal.h> int main(int argc, char * argv[])

Python-ML感知學習演算法(perceptron)

1、數學模型   2、權值訓練   3、Python程式碼 感知器收斂的前提是兩個類別必須是線性可分的,且學習速率足夠小。如果兩個類別無法通過一個線性決策邊界進行劃分,要為模型在訓練集上的學習迭代次數設定一個最大值,或者設定一個允許錯誤分類樣本數量的閾值,否則感知器

Python筆記原始碼編譯安裝Python時,如何支援自定義安裝的高版本openssl庫

最近有個小需求想使用Scrapy庫做抓取,但公司開發機作業系統版本老舊,導致系統預設的openssl庫版本也很低(OpenSSL 0.9.7a Feb 19 2003),最終導致安裝Scrapy非常麻煩。趁著元旦假期,決定用自己安裝好的高版本openssl庫作為依賴,重新編

python筆記35-裝飾

time() 結果 就是 檢查 實例對象 返回 定義 增加 imp 前言 python裝飾器本質上就是一個函數,它可以讓其他函數在不需要做任何代碼變動的前提下增加額外的功能,裝飾器的返回值也是一個函數對象。 很多python初學者學到面向對象類和方法是一道大坎,那麽pyth

學習筆記平衡二叉樹AVL樹簡介及其查詢、插入、建立操作的實現

  目錄 平衡二叉樹簡介: 各種操作實現程式碼:   詳細內容請參見《演算法筆記》P319 初始AVL樹,一知半解,目前不是很懂要如何應用,特記錄下重要內容,以供今後review。   平衡二叉樹簡介: 平衡二叉樹由兩位前

學習筆記資料庫優化之索引聚簇索引&非聚簇索引

索引:對資料庫表中一列或多列的值進行排序的一種結構,通過索引可快速訪問資料庫表中的特定資訊,即通過索引對資料列的值進行結構化排序。 其中,索引包含聚簇索引和非聚簇索引 聚簇索引的順序就是資料的物理儲存順序 非聚簇索引的索引順序與資料物理排列順序無關 所以一個表