什麼時候會用到python裝飾器?


這有一份Python官方的裝飾器實例列表,你可以在裡邊看到裝飾器的各種妙用:PythonDecoratorLibrary,基本上你差不多能想到的都有了。

差不多有這麼幾類:

1. 注入參數(提供默認參數,生成參數)

2. 記錄函數行為(日誌、緩存、計時什麼的)

3. 預處理/後處理(配置上下文什麼的)

4. 修改調用時的上下文(線程非同步或者並行,類方法)


裝飾器其實也就是一個函數,一個用來包裝函數的函數,返回一個修改之後的函數對象。經常被用於有切面需求的場景,較為經典的有插入日誌、
性能測試、事務處理等。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量函數中與函數功能本身無關的雷同代碼並繼續重用。概括的講,裝
飾器的作用就是為已經存在的對象添加額外的功能。

首先來看看一個小例子:

def alan():
print("alan speaking")

這是一個很簡單的函數,就是輸出「alan speaking」。

現在我再來改改這個函數,同時輸出今天的時間:

def alan():
print("alan speaking")
date = datetime.utcnow()
print(date)

這個時候,我有另外的函數,tom()、john()、Mary()也要輸出類似句子。怎麼做?再寫一個date在tom函數里?為了減少重複寫代碼,我們可以這樣做

我重新定義一個函數:

def date(func):
func()
date = datetime.utcnow()
print(date)

def alan():
print("alan speaking")

def tom():
print("tom speaking")

date(tom)

邏輯上不難理解,而且運行正常。 但是這樣的話,我們每次都要將一個函數傳遞寫入date中,如果我要實現直接調用alan()或者tom()就可以輸出結果,又要避免重複寫相同的代碼,應該怎麼修改?

def date(func):
def wrapper():
func()
date = datetime.utcnow()
print(date)
return wrapper

def alan():
print("alan speaking")

def tom():
print("tom speaking")

tom = date(tom)
tom()

我們只需要在定義tom以後調用tom之前,加上tom= date(tom),就可以達到目的。

這也就是裝飾器的概念,看起來像是tom被date裝飾了。在在這個例子中,函數進入和退出時 ,被稱為一個橫切面(Aspect),這種編程方式被稱為面向切面的編程(Aspect-Oriented Programming)。

定義好裝飾器後,我們就可以通過@語法使用了,使用裝飾器輸出我們想要的結果。

def date(func):
def wrapper():
func()
date = datetime.utcnow()
print(date)
return wrapper

@date
def alan():
print("alan speaking")
@date
def tom():
print("tom speaking")

tom()

如上所示,這樣我們就可以減去:tom = date(tom)這一句了,直接調用tom()即可得到想要的結果。

如果我們有其他的類似函數,我們可以繼續調用decorator來修飾函數,而不用重複修改函數或者增加新的封裝。這樣,我們就提高了程序的可重複利用性,並增加了程序的可讀性。

裝飾器還有更大的靈活性,例如帶參數的裝飾器

在上面的裝飾器調用中,比如@date,該裝飾器默認它後面的函數是唯一的參數。裝飾器的語法允許我們調用decorator時,提供其它參數,比如@decorator(a)。這樣,就為裝飾器的編寫和使用提供了更大的靈活性。

def pre_date(pre):
def date(func):
def wrapper():
func()
date = datetime.utcnow()
print(pre + str(date))
return wrapper
return date

@pre_date("Today is :")
def alan():
print ("alan speaking")

@pre_date("I am Tom :")
def tom():
print ("tom speaking")

alan()

上面的pre_date是允許參數的裝飾器。它實際上是對原有裝飾器的一個函數封裝,並返回一個裝飾器。我們可以將它理解為一個含有環境參量的閉包。當我
們使用@pre_date("Today is :")調用的時候,Python能夠發現這一層的封裝,並把參數傳遞到裝飾器的環境中。

裝飾器的參數除了允許基本類型,還允許 類 作為參數:

from datetime import datetime
# Create your tests here.
class params:
def __init__(self):
print("init called")

@staticmethod
def released():
print("release this class")

def pre_date(cls):
def date(func):
def wrapper():
print("before %s ,we called (%s)." % (func.__name__,cls))
try:
func()
date = datetime.utcnow()
print (date)
finally:
cls.released()
return wrapper
return date

@pre_date(params)
def alan():
print ("alan speaking")

@pre_date(params)
def tom():
print ("tom speaking")

alan()

