當前位置:
首頁 > 知識 > 12步輕鬆搞定Python裝飾器

12步輕鬆搞定Python裝飾器

譯者:寒尋

譯文:http://www.cnblogs.com/imshome/p/8327438.html

原文:https://dzone.com/articles/understanding-python

呵呵!作為一名教python的老師,我發現學生們基本上一開始很難搞定python的裝飾器,也許因為裝飾器確實很難懂。搞定裝飾器需要你了解一些函數式編程的概念,當然還有理解在python中定義和調用函數相關語法的一些特點。

我沒法讓裝飾器變得簡單,但是通過一步步的剖析,我也許能夠讓你在理解裝飾器的時候更自信一點。因為裝飾器很複雜,這篇文章將會很長(自己都說很長,還敢這麼多廢話blablabla...前戲就不繼續翻譯直接省略了)


1. 函數

在python中,函數通過 關鍵字、函數名和可選的參數列表定義。通過 關鍵字返回值。我們舉例來說明如何定義和調用一個簡單的函數:

方法體(當然多行也是一樣的)是必須的,通過縮進來表示,在方法名的後面加上雙括弧 就能夠調用函數


2. 作用域

在python中,函數會創建一個新的作用域。python開發者可能會說函數有自己的命名空間,差不多一個意思。這意味著在函數內部碰到一個變數的時候函數會優先在自己的命名空間裡面去尋找。讓我們寫一個簡單的函數看一下 和 有什麼不同:

內置的函數 返回一個包含所有python解釋器知道的變數名稱的字典(為了乾淨和洗的白白的,我省略了python自行創建的一些變數)。在#2我調用了函數 把函數內部 裡面的內容列印出來。我們能夠看到,函數 有自己獨立的命名空間,雖然暫時命名空間裡面什麼都還沒有。


3. 變數解析規則

當然這並不是說我們在函數裡面就不能訪問外面的全局變數。在python的作用域規則裡面,創建變數一定會一定會在當前作用域里創建一個變數,但是訪問或者修改變數時會先在當前作用域查找變數,沒有找到匹配變數的話會依次向上在閉合的作用域裡面進行查看找。所以如果我們修改函數 的實現讓它列印全局的作用域里的變數也是可以的:

在#1處,python解釋器會嘗試查找變數 ,當然在函數的 裡面是找不到的,所以接著會去上層的作用域裡面去查找。

但是另一方面,假如我們在函數內部給全局變數賦值,結果卻和我們想的不一樣:

我們能夠看到,全局變數能夠被訪問到(如果是可變數據類型(像list,dict這些)甚至能夠被更改)但是賦值不行。在函數內部的#1處,我們實際上 了一個局部變數, 全局作用域中的同名變數。我們可以通過列印出局部命名空間中的內容得出這個結論。我們也能看到在#2處列印出來的變數 的值並沒有改變。


4. 變數生存周期

值得注意的一個點是,變數不僅是生存在一個個的命名空間內,他們都有自己的生存周期,請看下面這個例子:

1處發生的錯誤不僅僅是因為 導致的(儘管這是拋出了NameError的錯誤的原因)它還和python以及其它很多編程語言中函數調用實現的機制有關。在這個地方這個執行時間點並沒有什麼有效的語法讓我們能夠獲取變數 的值,因為它這個時候壓根不存在!函數 的命名空間隨著函數調用開始而開始,結束而銷毀。


5. 函數參數

python允許我們向函數傳遞參數,參數會變成本地變數存在於函數內部。

在Python里有很多的方式來定義和傳遞參數,完整版可以查看 python官方文檔。我們這裡簡略的說明一下:函數的參數可以是必須的 或者是可選的 。

在#1處我們定義了函數 ,它有一個位置參數 和一個命名參數 。在#2處我們能夠通過常規的方式來調用函數,儘管有一個命名參數,但參數依然可以通過位置傳遞給函數。在調用函數的時候,對於命名參數 我們也可以完全不管就像#3處所示的一樣。如果命名參數沒有接收到任何值的話,python會自動使用聲明的默認值也就是 。需要注意的是我們不能省略第一個位置參數 , 否則的話就會像#5處所示發生錯誤。

