python裝飾器原理及應用

在python編程中,我們經常看到下面的函數用法:

with open("test.txt", "w") as f:
f.write("hello world!")
習慣了java開發的python初學者,心里不免犯嘀咕:

文件open操作之后,為什么沒有close,不怕文件描述符資源耗盡嗎?

文件write操作沒有異常捕獲,不怕中斷程序主流程嗎?如果您也有同樣的憂慮,那太正常不過了,起碼說明您是一位有“開發原則”的人,同時也說明您對其背后的原理了解存在盲區。如果是這種情況,本文強烈建議您耐心閱讀完以下章節。為了系統的闡述其背后的奧秘,本文從最基本的函數講起。

關于函數
在Python中,一切皆為對象,包括函數。

def foo(num):
return num + 1

value = foo(3)
print(value)

def bar():
print("bar")

foo = bar
foo()
上面簡單的函數例子中,可以總結幾點信息:

函數名字foo可以作為變量名字,指向函數對象

函數名字foo作為對象,可以賦值給變量value

函數名字foo可以作為變量名字,指向其他函數bar

函數名字(函數對象)通過括號調用函數 不僅如此,作為對象的函數也具有一般對象的特性,比如:

函數作為參數

def foo(num):
return num + 1

def bar(fun):
return fun(3)

value = bar(foo)
print(value)
函數作為返回值

def foo():
return 1

def bar():
return foo #注意這里沒有括號

print(bar()) # <function foo at 0x10a2f4140>

print(bar()()) # 1

等價于

print(foo()) # 1
函數嵌套

def outer():
x = 1
def inner():
print(x)
inner() # 注意這里有括號,直接被調用

outer() #
閉包

def outer(x):
def inner():
print(x)

return inner #沒括號,不被直接調用

closure = outer(1) # closure就是一個閉包
closure()
同樣是嵌套函數,只是稍改動一下,把局部變量 x 作為參數了傳遞進來,嵌套函數不再直接在函數里被調用,而是作為返回值返回,這里的 closure就是一個閉包,本質上它還是函數,閉包是引用了自由變量(x)的函數(inner)。

裝飾器

def outer(func):
def inner():
print("before call fun")
func()
print("after call fun")
return inner

def foo():
print("foo")

new_foo = outer(foo)
new_foo()
outer 函數其實就是一個裝飾器:一個帶有函數作為參數并返回一個新函數的閉包.本質上裝飾器也是函數,outer 函數的返回值是 inner 函數。

注:上面示例中的裝飾器函數調用,可以用語法糖@簡寫為:

@outer
def foo():
print("foo")

foo()
我們進一步抽象裝飾器:

def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper

@decorator
def function():
print("hello, decorator")
可見,通過裝飾器,可以讓代碼更加簡練、優雅、可讀性更強。

裝飾器進階
類裝飾器 基于類裝飾器的實現,必須實現 call 和init 兩個內置函數。 init :接收被裝飾函數 call:實現裝飾邏輯。以日志打印為例:

class logger(object):
def init(self, func):
self.func = func

def __call__(self, *args, **kwargs):
    print("[INFO]: the function {func}() is running..."\
        .format(func=self.func.__name__))
    return self.func(*args, **kwargs)

@logger
def say(something):
print("say {}!".format(something))

say("hello")
裝飾類的裝飾器 裝飾器不僅可以裝飾函數,還可以裝飾類,比如如果想改寫類的方法的部分實現,除了通過類繼承重載,還可以通過裝飾器,實現如下:

def log_getattribute(cls):
# Get the original implementation
orig_getattribute = cls.getattribute

# Make a new definition
def new_getattribute(self, name):
    print('getting:', name)
    return orig_getattribute(self, name)

# Attach to the class and return
cls.__getattribute__ = new_getattribute
return cls

Example use

@log_getattribute
class A:
def init(self,x):
self.x = x
def spam(self):
pass

a = A(42)
print(a.x)
示例中,通過裝飾器函數log_getattribute修改原有類的屬性方法getattribute的指向來達到目的:通過指向新的方法new_getattribute,在新的方法中在調用原來方法之前,添加額外邏輯。

偏函數 使用裝飾器的前提是裝飾器必須是可被調用的對象,比如函數、實現了call 函數的類等,即將介紹的偏函數其實也是 callable 對象。在了解偏函數之前,先舉個例子:計算 100 加任意個數字的和。我們用parital函數解決這個問題:

from functools import partial

def add(*args):
return sum(args)

add_100 = partial(add, 100)
print(add_100(1, 2)) # 103

print(add_100(1, 2, 3)) # 106
跟上面的例子那樣,偏函數作用和裝飾器一樣,它可以擴展函數的功能,但又不完全等價于裝飾器。通常應用的場景是當我們要頻繁調用某個函數時,其中某些參數是已知的固定值,可以將這些固定值“固定”,然后用其他的參數參與調用。類似偏導數計算那樣,固定幾個變量,對剩下的變量求導。我們看下partial的函數參數定義:

func = functools.partial(func, *args, **keywords)
func: 需要被擴展的函數,返回的函數其實是一個類 func 的函數
*args: 需要被固定的位置參數
**kwargs: 需要被固定的關鍵字參數

如果在原來的函數 func 中關鍵字不存在,將會擴展,如果存在,則會覆蓋

同樣是剛剛求和的代碼,不同的是加入的關鍵字參數

def add(*args, *kwargs):
# 打印位置參數
for n in args:
print(n)
print("-"
20)
# 打印關鍵字參數
for k, v in kwargs.items():
print('%s:%s' % (k, v))
# 暫不做返回,只看下參數效果,理解 partial 用法

