標籤:

如何理解Python裝飾器?

python裝飾器的理解 - 天行健 - 博客頻道 - CSDN.NET

如果你是新手的話,可能會有很大的幫助。


可以看我的這篇文章python裝飾器-代碼山


在語法層面支持aop


我是新手。看本回答前,請務必弄清閉包的含義。

我覺得下面的例子(相當於自帶參數的裝飾器可以實現的功能)會更好理解一些。

只要你懂閉包,這段代碼肯定看得懂。

def external_function(i, j):
k = i - j
def decorator(func):
def wrapper(*arg):
print this func is %s%(func.__name__)
print k * func(*arg)
return wrapper
return decorator

def my_func(x,y):
return x + y

if __name__ == __main__:
external_function(10,5)(my_func)(10,2)

裝飾器(decorator)函數把目標函數(需要裝飾的函數)作為它的參數。

裝飾器里的wrapper(封裝函數)把目標函數的參數作為它的參數。

如果把上面的例子寫成真正的裝飾器,就是:

def external_function(i, j):
k = i - j
def decorator(func):
def wrapper(*arg):
print this func is %s%(func.__name__)
print k * func(*arg)
return wrapper
return decorator

@external_function(10,5)
def my_func(x,y):
return x + y

if __name__ == __main__:
my_func(10, 2)

上面代碼中的裝飾器語法糖 @xxx 中的 xxx 必須是個函數。

@xxx 的意思就是賦值語句 func = xxx(func)

func就是目標函數,也就是@xxx下面定義的函數。在這個例子里就是指my_func。

但這個語法糖 @xxx 必須寫在定義目標函數之前。

裝飾器的語法糖必須寫在定義目標函數之前,這在邏輯上很難接受。

因為不用語法糖,而用普通的賦值語句 func = xxx(func) ,代碼是這樣的:

def external_function(i, j):
k = i - j
def decorator(func):
def wrapper(*arg):
print this func is %s%(func.__name__)
print k * func(*arg)
return wrapper
return decorator

def my_func(x,y):
return x + y

my_func = external_function(10,5)(my_func)

if __name__ == __main__:
my_func(10,2)

看到沒,要用普通賦值語句的話,這條語句是在定義目標函數之後才能寫。

而@xxx 這個裝飾器語法糖得寫在前面。

-----------------------------未完----------------------------

-----------------------------------------------

我不喜歡評論,尤其是反面評論,因為我是無償回答問題,所以我不想再無償回複評論。

如果你覺得我的觀點不對,請自己寫個回答給答主,甚至你可以在裡面拿我做反例。

我不在意。


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

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

【要點搶先看】

1.神奇的裝飾器到底是什麼
2.裝飾器的用法和語法糖
3.裝飾器如何添加參數

裝飾器是python里的一個非常有意思的部分,他用於封裝函數代碼,顯式的將封裝器應用到被封裝的函數上,從而使得他們選擇加入到裝飾器指定的功能中。對於在函數運行前處理常見前置條件(例如確認授權),或在函數運行後確保清理(輸出清除或異常處理),裝飾器都非常有用。

是不是感覺聽不明白,太繞了!

簡單來說,裝飾器就是實現了一個通用的功能,然後將這個通用的功能應用到不同的、需要使用這個功能的函數上,從而避免每次都在不同函數上反覆寫相同的功能代碼。

裝飾器的本質是一個函數,他接受被裝飾的函數作為位置參數,裝飾器通過使用該參數來執行某些操作,然後返回一個函數引用,這個函數可以是原始函數,或者是另外一個函數。

我們舉例子說明,裝飾器是這樣的函數,他們接受被裝飾的可調用函數作為唯一的參數,並且返回一個可調用函數,

registry = []
def register(decorated):
registry.append(decorated)
return decorated

def foo():
return 3

foo = register(foo)
print(registry[0])

&