這裡把類params作為參數傳入裝飾器中,其中我們用到了@staticmethod這個內置的裝飾器,作用是把類中定義的實例方法變成靜態方法。

內置的裝飾器有三個,分別是staticmethodclassmethodproperty,作用分別是把類中定義的實例方法變成靜態方法、類方法和類屬性.。

再來看看類裝飾器,相比函數裝飾器,具有靈活度大,高內聚、封裝性等優點。使用類裝飾器還可以依靠類內部的 __call__ 方法,當使用 @ 形式將裝飾器附加到函數上時,就會調用此方法。

class Foo(object):
def __init__(self, func):
self._func = func

def __call__(self):
print ("class decorator runing")
self._func()
print ("class decorator ending")

@Foo
def bar():
print ("bar")

bar()


分享一篇我的筆記:Python裝飾器筆記(知乎這裡排版我排的不好,有意仔細看的可以點前面的鏈接跳轉到簡書)

============

一.函數裝飾器

1.從Python內層函數說起

首先我們來探討一下這篇文章所講的內容Inner Functions - What Are They Good For?(中文版)

使用內層函數的三個好處

  • 封裝
  • 貫徹DRY原則
  • 閉包和工廠函數

1.封裝

def outer(num1):
def inner_increment(num1): # hidden from outer code
return num1 + 1
num2 = inner_increment(num1)
print(num1, num2)

inner_increment(10) #不能正確運行
# outer(10) #可以正常運行

這樣把內層函數從全局作用域隱藏起來,不能直接調用。

使用這種設計模式的一個主要優勢在於:在外部函數中對全部參數執行了檢查,你可以在內部函數中跳過全部的檢查過程。

2.貫徹DRY原則

比如,你可能寫了一個函數用來處理文件,並且你希望它既可以接受一個打開文件對象或是一個文件名:

def process(file_name):
def do_stuff(file_process):
for line in file_process:
print(line)
if isinstance(file_name, str):
with open(file_name, "r") as f:
do_stuff(f)
else:
do_stuff(file_name)

3.閉包和工廠函數

閉包無非是使內層函數在調用時記住它當前環境的狀態。初學者經常認為閉包就是內層函數,而且實際上它是由內層函數導致的。閉包在棧上「封閉」了局部變數,使其在棧創建執行結束後仍然存在。

def generate_power(number):

# define the inner function ...
def nth_power(power):
return number ** power

# ... which is returned by the factory function
return nth_power

&>&>raise_two = generate_power(2)

&>&>print(raise_two(7))

128

外層函數接受一個參數number=2,然後生成一個nth_power()函數,該函數只接受一個單一的參數power,其中包含number=2

返回的函數被賦值給變數raise_two,我們可以通過raise_two來調用函數並傳遞變數。

換句話說,閉包函數「初始化」了nth_power()函數並將其返回。現在無論你何時調用這個新返回的函數,它都會去查看其私有的快照,也就是包含number=2的那一個。

2.裝飾器

「裝飾器」是一個很著名的設計模式,經常被用於有切面需求的場景,較為經典的有插入日誌、性能測試、事務處理

裝飾器其實就是一個工廠函數,它接受一個函數為參數,然後返回一個新函數,其閉包中包含了原函數

1.簡單裝飾器

def deco(func):
def wrapper():
print "start"
func() #調用函數
print "end"
return wrapper

@deco
def myfun():
print "run"

myfun()

由於裝飾器函數返回的是原函數的閉包wrapper,實際上被裝飾後的函數就是wrapper,其運行方式就和wrapper一樣。

相當於

myfun=deco(myfun)

2.裝飾一個需要傳遞參數的函數

def deco(func):
def wrapper(param):
print "start"
func(param)
print "end"
return wrapper

@deco
def myfun(param):
print "run with param %s"%(param)

myfun("something")

這種情況下,仍然返回wrapper,但是這個wrapper可以接受一個參數,因此這樣的裝飾器只能作用於接受一個參數的函數

3.裝飾任意參數的函數

def deco(func):
def warpper(*args,**kw):
print "start"
func(*args,**kw)
print "end"
return warpper

@deco
def myfun1(param1):
print "run with param %s"%(param1)

@deco
def myfun2(param1,param2):
print "run with param %s and %s"%(param1,param2)

myfun1("something")
myfun2("something","otherthing")

# start
# run with param something
# end
# start
# run with param something and otherthing
# end

兩個函數可以被同樣一個裝飾器所裝飾

