Python 中的 decorator 設計的價值和初衷何在?

@Decorator

的實現機制是closure .

但是decorator 之後,原來的函數名就不能用了(被覆蓋)。但是我還是不是很清楚在實際工程中decorator 的重要性體現在哪裡。是否有一個non trivial 的例子能體現一下?

能再深入講解一下Python closure 就更好了。多謝各位老師!


++++++++++++++++++++++++++

這幾天在琢磨下PHP的函數式編程,感覺對閉包有了新的體會,於是想更新下這個答案。

++++++++++++++++++++++++++

嗯,既然要求一個實際的例子,那就來個例子熱下身(類比Django):

假設給某一個網站寫好了一系列視圖函數helloword(),goodpython(),evilbat(),……訪問特定的URL就執行對應的這些視圖函數。

然後代碼寫好了,忽然覺得應該給這些視圖函數都加上身份驗證功能,避免隨隨便便的一個遊客就可以訪問你的URL。

這時候咋操作?

方案一:修改每一個視圖函數在其中加上要求登陸的邏輯?

如果是我,我肯定不會,因為這樣會違背了DRY原則。

方案二:嘗試以OOP的方式用裝飾器模式搞定這些。

嗯,確實實現了封裝變化,降低耦合,但是那樣會相當麻煩,得寫好長一段代碼。

方案三:利用裝飾器語法。

大致思路為:

先寫一個裝飾器函數,比如require_login()

然後再敲如下代碼:

#注意有很多情況下不需要()。既然是偽代碼,那就不糾結參數

@require_login()

helloword()

@require_login()

goodpython()

@require_login()

evilbat()

那如果哪天你又想開放這些URL,不需要登陸就可以訪問這些URL怎麼辦?

那就把@require_login去掉。

如果題主注意觀察Django之類代碼,會在其中發現相當比例的這樣的寫法。
我的意思是,這種寫法並非我的杜撰或者YY,而是實際中常用的寫法。

所以,總結下裝飾器的作用:

裝飾器實現代碼分離、功能解耦、……。簡而言之,用來裝飾的

也許題主和我當初一樣對閉包、裝飾器比較疑惑,不妨來思考一個搞怪的問題:

在不改動原foo()內部實現代碼的情況下,既要不改動原函數foo()的名稱,又想在foo()原有工作前後追加工作量,如何實現?

以下是裝飾器(閉包)的思路:

  1. 定義一個新函數,此函數會接受函數對象作為輸入參數,以確保能執行其功能

  2. 新函數內定義一個和舊函數參數列表一致的包裝函數,同時添加欲追加的工作量,甚至修改舊函數。
  3. 新函數返回值設置為包裝函數

  4. 把舊函數對象傳遞給新函數去執行,返回值(包裝函數)賦值到舊函數名上
  5. 用戶以舊函數之名調用包裝函數。

輸出結果為:

before calling : miss Brz

while calling : have fun with Brz

after calling : say good bye to Brz

這就是函數裝飾的基本原理與實現。

總結來說,裝飾器函數只做三件事:

  • 接收一個函數作為參數,
  • 然後嵌套一個包裝函數,
  • 再返回嵌套函數對象。

這個包裝函數會做三件事

  • 接受和原函數有一致的參數列表
  • 執行原函數
  • 執行附加功能

++++++++++++++++++++++++++++++++++++++

2014年9月24日凌晨追加:

基於上面的認識,我們甚至可以在PHP中寫出類似的裝飾器,

打開終端:

vim testdecorator.php

輸入:

然後執行:

:!php %

得到:

所以,可以順帶說一句:網上有許多人說的「PHP要實現裝飾只能依靠OOP的裝飾器模式或者runkit」,這話其實是不確切的。

以上在PHP中再現這個裝飾器實現過程,也說明一個問題,裝飾器並非是Python獨有。

1、裝飾器不是Python設計出來的,是各個語言通用的編程思想。

2、Python設計出這個@語法糖,只是讓」定義裝飾器、把裝飾器函數調用原函數再把結果賦值為原函數的對象名「這個過程更簡單。

thats all


初衷我不知道,對我而言,decorator 的價值是封裝和重用,幾乎和面向對象的價值一樣。

封裝的主要用途是隱藏,隱藏調用者不需要了解的實現,或是不需要用到的介面參數等。

此外還能修改介面、返回值或函數的行為,而這些細節在大部分情況下是無需調用者去了解的。

重用就更不用說了,有一堆函數都要加上同一邏輯代碼,使用 decorator 就能復用這些代碼,不需要複製粘貼。

實際的例子很多,built-in functions 里就有 classmethod()、staticmethod() 和 property() 這 3 個,contextlib 和 functools 這 2 個標準模塊里也有很實用的 decorators。

此外,常見的基本用途還有緩存返回值、捕捉並忽略異常、計時、記錄日誌、unittest.mock 等。