register方法是一個簡單的裝飾器,它把被裝飾的函數添加到一個列表中,然後這裡是將未改變的被裝飾函數返回,可以看出,裝飾器一般是傳入被裝飾函數的引用,然後經過一些指定的處理,最後返回值也是一個函數引用。

還有一種更簡單的語法形式:

裝飾器的語法糖:我們這裡看到的對foo進行裝飾的方法是運用

foo = register(foo)語句,還有一種簡單的用法是在聲明函數的位置應用裝飾器,從而使得代碼更容易閱讀,並且讓人立刻意識到使用了裝飾器

registry = []
def register(decorated):
registry.append(decorated)
return decorated

@register
def foo(x=3):
return x

@register
def bar(x=5):
return 5

for func in registry:
print(func())

3
5

再看一個更複雜、更一般化的裝飾器函數。裝飾器的本質是在執行原有函數(被裝飾的函數)的同時,再加上一些額外的功能。

def requires_ints(decorated):
def inner(*args, **kwargs):
kwarg_values = [i for i in kwargs.values()]
for arg in list(args) + kwarg_values:
if not isinstance(arg, int):
raise TypeError({} only accepts integers as arguments.format(decorated.__name__))
return decorated(*args, **kwargs)
return inner

在這個裝飾器函數requires_ints我們可以看到,他定義了一個內嵌函數inner,這個內嵌函數的參數首先收集被裝飾函數的所有參數,然後對其進行判斷,判斷其是否為整數類型(這就是裝飾器添加的額外功能),然後再調用被裝飾的函數本身,最後將這個內嵌函數返回。因此當我們再用原函數名進行調用的時候,原來的被裝飾函數的引用就能指向這個新的內嵌函數,就能在實現原函數功能的基礎上,加上附加的功能了。

同時,我們再提煉一下這裡面的幾個重難點:

第一,requires_ints中,decorated這個變數是內嵌作用域的變數,在他調用退出後,返回的inner函數是可以記住這個變數的。

第二,python不支持函數的參數列表的多態,即一個函數名只能對應唯一的參數列表形式。

第三,在內嵌函數內部調用被裝飾函數的時候,使用了解包參數,關於這*args, **kwargs,的參數形式,前面章節中細講過。

那我們也用這個裝飾器來裝飾一個函數。

@requires_ints
def foo(x,y):
print(x+y)

foo(3,5)

8

這裡將名稱foo賦給inner函數,而不是賦給原來被定義的函數,如果運行foo(3,5),將利用傳入的這兩個參數運行inner函數,inner函數執行類型檢查,然後運行被裝飾方法,如果傳入的不是整形數,例如下面這個例子,那麼裝飾器的附加功能就會進行類型檢查:

@requires_ints
def foo(x,y):
print(x+y)

foo(a,5)

Traceback (most recent call last):
File "E:/12homework/12homework.py", line 15, in &
foo(a,5)
File "E:/12homework/12homework.py", line 7, in inner
raise TypeError({} only accepts integers as arguments.format(decorated.__name__))
TypeError: foo only accepts integers as arguments

其次內嵌的函數和被裝飾的函數的參數形式必須完全一樣,這裡用的*args, **kwargs概況函數參數的一般形式,因此也是完全對應的。

最後說說裝飾器參數

最後來介紹這個複雜一些的話題,裝飾器參數。之前我們列舉的常規例子里,裝飾器只有一個參數,就是被裝飾的方法。但是,有時讓裝飾器自身帶有一些需要的信息,從而使裝飾器可以用恰當的方式裝飾方法十分有用。

這些參數並不是和被裝飾的函數並列作為參數簽名,而是在原有裝飾器的基礎上額外再增加一層封裝,那麼,實質是這個接受其他參數的裝飾器並不是一個實際的裝飾器,而是一個返回裝飾器的函數。

最終返回的內嵌函數inner是最終使用indent和sort_keys參數的函數,這沒有問題