目前還算簡潔清晰吧, 但是接下來可能會有點令人困惑。python支持函數調用時的命名參數(個人覺得應該是命名實參)。看看#5處的函數調用,我們傳遞的是兩個命名實參,這個時候因為有名稱標識,參數傳遞的順序也就不用在意了。

當然相反的情況也是正確的:函數的第二個形參是 ,但是我們通過位置的方式傳遞值給它。在#2處的函數調用 ,我們把 傳遞給了第一個參數,把 傳遞給了第二個參數,儘管第二個參數是一個命名參數。

桑不起,感覺用了好大一段才說清楚這麼一個簡單的概念:函數的參數可以有 和 。這意味著在函數的定義和調用的時候會稍稍在理解上有點兒不同。我們可以給只定義了位置參數的函數傳遞命名參數(實參),反之亦然!如果覺得不夠可以查看官方文檔


6. 嵌套函數

Python允許創建嵌套函數。這意味著我們可以在函數裡面定義函數而且現有的作用域和變數生存周期依舊適用。

這個例子有一點兒複雜,但是看起來也還行。

想一想在#1發生了什麼:python解釋器需找一個叫 的本地變數,查找失敗之後會繼續在上層的作用域裡面尋找,這個上層的作用域定義在另外一個函數裡面。對函數 來說,變數 是一個本地變數,但是如先前提到的一樣,函數 可以訪問封閉的作用域(至少可以讀和修改)。

在#2處,我們調用函數 ,非常重要的一點是, 也僅僅是一個遵循python變數解析規則的變數名,python解釋器會優先在 的作用域裡面對變數名 查找匹配的變數.


7. 函數是python世界裡的一級類對象

顯而易見,在python里函數和其他東西一樣都是對象。(此處應該大聲歌唱)啊!包含變數的函數,你也並不是那麼特殊!

你也許從沒有想過,你定義的函數居然會有屬性。沒辦法,函數在python裡面就是對象,和其他的東西一樣,也許這樣描述會太學院派太官方了點:在python里,函數只是一些普通的值而已和其他的值一毛一樣。

這就是說你尅一把函數想參數一樣傳遞給其他的函數或者說從函數了裡面返回函數!如果你從來沒有這麼想過,那看看下面這個例子:

這個例子對你來說應該不會很奇怪。 和 是非常普通的兩個python函數,接受兩個值,返回一個計算後的結果值。

在#1處你們能看到準備接收一個函數的變數只是一個普通的變數而已,和其他變數一樣。

在#2處我們調用傳進來的函數:" 代表著調用的操作並且調用變數包含的值。

在#3處,你們也能看到傳遞函數並沒有什麼特殊的語法。"函數的名稱只是很其他變數一樣的表標識符而已。你們也許看到過這樣的行為:"python把頻繁要用的操作變成函數作為參數進行使用,像通過傳遞一個函數給內置排序函數的 參數從而來自定義排序規則。那把函數當做返回值回事這樣的情況呢:

這個例子看起來也許會更加的奇怪。在#1處我把恰好是函數標識符的變數 作為返回值返回出來。這並沒有什麼特殊的語法:"把函數 返回出來,否則它根本不可能會被調用到。"還記得變數的生存周期嗎?每次函數 被調用的時候,函數 都會被重新定義,如果它不被當做變數返回的話,每次執行過後它將不復存在。

在#2處我們捕獲住返回值 - 函數 ,將它存在一個新的變數 里。我們能夠看到,當對變數 進行求值,它確實包含函數 ,而且我們能夠對他進行調用。初次看起來可能會覺得有點奇怪,但是理解起來並不困難是吧。堅持住,因為奇怪的轉折馬上就要來了(嘿嘿嘿嘿,我笑的並不猥瑣!)

8. 閉包

我們先不急著定義什麼是閉包,先來看看一段代碼,僅僅是把上一個例子簡單的調整了一下:

在上一個例子中我們了解到, 作為一個函數被 返回,保存在一個變數 ,並且我們能夠對它進行調用 。不過它會正常的運行嗎?我們先來看看作用域規則。