4.帶參數的裝飾器

裝飾器接受一個函數作為參數,這個毋庸置疑。但是有時候我們需要裝飾器接受另外的參數。此時需要再加一層函數,實際上是定義了一個生成裝飾器的工廠函數,調用它,搭配需要的參數,來返回合適的裝飾器。

def log(text):
def deco(func):
def wrapper(*args,**kw):
print text
func(*args,**kw)
print text + " again"
return wrapper
return deco

@log("hello")
def myfun(message):
print message

myfun("world")

# hello
# world
# hello again

這裡分兩步

  • log=log("hello"),把返回的deco函數賦值給log,此時log相當於其包含text=「hello」的閉包
  • myfun=log(myfun),相當於把myfun傳入了deco函數,並且返回wrapper,並賦值給myfun,此時myfun相當於其裝飾後的閉包。

整體來看是myfun=log("hello")(myfun)

5.裝飾器帶類參數

# -*- coding:gbk -*-
"""""示例8: 裝飾器帶類參數"""

class locker:
def __init__(self):
print("locker.__init__() should be not called.")

@staticmethod
def acquire():
print("locker.acquire() called.(這是靜態方法)")

@staticmethod
def release():
print(" locker.release() called.(不需要對象實例)")

def deco(cls):
"""""cls 必須實現acquire和release靜態方法"""
def _deco(func):
def __deco():
print("before %s called [%s]." % (func.__name__, cls))
cls.acquire()
try:
return func()
finally:
cls.release()
return __deco
return _deco

@deco(locker)
def myfunc():
print(" myfunc() called.")

myfunc()
myfunc()

關於wrapper的返回值

上面的代碼中,我們的wrapper函數都沒有返回值,而是在wrapper中直接調用了func函數,這麼做的目的是要在函數運行前後列印一些字元串。而func函數本事也只是列印字元串而已。

但是這麼做有時會違背func函數的初衷,比如func函數確實是需要返回值的,那麼其裝飾後的函數wrapper也應該把值返回。

我們看這樣一段函數:

def deco(func):
def warpper(*args,**kw):
print "start"
func(*args,**kw)#直接調用,無返回值
print "end"
return warpper

@deco
def myfun(param):
return 2+param

sum=myfun(2) #期望紀錄返回值並列印
print sum

結果,並沒有返回值

&>&>
start
end
None

因此我們需要wrapper把函數結果返回:

def deco(func):
def warpper(*args,**kw):
print "start"
result=func(*args,**kw)#紀錄結果
print "end"
return result #返回
return warpper
@deco
def myfun(param):
return 2**param

sum=myfun(2) #這裡其實是sum=result
print sum

當然,如果不是為了在func前後列印字元串,也可以把func直接返回

一個實際例子:統計函數執行時間

from time import time,sleep
def timer(func):
def warpper(*args,**kw):
tic=time()
result=func(*args,**kw)
toc=time()
print "%f seconds has passed"%(toc-tic)
return result
return warpper

@timer
def myfun():
sleep(2)
return "end"

print myfun()

# 2.005432 seconds has passed
# end

關於裝飾器裝飾過程中函數名稱的變化

當裝飾器裝飾函數並返回wrapper後,原本myfun的__name__就改變了

from time import time,sleep

def timer(func):
def warpper(*args,**kw):
tic=time()
result=func(*args,**kw)
toc=time()
print func.__name__
print "%f seconds has passed"%(toc-tic)
return result
return warpper

@timer
def myfun():
sleep(2)
return "end"

myfun()
print myfun.__name__ #wrapper

# myfun
# 2.003399 seconds has passed
# warpper

這樣對於一些依賴函數名的功能就會失效,而且也不太符合邏輯,畢竟wrapper對於我們只是一個中間產物

from time import time,sleep
import functools

def timer(func):
@functools.wraps(func)
def warpper(*args,**kw):
tic=time()
result=func(*args,**kw)
toc=time()
print func.__name__
print "%f seconds has passed"%(toc-tic)
return result
return warpper

@timer
def myfun():
sleep(2)
return "end"

myfun()
print myfun.__name__ #wrapper

# myfun
# 2.003737 seconds has passed
# myfun

導入模塊import functools,並且用@functools.wraps(func)裝飾wrapper即可

3.Flask中的@app.route()裝飾器

Things which aren"t magic - Flask and @app.route - Part 1

Things which aren"t magic - Flask and @app.route - Part 2