import json

def json_output(indent=None, sort_keys=False):
def actual_decorator(decorated):
def inner(*args, **kwargs):
result = decorated(*args, **kwargs)
return json.dumps(result, indent=indent, sort_keys=sort_keys)
return inner
return actual_decorator

@json_output(indent=8)
def do_nothing():
return {status:done,func:yes}

print(do_nothing())

{
"status": "done",
"func": "yes"
}

我們在這裡詳細解釋說明的是操作順序,看上去我們使用的是@json_output(indent=8),作這和之前的裝飾器語法糖看上去有些不同,實際上這個不是最終的裝飾器函數,通過調用json_output(indent=8),返回函數指針actual_decorator,這個函數才是真正放在@後的裝飾器函數,原始的被裝飾函數最終獲得了內涵更豐富的inner函數對象,完成了裝飾過程,值得一提的是,所謂的裝飾器參數最終傳給了最內層的inner函數。

記住,在定義裝飾器函數後,真正的裝飾器函數只有一個參數,那就是被裝飾的函數指針,而有其他參數的函數實質上只是裝飾器的外圍函數,他可以依據參數對裝飾器進行進一步的定製。一句話:一個函數不可能接受被裝飾的方法,又接受其他參數

在語法糖中@func這種不帶括弧的,就是直接使用裝飾器函數進行裝飾,如果是@func()帶括弧的,實質上是先調用func()函數返回真正的裝飾器,然後再用@進行調用。

@劉志軍 @黃哥 @蕭井陌 @廖雪峰


雖然是編程新手,也嘗試來答一個,選自我的博客

裝飾器的定義

在不同語境下,裝飾器有不一樣的含義,我大致認為有3種定義:

  1. 一種把另一個對象當參數的對象
  2. 一種語法糖
  3. 面對對象設計中的裝飾器模式

之所以這裡特別指出,是因為在很多文章和書中

把裝飾器定義成一個函數,有些更擴展一些,把裝飾器定義成一個callable對象

對剛開始學習的讀者來說這麼解釋或許不錯,但在使用python的過程中,我們發現裝飾器可以是

  1. 函數
  2. 類 (例: classmethodproperty)
  3. 類方法 (例: FLask類的route)

如果說類方法有__call__方法,能符合上面callable對象的定義的話

classmethod類和property類可沒__call__方法,所以我認為更準確的定義是我上面說的第1,2點

裝飾器的作用

廣義上說,更優雅地處理對象,相比taget = decorate(target)@decorate可要優雅多了

狹義上說,各種功能:

  1. 禁止函數運行 (剛想到,寫個不return原函數的裝飾器去裝飾某函數,這個函數不就不運行了嘛,不曉得是否有人會這麼干)
  2. 記錄函數的運行狀況
  3. 緩存計算結果
  4. 檢查/修改參數
  5. 裝逼?
  6. 其他


一句話總結:

a被b裝飾,a函數就等同於,以a做參數b返回的函數

翻譯一下:

a(函數調用,帶括弧)被b(@b)裝飾,a(函數名)就等同於,以a(函數名,不加括弧)做參數b(函數調用,帶括弧)返回的函數(函數名,不加括弧)


裝飾器本質上也是函數,接受函數對象來作為參數,並在裝飾器的內部來調用接受的函數對象完成相關的函數調用。也可以這樣理解,為了方便在幾個不同函數調用之前或者之後完成相關的統一操作,注意是完成統一的操作,可以傳參數使得裝飾器不完全一樣,後面會講到。最重要的應用如工程應用上記錄相關的內部調用介面的流水日誌,不同的介面需要統一的樣式,所以可以用裝飾器來實現。先簡單看一下示例:

from time import ctime

def deco(func):
def decorator(*args, **kwargs):
print([%s] %s() is called % (ctime(), func.__name__))
return func(*args, **kwargs)
return decorator