所有的東西都在python的作用域規則下進行工作:" 是函數 里的一個局部變數。當函數 在#1處列印 的時候,python解釋器會在 內部查找相應的變數,當然會找不到,所以接著會到封閉作用域裡面查找,並且會找到匹配。

但是從變數的生存周期來看,該怎麼理解呢?我們的變數 是函數 的一個本地變數,這意味著只有當函數 正在運行的時候才會存在。根據我們已知的python運行模式,我們沒法在函數 返回之後繼續調用函數 ,在函數 被調用的時候,變數 早已不復存在,可能會發生一個運行時錯誤。

萬萬沒想到,返回的函數 居然能夠正常工作。Python支持一個叫做 的特性,用人話來講就是,嵌套定義在 裡面的函數能夠記住它在被定義的時候它所處的封閉命名空間。這能夠通過查看函數的 屬性得出結論,這個屬性裡面包含封閉作用域裡面的值(只會包含被捕捉到的值,比如 ,如果在 裡面還定義了其他的值,封閉作用域裡面是不會有的)

記住,每次函數 被調用的時候,函數 都會被重新定義。現在變數 的值不會變化,所以每次返回的函數 會是同樣的邏輯,假如我們稍微改動一下呢?

從這個例子中你能夠看到 - 被函數記住的封閉作用域 - 能夠被用來創建自定義的函數,本質上來說是一個 。事實上我們並不是傳遞參數 或者 給函數 ,我們實際上是創建了能夠列印各種數字的各種自定義版本。

閉包單獨拿出來就是一個非常強大的功能, 在某些方面,你也許會把它當做一個類似於面向對象的技術: 像是給 服務的構造器, 像一個私有變數。使用閉包的方式也有很多:你如果熟悉python內置排序方法的參數 ,你說不定已經寫過一個 方法在排序一個列表的列表的時候基於第二個元素而不是第一個。現在你說不定也可以寫一個 方法,接收一個索引值來返回一個完美的函數,傳遞給排序函數的參數 。

不過,我們現在不會用閉包做這麼low的事(⊙o⊙)…!相反, ,寫一個高大上的 !


9. 裝飾器

裝飾器其實就是一個閉包,把一個函數當做參數然後返回一個替代版函數。我們一步步從簡到繁來瞅瞅:

仔細看看上面這個裝飾器的例子。們定義了一個函數 ,它只有一個 的參數,在他裡面我們定義了一個嵌套的函數 。 會列印一串字元串,然後調用 ,在#1處得到它的返回值。在 每次調用的時候 的值可能會不一樣,但是不管 的之如何,我們都會調用它。最後, 返回 的值 - 我們通過調用在#2處存儲在變數 裡面的函數能夠看到被列印出來的字元串以及返回值 ,而不是期望中調用函數 得到的返回值 。

我們可以認為變數 是函數 的一個裝飾版本,一個加強版本。事實上如果打算寫一個有用的裝飾器的話,我們可能會想願意用裝飾版本完全取代原先的函數 ,這樣我們總是會得到我們的"加強版" 。想要達到這個效果,完全不需要學習新的語法,簡單地賦值給變數 就行了:

現在,任何怎麼調用都不會牽扯到原先的函數 ,都會得到新的裝飾版本的 ,現在我們還是來寫一個有用的裝飾器。

想像我們有一個庫,這個庫能夠提供類似坐標的對象,也許它們僅僅是一些x和y的坐標對。不過可惜的是這些坐標對象不支持數學運算符,而且我們也不能對源代碼進行修改,因此也就不能直接加入運算符的支持。我們將會做一系列的數學運算,所以我們想要能夠對兩個坐標對象進行合適加減運算的函數,這些方法很容易就能寫出:

如果不巧我們的加減函數同時也需要一些邊界檢查的行為那該怎麼辦呢?搞不好你只能夠對正的坐標對象進行加減操作,任何返回的值也都應該是正的坐標。所以現在的期望是這樣:

我們期望在不更改坐標對象 的前提下 減去 的值是 , 加上 的值是 。與其給每個方法都加上參數和返回值邊界檢查的邏輯,我們來寫一個邊界檢查的裝飾器!

這個裝飾器能想先前的裝飾器例子一樣進行工作,返回一個經過修改的函數,但是在這個例子中,它能夠對函數的輸入參數和返回值做一些非常有用的檢查和格式化工作,將負值的 和 替換成 。