我在 web 開發時,也經常需要對某些介面進行用戶驗證,這些代碼也可以被封裝和重用。實現可參考 Tornado 的 tornado.web.authenticated()。

總之,如果你覺得一類函數調用起來麻煩,想修改部分邏輯或介面,就可以考慮用 decorator。


是為了速食麵向切面編程吧


For declarative programming.


可以先看看《設計模式》裡面的「裝飾模式」。

發現有人已經說了這個 = =、、、


Decorator pattern 是design pattern中的一種。累加式地修改某個函數的功能。


So how (and why) did this syntax come into being? What was the inspiration behind decorators? Well, when static and class methods were added to Python in 2.2, the idiom required to realize them was clumsy, confusing, and makes code less readable, i.e.

class MyClass(object):
def staticFoo():
.
.
staticFoo = staticmethod(staticFoo)

引自《Core Python》


我用專欄里的一篇文章來答題。

專欄鏈接:給妹子講python,歡迎大家關注,提意見!

python有一個很有意思的地方,就是def函數可以嵌套在另一個def函數之中。調用外層函數時,運行到的內層def語句僅僅是完成對內層函數的定義,而不會去調用內層函數,除非在嵌套函數之後又顯式的對其進行調用。

x = 99

def f1():
x = 88
def f2():
print(x)
f2()

f1()

88

可以看出,f1中的嵌套變數x覆蓋了全局變數x=99,然後f2中的本地變數按照引用規則,就引用了x=88。

下面我們來說說嵌套作用域的一個特殊之處:

本地作用域在函數結束後就立即失效,而嵌套作用域在嵌套的函數返回後卻仍然有效。

def f1():
x = 88
def f2():
print(x)
return f2

action = f1()
action()

88

這個例子非常重要,也很有意思,函數f1中定義了函數f2,f2引用了f1嵌套作用域內的變數x,並且f1將函數f2作為返回對象進行返回。最值得注意的是我們通過變數action獲取了返回的f2,雖然此時f1函數已經退出結束了,但是f2仍然記住了f1嵌套作用域內的變數名x。

上面這種語言現象稱之為閉包:一個能記住嵌套作用域變數值的函數,儘管作用域已經不存在。

這裡有一個應用就是工廠函數,工廠函數定義了一個外部的函數,這個函數簡單的生成並返回一個內嵌的函數,僅僅是返回卻不調用,因此通過調用這個工廠函數,可以得到內嵌函數的一個引用,內嵌函數就是通過調用工廠函數時,運行內部的def語句而創建的。

def maker(n):
k = 8
def action(x):
return x ** n + k
return action

f = maker(2)
print(f)

&.action at 0x00000000021C51E0&>

再看一個例子:

def maker(n):
k = 8
def action(x):
return x ** n + k
return action

f = maker(2)
print(f(4))

24

這裡我們可以看出,內嵌的函數action記住了嵌套作用域內得兩個嵌套變數,一個是變數k,一個是參數n,即使後面maker返回並退出。我們通過調用外部的函數maker,得到內嵌的函數action的引用。這種函數嵌套的方法在後面要介紹的裝飾器中會經常用到。這種嵌套作用域引用,就是python的函數能夠保留狀態信息的主要方法了。

這裡接著說說另一個關鍵字nonlocal

本地函數通過global聲明對全局變數進行引用修改,那麼對應的,內嵌函數內部想對嵌套作用域中的變數進行修改,就要使用nonlocal進行聲明。

def test(num):
in_num = num
def nested(label):
nonlocal in_num
in_num += 1
print(label, in_num)
return nested

F = test(0)
F(a)
F(b)
F(c)

a 1
b 2
c 3

這裡我們可以看到幾個點,我們在nested函數中通過nonlocal關鍵字引用了內嵌作用域中的變數in_num,那麼我們就可以在nested函數中修改他,即使test函數已經退出調用,這個「記憶」依然有效。

再最後一個例子:

def test(num):
in_num = num
def nested(label):
nonlocal in_num
in_num += 1
print(label, in_num)
return nested

F = test(0)
F(a)
F(b)
F(c)

G = test(100)
G(mm)

a 1
b 2
c 3
mm 101

多次調用工廠函數返回的不同內嵌函數副本F和G,彼此間的內嵌變數in_num是彼此獨立隔離的。

@大水哥 @卡卡 @楊揚 @xythu @崔亞楠


比如現在有許多個函數,這些函數執行之前都需要驗證某個東西,比如用戶是否已登錄什麼的(這是另一套邏輯了,可能有多長時間未操作下線什麼的),那麼我就可以把驗證登錄這一段代碼寫成一個裝飾器,作用到每個函數func上,或者我需要在每個函數執行完成之後做某一個動作,這樣也可以寫成裝飾器。面向切面編程,個人感覺就是不用把函數寫成多個羅列函數的形式了,好看一些


推薦閱讀:

TAG:Python | 計算機 | Python入門 |