@deco
def foo():
print(Hello, Python)

foo()

在如上示例中,定義了一個裝飾器,其中參數func需要函數的對象,返回值是decorator函數,其中decorator函數的返回值正是func的返回值。該裝飾器的功能就是在函數調用之前,列印了函數調用的時間和函數名。

裝飾器的使用過程很簡單,通過註解@符號標註一下即可。這本質上相當於foo = deco(foo) 的嵌套調用。

這裡面,你又遇到了 *args 和 **kwargs,它們可以組合接收任意函數參數。不理解的可以翻看Python 中的 *args 和 **kwargs 。

裝飾器也可以堆疊起來,即對某個函數使用多個裝飾器,比如:

from time import ctime

def deco1(func):
def decorator1(*args, **kwargs):
print([%s] %s() is called % (ctime(), func.__name__))
return func(*args, **kwargs)
return decorator1

def deco2(func):
def decorator2(*args, **kwargs):
print([%s] %s() is called % (ctime(), func.__name__))
return func(*args, **kwargs)
return decorator2

@deco2
@deco1
def foo():
print(Hello, Python)

foo()

會輸出什麼呢?可以先分析一下。

運行一下,輸出如下:

[Fri Jul 21 15:15:53 2017] decorator1() is called

[Fri Jul 21 15:15:53 2017] foo() is called

Hello, Python

是否跟你想的一樣?在嵌套調用的過程中,foo = deco2(deco1(foo)),所以先返回 deco1(foo) 的函數名字即 decorator1, 後返回 foo 的函數名。

裝飾器本身也可以傳入參數,使得在統一的過程中帶點奇特的色彩,如:

from time import ctime
def deco(tag):
def decorator(func):
def wrapper(*args, **kw):
print([%s] %s() is called, Tag is %s. % (ctime(), func.__name__, tag))
return func(*args, **kw)
return wrapper
return decorator

@deco(Python)
def foo():
print(Hello, Python)

@deco(Java)
def bar():
print(Hello, Python)

foo()
bar()

讓我們簡單分析下這個裝飾器,deco函數接受的是一個str對象tag,當執行deco(Python) 後返回的是decorator函數,此函數需要接受一個函數對象,同時返回wrapper函數,而wrapper函數的結果就是func函數的返回值。說的可能有點繞,但理一下會覺得非常簡單。

最後說明一下的是,由於加入了裝飾器,函數的__name__和__doc__等信息都發生了變化:

from time import ctime

def deco(func):
def decorator(*args, **kwargs):
decorator for func
print([%s] %s() is called % (ctime(), func.__name__))
return func(*args, **kwargs)
return decorator

@deco
def foo():
function: foo
print(Hello, Python)

foo.__name__
foo.__doc__

運行的結果如下:

decorator

decorator for func

由此可見,加入裝飾器改變了函數內部的相關屬性。如何避免此問題呢?Python中有專門的包來避免這種轉換:functools.wraps。示例如下:

from time import ctime
import functools