普通調用

add(1, 2, 3, v1=10, v2=20)
add_partial = partial(add, 10, k1=10, k2=20)
add_partial(1, 2, 3, k3=20)
偏函數與裝飾器 我們再看看如何使用類和偏函數結合實現裝飾器,如下所示,DelayFunc 是一個實現了call 的類,delay 返回一個偏函數,在這里 delay 就可以做為一個裝飾器:

import time
import functools

class DelayFunc:
def init(self, duration, func):
self.duration = duration
self.func = func

def __call__(self, *args, **kwargs):
    print(f'Wait for {self.duration} seconds...')
    time.sleep(self.duration)
    return self.func(*args, **kwargs)

def delay(duration):
"""
裝飾器:推遲某個函數的執行。
"""
# 此處為了避免定義額外函數,
# 直接使用 functools.partial 幫助構造 DelayFunc 實例
return functools.partial(DelayFunc, duration)

@delay(duration=2)
def add(a, b):
return a+b

wraps
繼續深入函數裝飾器,首先打印被裝飾的函數function的名字:

def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper

@decorator
def function():
print("hello, decorator")

print(function.name) #wrapper
輸出發現是wrapper,其實這也好理解,因為decorator返回的就是wrapper。但有時我們需要返回function的本來名字,那怎么做呢?python 的functools模塊提供了一系列的高階函數以及對可調用對象的操作,比如reduce,partial,wraps等。其中partial作為偏函數,在前面已經介紹過,warps旨在消除裝飾器對原函數造成的影響,即對原函數的相關屬性(比如name)進行拷貝,以達到裝飾器不修改原函數(屬性)的目的:

from functools import wraps

def decorator(func):
@wraps(func)
def wrapper(*args, **kw):
print(func.name)
return func()
return wrapper

@decorator
def function():
print("hello, decorator")

function()
print(function.name)
注意代碼中return func(),括號表示調用執行函數。作為對比,請看下面的調用:

from functools import wraps

def decorator(func):
@wraps(func)
def wrapper(*args, **kw):
print(func.name)
return func
return wrapper

@decorator
def function():
print("hello, decorator")

因為裝飾返回func,不會發生調用,因此需要兩對括號,其中function()返回的是函數定義。

print(function())
function()()
print(function.name)
裝飾器應用之contextmanager
contextmanager是python中一個使用廣泛的上下文管理器,(實際上也是裝飾器)經常跟with語句一起使用,用于精確地控制資源的分配和釋放。回憶以下常規代碼結構:

def controlled_execution(callback):
try:
#比如環境初始化、資源分配等
set things up
callback(thing)
finally:
#比如資源回收、事物提交等
tear things down

def my_function(thing):
#執行具體的業務邏輯
do something

controlled_execution(my_function)
以上為了防止業務邏輯出現異常,導致一些必須要執行的操作無法執行,通常使用try...finally語句,保證必要操作一定被執行。但是如果代碼中大量使用這種語句,又導致程序邏輯冗余,可讀性變差。但是結合with,并將以上語句稍作改動:將try...finally的邏輯拆分成兩個函數,分別執行比如資源的初始化和釋放,封裝在一個class中:

class controlled_execution:
def enter(self):
set things up
return thing
def exit(self, type, value, traceback):
tear things down

with controlled_execution() as thing:
# code body
do something
其中with expression [as variable],用來簡化 try / finally 語句。當執行with語句、進入代碼塊前,調用enter方法,代碼塊執行結束之后執行exit方法。需要注意的是可以根據exit方法的返回值來決定是否拋出異常,如果沒有返回值或者返回值為 False ,則異常由上下文管理器處理,如果為 True 則由用戶自己處理。上述代碼可以通過contextmanager進一步簡化:

@contextmanager
def controlled_execution():
#set things up
yield thing
#tear things down

with controlled_execution() as t:
print(t)
引入yield將函數變成生成器,yield將函數體分為兩部分:yield之前的語句在執行with代碼塊之前執行,yield之后的代碼塊在with代碼塊之后執行。到此為止,相信大家能夠理解文章開篇提到的代碼塊了,然后基于此,我們也可以自定義一個open函數:

from contextlib import contextmanager

@contextmanager
def my_open(name):
f = open(name, 'w')
yield f
f.close()

with my_open('some_file') as f:
f.write('hola!')

參考

https://mp.weixin.qq.com/s/cMqJulHjfo5oYfnwKDP7zw

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 裝飾器本質上是一個函數,該函數用來處理其他函數,它可以讓其他函數在不需要修改代碼的前提下增加額外的功能,裝飾器的返...
    胡一巴閱讀 420評論 0 0
  • 每個人都有的內褲主要功能是用來遮羞,但是到了冬天它沒法為我們防風御寒,咋辦?我們想到的一個辦法就是把內褲改造一下,...
    chen_000閱讀 1,370評論 0 3
  • 一、裝飾器的基本使用 在不改變函數源代碼的前提下,給函數添加新的功能,這時就需要用到“裝飾器”。 0.開放封閉原則...
    NJingZYuan閱讀 535評論 0 0
  • 一. 有時候我們會有這樣需求: 在原有的邏輯前后添加一段邏輯 如: 在增/刪/改操作之前檢查用戶是否登錄、某個操...
    元亨利貞o閱讀 698評論 1 4
  • 夸獎寶寶,就像使用抗生素一樣,需要有一定的標準,如果夸獎不到位,對孩子也會產生負面影響。那么夸獎寶寶要遵循怎樣的原...
    光谷魚兒也想飛閱讀 117評論 0 0