class NotFlask():
def route(self, route_str):
def decorator(f):
return f

return decorator

app = NotFlask()

@app.route("/")
def hello():
return "Hello World!"

route是NotFlask類的一個方法,並且其實際上是一個裝飾器工廠,這裡我們並沒有裝飾我們的函數,裝飾器僅僅返回了函數的引用而沒有裝飾它。

class NotFlask():
def __init__(self):
self.routes = {}

def route(self, route_str):
def decorator(f):
self.routes[route_str] = f
return f

return decorator

app = NotFlask()

@app.route("/")
def hello():
return "Hello World!"

現在給裝飾器初始化一個字典,在我們傳入參數生產裝飾器route的時候,把函數存入字典響應位置,key為url字元串,value為相應函數。

不過此時,我們並不能訪問這個內部的視圖函數,我們需要一個方法來獲取相應的視圖函數。

class NotFlask():
def __init__(self):
self.routes = {}

def route(self, route_str):
def decorator(f):
self.routes[route_str] = f
return f

return decorator

def serve(self, path):
view_function = self.routes.get(path)#獲取相應函數
if view_function:
return view_function()#返回函數
else:
raise ValueError("Route "{}"" has not been registered".format(path))

app = NotFlask()

@app.route("/")
def hello():
return "Hello World!"

然後我們可以這樣,通過url字元串來訪問相應的視圖函數

app = NotFlask()

@app.route("/")
def hello():
return "Hello World!"

print app.serve("/")

#&>&>Hello World!

小結

Flask路由裝飾器的主要功能,就是綁定url到相應的函數。

(如何訪問視圖函數其實是HTTP伺服器的一部分)

當然,目前的url綁定還太死板,我們需要url能夠加入可變參數

下面我們要實現從url中識別出參數

app = Flask(__name__)

@app.route("/hello/&")
def hello_user(username):
return "Hello {}!".format(username)

首先我們要利用命名捕獲組,從url中識別參數

route_regex = re.compile(r"^/hello/(?P&.+)$")
match = route_regex.match("/hello/ains")

print match.groupdict()

當然,我們需要一個方法來把輸入的url轉化為相應的正則表達式

def build_route_pattern(route):
route_regex = re.sub(r"(&)", r"(?P1.+)", route)
return re.compile("^{}$".format(route_regex))

print build_route_pattern("/hello/&")

class NotFlask():
def __init__(self):
self.routes = []

# Here"s our build_route_pattern we made earlier
@staticmethod
def build_route_pattern(route):
route_regex = re.sub(r"(&)", r"(?P1.+)", route)
return re.compile("^{}$".format(route_regex))

def route(self, route_str):
def decorator(f):
# Instead of inserting into a dictionary,
# We"ll append the tuple to our route list
route_pattern = self.build_route_pattern(route_str)
self.routes.append((route_pattern, f))

return f

return decorator

與之前的代碼不同,字典被移除了,取而代之的是一個列表,然後我們把生成的正則表達式和相應的函數作為元組放到列表裡。

同樣,我們需要一個方法來返回視圖函數,當然,還有捕獲匹配組的字典,我們需要它來傳遞正確的參數

def get_route_match(path):
for route_pattern, view_function in self.routes:
m = route_pattern.match(path)
if m:
return m.groupdict(), view_function

return None

最終結果:

class NotFlask():
def __init__(self):
self.routes = []

@staticmethod
def build_route_pattern(route):
route_regex = re.sub(r"(&)", r"(?P1.+)", route)
return re.compile("^{}$".format(route_regex))

def route(self, route_str):
def decorator(f):
route_pattern = self.build_route_pattern(route_str)
self.routes.append((route_pattern, f))

return f

return decorator

def get_route_match(self, path):
for route_pattern, view_function in self.routes:
m = route_pattern.match(path)
if m:
return m.groupdict(), view_function

return None

def serve(self, path):
#查找和path匹配的視圖函數以及捕獲組字典
route_match = self.get_route_match(path)
if route_match:
kwargs, view_function = route_match
return view_function(**kwargs)#捕獲組字典作為函數參數
else:
raise ValueError("Route "{}"" has not been registered".format(path))

使用方法:

app = NotFlask()

@app.route("/hello/&")
def hello_user(username):
return "Hello {}!".format(username)

print app.serve("/hello/ains")

&>&>Hello ains!

小結

裝飾階段:

  • 裝飾器工廠route接受url字元串,生成一個合適的裝飾器
  • 裝飾器裝飾視圖函數,生成url字元串對應的正則表達式模板,連同視圖函數組成元組,存放在列表中。然後把函數返回。

調用階段:

  • app.serve並傳入url的時候,首先在列表中查找,依次進行匹配,是否有符合該模式的路徑和視圖函數
  • 有則返回相應獲取捕獲組字典和視圖函數
  • 將字典作為參數,返回該視圖函數的運行結果


註解就是把一個函數轉換成另一個函數,把一個類轉換為另一個類,之類。需要這種轉換的情形很多,舉個簡單的例子,某個組件要求傳入一個函數,這個函數只接受一個參數,是實際參數的列表;但是如果所有的這類函數都這麼定義的話很不方便,所以可以用修飾器轉換一下:

def arglist(f):
@functools.wraps(f)
def func(args):
return f(*args)
return func

@arglist
def myfunc(a, b):
return a + b

這樣的語法比較優雅,可以看到myfunc的功能就是寫的那樣,但是實際的介面或者實現細節上通過修飾器進行了修改,閱讀和運行起來都比較方便。


再你想改變一個已有函數的功能的時候可以用。 可以添加功能在調用函數之前和之後,這樣就可以生成一個基於之前函數的新函數

例如:

def addOne(func):
def wraper(*args,**kwargs):
saySmthing = "Result :"
return saySmthing +" "+ str(func(*args,**kwargs))
return wraper

@addOne
def func(a,b):
return a+b

print(func(10,20))


無非是將之前的編程式或者聲明式(硬編碼)函數功能擴展改為註解式的,通過註解標籤完成類似功能擴展比如註解式的緩存…當然標準的裝飾器我覺得還是像JAVA中的流比如用戶態增加緩存buffer減少與內核buffer的交互


說其中一個吧

@classmethod

修飾類成員函數,修飾後類似C++的類全局函數,可以不實例化對象就可調用。


偷懶的時候。

裝飾器很容易在某個流程中注入一些代碼(類似aop),可以集中控制原有函數或者類的行為,可以方便的做全局單例,異常處理等。


裝飾器是在Python 2.4中加入的,它使得函數和方法封裝(接收一個函數並返回增強版本的一個函數)更容易閱讀和理解。原始的使用場景是可以將方法在定義的首部將其定義為類的方法或靜態方法。

常見的裝飾器模式包括:

  • 參數檢查;
  • 緩存;
  • 代理;
  • 上下文提供者。

Python高級編程 (豆瓣) P37-46

如果你不想看英文文檔的話,可以看看這本書。


就像作戰里的輔助軍一樣。正規軍幹活,輔助軍搞點邊角料工作。跟你程序邏輯相關部分的放在正規軍函數里。而有些輔助邏輯跟主要邏輯無關,又具有很高的重複性,放在主要函數裡面會覺得不清晰。做成裝飾器,用起來美觀大方,符合python的美學。


AOP編程


看一下flask框架。裝飾器用的飛起。特別是請求路由。


比如我寫了一個網頁,但是我要求必須先登錄才能訪問


# decorator factory
from functools import wraps
def max_result(threshold):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if result &> threshold:
print("Result is too big ({0}). Max allowed is {1}.".format(result, threshold))
return result
return wrapper
return decorator

@max_result(75)
def cube(n):
return n ** 3
@max_result(100)
def square(n):
return n ** 2
@max_result(1000)
def multiply(a, b):
return a * b
print(cube(5))
print(square(5))
print(multiply(50,40))


舉個實際的例子:PyQt中綁定事件和事件處理程序的時候

@QtCore.pyqtSlot()

def on_btnOpen_clicked(self):

pass

這樣就不用顯示的connect了


裝飾器比較多的用於面向切片編程(AOP)

當然很多時候都被拿來當做路由

總之有想要批量的注入代碼的時候就可以考慮使用


前些天看了兩個演算法,搞明白以後,我用自己的想法實現了一次。然後又用他的實現實現了一次。為了比較哪個更耗時。就做了個裝飾器,功能很簡單就是計算函數執行時間。

但是我說不上來理論根據是什麼,我只知道我會用。


推薦閱讀:

python如何恰當的判斷多個值是否在list中?
python3.4版本 scipy庫函數怎麼安裝?
有的python內置函數怎麼就一個pass?
想知道大家都用python寫過哪些有趣的腳本?
python網頁爬蟲是非法的嗎?

TAG:Python | Python3x | Python入門 | Python開發 |