顯而易見,通過這樣的方式,我們的代碼變得更加簡潔:將邊界檢查的邏輯隔離到單獨的方法中,然後通過裝飾器包裝的方式應用到我們需要進行檢查的地方。

另外一種方式通過在計算方法的開始處和返回值之前調用邊界檢查的方法也能夠達到同樣的目的。但是不可置否的是,使用裝飾器能夠讓我們以最少的代碼量達到坐標邊界檢查的目的。事實上,如果我們是在裝飾自己定義的方法的話,我們能夠讓裝飾器應用的更加有逼格。


10. 使用@標識符將裝飾器應用到函數

Python2.4支持使用標識符 將裝飾器應用在函數上,只需要在函數的定義前加上 和裝飾器的名稱。在上一節的例子里我們是將原本的方法用裝飾後的方法代替:

這種方式能夠在任何時候對任意方法進行包裝。但是如果我們自定義一個方法,我們可以使用 進行裝飾:

需要明白的是,這樣的做法和先前簡單的用包裝方法替代原有方法是一毛一樣的, python只是加了一些語法糖讓裝飾的行為更加的直接明確和優雅一點。


11.args and *kwargs

我們已經完成了一個有用的裝飾器,但是由於硬編碼的原因它只能應用在一類具體的方法上,這類方法接收兩個參數,傳遞給閉包捕獲的函數。如果我們想實現一個能夠應用在任何方法上的裝飾器要怎麼做呢?再比如,如果我們要實現一個能應用在任何方法上的類似於計數器的裝飾器,不需要改變原有方法的任何邏輯。這意味著裝飾器能夠接受擁有任何簽名的函數作為自己的被裝飾方法,同時能夠用傳遞給它的參數對被裝飾的方法進行調用。

非常巧合的是Python正好有支持這個特性的語法。可以閱讀 Python Tutorial 獲取更多的細節。當定義函數的時候使用了 ,意味著那些通過位置傳遞的參數將會被放在帶有 前綴的變數中, 所以:

第一個函數 只是簡單地講任何傳遞過來的位置參數全部列印出來而已,你們能夠看到,在代碼#1處我們只是引用了函數內的變數 , 僅僅只是用在函數定義的時候用來表示位置參數應該存儲在變數 裡面。Python允許我們制定一些參數並且通過 捕獲其他所有剩餘的未被捕捉的位置參數,就像#2處所示的那樣。

操作符在函數被調用的時候也能使用。意義基本是一樣的。當調用一個函數的時候,一個用 標誌的變數意思是變數裡面的內容需要被提取出來然後當做位置參數被使用。同樣的,來看個例子:

1處的代碼和2處的代碼所做的事情其實是一樣的,在#2處,python為我們所做的事其實也可以手動完成。這也不是什麼壞事, 要麼是表示調用方法大的時候額外的參數可以從一個可迭代列表中取得,要麼就是定義方法的時候標誌這個方法能夠接受任意的位置參數。

接下來提到的 會稍多更複雜一點, 代表著鍵值對的參數字典,和 所代表的意義相差無幾,也很簡單對不對:

當我們定義一個函數的時候,我們能夠用 來表明,所有未被捕獲的關鍵字參數都應該存儲在 的字典中。如前所訴, he 並不是python語法的一部分,但在定義函數的時候,使用這樣的變數名算是一個不成文的約定。和 一樣,我們同樣可以在定義或者調用函數的時候使用 。


12. 更通用的裝飾器

有了這招新的技能,我們隨隨便便就可以寫一個能夠記錄下傳遞給函數參數的裝飾器了。先來個簡單地把日誌輸出到界面的例子:

請注意我們的函數 ,它能夠接受任意數量和類型的參數並把它們傳遞給被包裝的方法,這讓我們能夠用這個裝飾器來裝飾任何方法。

隨便調用我們定義的哪個方法,相應的日誌也會列印到輸出窗口,和我們預期的一樣。

題圖:pexels,CC0 授權。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 編程派 的精彩文章:

10G+AI人工智慧/複雜系統/數據挖掘/深度學習/Python資料

TAG:編程派 |