def deco(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
decorator for func
print([%s] %s() is called % (ctime(), func.__name__))
return func(*args, **kwargs)
return decorator

@deco
def foo():
function: foo
print(Hello, Python)

foo.__name__
foo.__doc__

運行結果如下:

foo

function: foo

這樣就保留了原先函數的屬性。小編在實際的工作中一般也是加入此項功能的。

歡迎大家關注同名微信公眾號 Python那些事。


@劉志軍 講解使得我從對裝飾器模糊的理解狀態,進階到了感覺十分清晰;我似乎感受到了python裝飾器的本質是,或者說主要描述方式是,通過內嵌函數的方式,並且返回的是函數的定義,對於最裡面的函數,無需出現return關鍵詞,也能實現@形式的註解;

為什麼要使用@ 形式的註解表示裝飾器呢?是為了代碼簡單地重用。而python如何實現可以以@形式的註解呢,是通過內嵌函數的形式,返回的是函數的定義,注意這裡返回的是函數定義,而不是已經把函數執行了。

有一種重寫了函數的定義感覺。


裝飾器設計模式就是增強方法的功能,比如說在方法執行前做點什麼,在方法執行後再做點什麼。

Python 的高階函數簡單理解成可以返回函數的函數。

所以Python的裝飾器就可以理解成具備裝飾器(模式)的高階函數。

先把裝飾器模式和高階函數理解了,就可以理解Python的裝飾器了。

當然還有可以有參數的裝飾器,修改.__name__等屬性這些細節再注意一下。

其實說增強方法功能,不能說它就是語法層面的AOP,AOP的重點不在於產生代理去增強方法的功能,而在於定義切點,切面。


李冬答主寫的很明了了

學代碼和日語的英文系,膜拜

我也是學代碼和日語的會計系大二

準備一會面壁去了

今天剛看到裝飾器,寫一下自己的理解好了

以下全是連偽代碼都不是的偽偽代碼

用書上例子的實現來:

假如我寫了一個好厲害的函數

&>&>&>def 加法 (*arg):

return 很厲害的過程

現在我想要在他運行的時候同時列印出

函數名

參數

結果

來進行測試

那麼我只要把函數改成

&>&>&>def 加法 ():

結果=很厲害的過程

print()

return 很厲害的過程的結果

這樣就好了

那麼問題來了

如果我一高興寫了一百個醬的函數

都要進行測試呢

那這樣改豈不是很麻煩?

而且如果我要單純的使用這些函數

不需要在過程中列印這些信息呢?

沒錯

按照《python語言及其應用》所述

python裝飾器可以使你在不修改源代碼的情況下,修改已經存在的函數

那麼,為了搞一搞這100個函數

我們寫個以函數為參數的函數,作為裝飾器

&>&>&>def 裝飾器(函數):

def 新函數(*arg):

結果=很厲害的過程

列印各種測試信息

return 很厲害的過程的結果

return 新函數

好啦,那麼裝飾器就寫好了那麼如何使用這個裝飾器呢?有以下兩種方法

1.我們只需要來個賦值

&>&>&>測試函數 = 裝飾器(加法)

沒錯,我們已經把自己寫的「加法」函數,作為參數帶入了裝飾器這個函數里,之後

&>&>&>測試函數(1, 2)

就可以得到之後的結果

(

列印的各種測試信息

3

第二種方法,在寫函數的時候艾特

所以請在之前寫好裝飾器

那麼先艾特一下我們的裝飾器咯

&>&>&>@裝飾器

· · · def 加法 (*arg):

· · · return 很厲害的過程

· · ·

那麼我們在運行函數的時候,就可以

&>&>&>加法(1, 2)

列印的各種測試信息

3

噹噹當

這樣就可以省去我們重複寫代碼的過程

也減少了刪改代碼的次數

應該是一個對出錯的很好的內部控制吧

那麼為什麼上面的裝飾器用了雙層函數呢

外層函數 裝飾器的參數是我們要裝飾的函數

而內層函數 新函數的參數,則我們要裝飾的函數的參數,也就是原函數所要處理的參數

那麼為什麼要內層使用閉包呢?

其實我也是今天看到的閉包

說實話不是很懂,自己測試了一下

沒有閉包的結果是字元串之類的對象類型

閉包的結果的對象類型則是函數

具體的呢,寶寶也不懂,成了老司機再說

成了老司機還有人問我再說

獻醜了

以上

話說我有答歪嗎。。。


推薦閱讀:

用Python做地圖投影 - 多面孔的世界
Python到底是個啥?
【Python基礎教程】閱讀&實驗報告〖一〗
為什麼在Python中沒有專門的char數據類型呢?
python初學者,如果你連這樣的習題寫不出代碼,該怎麼辦?

TAG:Python | 裝飾器 |

分頁阅读: 1 2