前言
Python的底層代碼,以及各種第三方框架中,你會看到各種各樣的@
符號,沒錯,他就是Python的裝飾器語法糖。
Python裝飾器看起來類似Java中的註解,OC中的Aspect框架,亦或是理解爲OC中Runtime的Hook操作,然鵝只是看起來而已。Python是通過@語法糖裏面的閉包來實現,iOS是Runtime底層交換方法來實現,再不改原先邏輯的情況下,在方法之前嵌入自己的邏輯,例如日誌,統計,預處理,清理,校驗等場景。Django中底層代碼大量用到了裝飾器,廣泛應用於緩存、權限校驗(如django中的@login_required和@permission_required裝飾器)
用法
用法很簡單,就三個步驟:
- 先定義一個裝飾函數(可以是類,可以是函數)
- 在定義你的業務函數,或者類
- 最後把裝飾器裝到你的業務函數頭上
根據上面的三個步驟,看看裝飾器的所有用法
閉包
首先介紹下閉包,應該都懂,危機百科的解釋:
在計算機科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。
官方就是不說人話,需要通俗的來介紹下,其實就是OC中的Block,看看Python中的存在形式
# 外部包裹
def decration():
para = 'I am closure'
# 嵌套一層 形成閉包
def wrapper():
print(para)
return wrapper
# 獲取一個閉包
closure = decration()
# 執行
closure()
para
參數是局部變量,在decration
執行後就被回收了。但是嵌套函數引用了這個變量,將局部變量封閉在嵌套函數中,形成閉包。
閉包就是引用自由變量的函數,這個函數保存了執行的上下文,可以脫離原本的作用於存在。
01.入門用法(不帶參數)
單個裝飾器
def logger(func):
# args 元祖 () kwargs 字典 {} 關鍵字參數
def wrapper(*args, **kwargs):
print('我正在進行計算: %s 函數:'%(func.__name__))
print('args = {}'.format(*args))
print('args is ', args)
print('kwargs is ', kwargs)
result = func(*args, **kwargs)
print('搞定,晚飯加個雞蛋')
return result
return wrapper
@logger
def add(a, b, x = 0):
print("%s + %s = %s" % (a, b, a + b))
return a + b
@logger
def multpy(a, b, x = 0):
print("%s * %s = %s" % (a, b, a * b))
return a * b
print(add(100, 200,x = 1))
print("*"*30)
print(multpy(10, 200))
/Users/mikejing191/Desktop/Python3Demo/venv/bin/python /Users/mikejing191/Desktop/Python3Demo/Demo5.py
我正在進行計算: add 函數:
args = 100
args is (100, 200)
kwargs is {'x': 1}
100 + 200 = 300
搞定,晚飯加個雞蛋
300
******************************
我正在進行計算: multpy 函數:
args = 10
args is (10, 200)
kwargs is {}
10 * 200 = 2000
搞定,晚飯加個雞蛋
2000
對於初學者看到這個@
語法會有些困擾,其實其實上面那段代碼與下面的調用方式一樣:
def add(a, b, x = 0):
print("%s + %s = %s" % (a, b, a + b))
return a + b
wrapper = logger(add)
wrapper(100,200)
仔細看的話,其實原函數被裝飾後,比如這個add
已經被替換成wrapper
的地址了,這樣外部打印func.__name__
就會變了,這種類似KVO,雖然被監聽了,但是Apple把對應的實現隱藏了,不會暴露出新增的類kvo_xxxx
,而會重寫class
方法返回原方法,這裏Python也類似,這裏下面會有一個方法來隱藏。
多個裝飾器
def logger(func):
# args 元祖 () kwargs 字典 {} 關鍵字參數
print('日誌裝飾器')
def wrapper_log(*args, **kwargs):
print('我正在進行日誌打印: %s 函數:'%(func.__name__))
print('日誌args is ', args)
print('日誌kwargs is ', kwargs)
result = func(*args, **kwargs)
print('日誌搞定,晚飯加個雞蛋')
return result
return wrapper_log
def statistics(func):
print('統計裝飾器')
def wrapper_static(*args, **kwargs):
print('我正在進行統計: %s 函數:'%(func.__name__))
print('統計args is ', args)
print('統計kwargs is ', kwargs)
result = func(*args, **kwargs)
print('統計搞定,晚飯加個雞蛋')
return result
return wrapper_static
@logger
@statistics
def add(a, b):
print("%s + %s = %s" % (a, b, a + b))
return a + b
print(add(100, 200))
統計裝飾器
日誌裝飾器
我正在進行日誌打印: wrapper_static 函數:
日誌args is (100, 200)
日誌kwargs is {}
我正在進行統計: add 函數:
統計args is (100, 200)
統計kwargs is {}
100 + 200 = 300
統計搞定,晚飯加個雞蛋
日誌搞定,晚飯加個雞蛋
300
和上面的單個裝飾器類似,只是多疊加了一個,可以看到我們這裏的logger
在statics
上面,按正常理解,先裝飾logger
,再裝飾statics
,但是Python這裏的規則是這樣的:
根據日誌分析下,首先編譯器遇到@logger和@statistics,這裏是會有代碼執行的,比如兩個裝飾器的第一句打印,是在裝飾器代碼執行到就調用,不需要調用被裝飾的函數。裝飾的前提是裝飾器的下一句代碼是方法函數,纔會裝飾,因此先跳過@logger,然後@statistics就會對func函數進行裝飾,因此先執行裝飾statistics,然後返回的值就是wrapper_static函數,再執行裝飾logger,執行的時候就是先執行裝飾logger裏面的inner函數,然後在執行裝飾2裏面的wrapper_log函數,好比一個東西,包裝的時候由內到外,執行的時候由外到內,這就是多層裝飾的邏輯
湊活看下畫了個抽象的圖,w1和w2分別代表logger和statistics,inner就是分別對應裝飾器裏面的閉包:
可以看到最終我們原函數的指針地址只想的是最外層logger
的閉包函數地址。
注意點:這裏的閉包函數返回的都是閉包,要等函數實際調用的時候纔會觸發,但是有些寫法是不需要閉包的,比如Django中的Admin註冊,這就有點不同,他會在裝飾器執行到的時候直接觸發內部代碼,因此,你腦洞多大,裝飾器的功能就有多大
from .models import BlogType, Blog
@admin.register(BlogType)
class BlogTypeAdmin(admin.ModelAdmin):
list_display = ('type_name',)
# 裝飾函數
def register(*models, site=None):
"""
Register the given model(s) classes and wrapped ModelAdmin class with
admin site:
@register(Author)
class AuthorAdmin(admin.ModelAdmin):
pass
The `site` kwarg is an admin site to use instead of the default admin site.
"""
from django.contrib.admin import ModelAdmin
from django.contrib.admin.sites import site as default_site, AdminSite
def _model_admin_wrapper(admin_class):
if not models:
raise ValueError('At least one model must be passed to register.')
admin_site = site or default_site
if not isinstance(admin_site, AdminSite):
raise ValueError('site must subclass AdminSite')
if not issubclass(admin_class, ModelAdmin):
raise ValueError('Wrapped class must subclass ModelAdmin.')
admin_site.register(models, admin_class=admin_class)
return admin_class
return _model_admin_wrapper
02.進階用法(帶參數)
看完入門,應該對裝飾器有個大概的瞭解,不過是不能接受參數的裝飾器,這不搞笑呢,對應裝飾器,只能執行固定的邏輯,不能被參數控制,這是不能忍的,而且你看過其他項目,可以看到大部分裝飾器是帶有參數的。
那麼裝飾器的傳參如何實現,這個就需要多層嵌套了,看下實際案例:
def american():
print("I am from America.")
def chinese():
print("我來自中國。")
有個需求,給他們兩根據不同國家,自動加上打招呼的功能。
def say_hello(contry):
def wrapper(func):
def inner_wrapper(*args, **kwargs):
if contry == 'china':
print('你好!')
elif contry == 'america':
print('Hello!')
else:
return
return func(*args, **kwargs)
return inner_wrapper
return wrapper
@say_hello('america')
def american():
print("I am from America.")
@say_hello('china')
def chinese():
print("我來自中國。")
@say_hello('japanese')
def japanese():
print('I am from jp')
american()
chinese()
japanese()
Hello!
I am from America.
你好!
我來自中國。
em。。。實屬牛逼。。。。。。。。
但是又有點懵逼,包了一層,內部的wrapper
的func
是怎麼穿進去的?
其實去掉@
語法,我們來恢復下調用邏輯:
def american():
print("I am from America.")
decoration = say_hello('china')
wrapper = decoration(american)
wrapper()
em。。。好像一點也不牛逼。。。。。。。。
裝飾器這一語法體現了Python中函數是第一公民,函數是對象、是變量,可以作爲參數、可以是返回值,非常的靈活與強大。
03.高階用法(不帶參數的類裝飾器)
以上是基於函數實現的裝飾器,在閱讀別人的代碼的時候,經常還能發現基於類實現的裝飾器。
__call__內置函數
絕大多數裝飾器都是基於函數和 閉包 實現的,但這並非製造裝飾器的唯一方式。事實上,Python 對某個對象是否能通過裝飾器( @decorator)形式使用只有一個要求:decorator 必須是一個“可被調用(callable)的對象。
class Foo():
def __call__(self, *args, **kwargs):
print('Hello Foo')
class Bar():
pass
print(callable(Foo))
print(callable(Foo()))
print(callable(Bar))
print(callable(Bar()))
True
True
True
False
要實現基於類的裝飾器,必須理解__call__
內置函數的作用。
import sys
class MKJ(object):
def __init__(self, name):
super().__init__()
self.name = name
def __call__(self, *args, **kwargs):
print('當前類名:%s'%self.__class__.__name__)
print('當前函數名稱:%s'%sys._getframe().f_code.co_name)
print('當前參數:',args)
m = MKJ('mikejing')
m('Faker', 'Deft')
當前類名:MKJ
當前函數名稱:__call__
當前參數: ('Faker', 'Deft')
call()
官方定義:Called when the instance is “called” as a function; if this method is defined, x(arg1, arg2, …) is a shorthand for x.call(arg1, arg2, …).
它是在“實例被當成函數調用時”被調用。
舉個例子,實例如果是m = MKJ()
,那麼,當你寫下m()
的時候,該實例(即m)的創建者MKJ類
(注意:此處提到的創建者既有可能是類,也有可能是元類)中的__call__()
被調用。
如果這個實例是一個類,那麼它的創建者就是一個元類,如果這個實例是一個對象,那麼它的創建者就是一個類。
明白了__call__
的用法,就可以實現最基本的不帶參數的類裝飾器,代碼如下:
import sys
class logger(object):
def __init__(self, func):
super().__init__()
print('裝飾類開始')
self.func = func
def __call__(self, *args, **kwargs):
print('當前類名:%s'%self.__class__.__name__)
print('當前函數名稱:%s'%sys._getframe().f_code.co_name)
print('裝飾函數名稱:%s' % self.func.__name__)
print('當前參數:',args)
self.func(*args, **kwargs)
@logger
def say(sm):
print('say:%s'%sm)
say('hello!')
print(say) # <__main__.logger object at 0x108323160>
# 輸出如下
裝飾類開始
當前類名:logger
當前函數名稱:__call__
裝飾函數名稱:say
當前參數: ('hello!',)
say:hello!
說明:
- 當我們把
logger
類作爲裝飾器的時候,首先會默認創建logger的實例,可以試試先不調用say('hello)
,可以看到logger實例的__init__
方法被調用,被裝飾的函數作爲參數被傳遞進來。func
變量指向了say
的函數體。 - 此時say函數相當於重新指向了logger創建出來的實例對象的地址
- 當調用
say()
的時候,就相當於調用這個對象類的__call__
方法 - 爲了能夠在
__call__
中調用會原來say
函數,所以在__init__
中需要一個實例變量保存原函數的引用,所有才有了self.func = func
,從而在__cal__
中取出原函數地址和參數,進行回調
印證的話可以打開這個裝飾器關閉裝飾器打印一下say
看下函數和對象的轉換
# 關閉
# @logger
def say(sm):
print('say:%s'%sm)
print(say)
# 輸入如下
<function say at 0x1011b8268>
# 打開
@logger
def say(sm):
print('say:%s'%sm)
print(say)
# 輸出如下
<__main__.logger object at 0x103233160>
04.高階用法(帶參數的類裝飾器)
還是用上面的logger
函數,由於日誌可以分爲很多級別info
,warning
,debug
等類型的日誌。這個時候就需要給類裝飾器傳入參數。回顧下函數裝飾器,對於傳參或者不傳參,只是外部在包一層與否,整體邏輯沒什麼變化,但是如果類裝飾器帶參數,就和不帶參就有很大不同了。
__init__
:該方法不再接受裝飾函數,而是接受傳入參數。__call__
:接受被裝飾函數,實現裝飾邏輯。
import sys
class logger(object):
def __init__(self, level):
super().__init__()
print('裝飾類開始')
self.level = level
def __call__(self, func):
def wrapper(*args, **kwargs):
print('[%s級別]--當前函數:'%(self.level),sys._getframe().f_code.co_name)
print('[%s級別]--裝飾函數名稱:%s'%(self.level,func.__name__))
print('[%s級別]--當前參數:'%(self.level), args)
func(*args, **kwargs)
return wrapper
@logger('WARNING')
def say(sm):
print('say:%s'%sm)
say('Hello')
# 日誌如下
裝飾類開始
[WARNING級別]--當前函數: wrapper
[WARNING級別]--裝飾函數名稱:say
[WARNING級別]--當前參數: ('Hello',)
say:Hello
第二種帶參數的類裝飾器其實有點奇怪,__init__
方法裏面沒有了func
參數,其實按正常邏輯來看,理解起來其實不容易記憶,但是你強行記憶也行。em…
05.高階用法(偏函數和類實現)
絕大部分裝飾器都是基於函數和閉包來實現的,但是並非只此一種,看了上面的類裝飾器,我們來實現一個與衆不同,但是底層框架都大量使用的方式(類和偏函數實現),這種方式就是擴展的不帶參數類函數裝飾器。
import time
import functools
class DelayFunc:
def __init__(self, durations, func):
super().__init__()
self.durations = durations
self.func = func
print('1111')
def __call__(self, *args, **kwargs):
print('please waite for %s seconds...'%self.durations)
time.sleep(self.durations)
return self.func(*args, **kwargs)
def no_delay_call(self, *args, **kwargs):
print('call immediately 。。。。')
return self.func(*args, **kwargs)
def delay(durations):
# Deley 裝飾器,推遲某個函數執行,同時提供no_delay_call不等待調用
# 此處爲了避免額外函數,直接使用 functools.partial 幫助構造 具體參見另一個博客介紹,這裏的作用我會在下面簡單通俗介紹下
return functools.partial(DelayFunc, durations)
@delay(3)
def add(a, b):
return a + b
print(add(100,200))
# print(add.no_delay_call(200,300))
這裏涉及到一種俗稱偏函數的東西functools.partial
,可以參見我的另一篇博客介紹,這裏簡單介紹下怎麼理解。首先定義了一個類DelayFunc
,做成裝飾器的前提是callable
也就是實現__call__
方法,按不帶參數的類裝飾器,如果做成傳參形式,上面有介紹,需要改動正常的類參數,現在按照此種方式進行擴展。定義一個函數deley
,我們把它當做裝飾器,類裝飾器裝飾其實把類實例化,可以看到deley
函數返回的應該是一個類,這裏能看到用到了functools.partial
,該方法先理解爲綁定DelayFunc
類,暫時先綁定一個durations
參數,那麼我們看到初始化方法裏面還有個參數是Func
,這個就是我們最終裝飾的時候自帶的參數,所以當你看到以下使用的時候
@delay(3)
def add(a, b):
return a + b
delay
返回的綁定一半的類和參數,然後再傳輸add
作爲func
進行實例化,此時add指向的不再是簡單的函數地址,而是指向了新的類的實例。最終調用add(100,200)
的時候執行__call__
至此,我們瞭解了函數裝飾器,類裝飾器的兩種實現,分別有帶參數和不帶參數的區別。最後一種是偏函數實現的類裝飾器,一共五種。
06.類裝飾器的優勢
那麼類裝飾器比函數裝飾器有哪些優勢:
-
實現有狀態的裝飾器時,操作類屬性比操作閉包內變量更符合直覺、不易出錯
-
實現爲函數擴充接口的裝飾器時,使用類包裝函數,比直接爲函數對象追加屬性更易於維護
-
更容易實現一個同時兼容裝飾器與上下文管理器協議的對象(參考 unitest.mock.patch)
07.可選優化裝飾器(使用 wrapt 第三方模塊編寫更扁平的裝飾器)
- 實現帶參數的裝飾器時,層層嵌套的函數代碼特別難寫、難讀
- 因爲函數和類方法的不同,爲前者寫的裝飾器經常沒法直接套用在後者上
看一下生成隨機數注入爲函數參數的裝飾器
import random
def random_number(min_num, max_num):
def wrapper(func):
def decorated(*args, **kwargs):
num = random.randint(min_num, max_num)
return func(num, *args, **kwargs)
return decorated
return wrapper
@random_number(0, 99)
def print_number(num):
print(num)
print_number()
@random_number
裝飾器功能看上去很不錯,但它有着我在前面提到的兩個問題:嵌套層級深、無法在類方法上使用。如果直接用它去裝飾類方法,會出現下面的情況:
class Foo:
@random_number(0, 99)
def print_number(self, num):
print(num)
print_number()
<__main__.Foo object at 0x10bdc12b0>
Foo
類實例中的 print_number
方法將會輸出類實例 self
,而不是我們期望的隨機數 num。
之所以會出現這個結果,是因爲類方法(method)和函數(function)二者在工作機制上有着細微不同。如果要修復這個問題, random_number
裝飾器在修改類方法的位置參數時,必須聰明的跳過藏在 *args
裏面的類實例 self 變量,才能正確的將 num 作爲第一個參數注入。
這時,就應該是 wrapt
模塊閃亮登場的時候了。 wrapt
模塊是一個專門幫助你編寫裝飾器的工具庫。利用它,我們可以非常方便的改造 random_number
裝飾器,完美解決“嵌套層級深”和“無法通用”兩個問題,
import random
import wrapt
def random_number(min_num, max_num):
@wrapt.decorator
def wrapper(wrapperd, instance, args, kwargs):
# 參數含義:
# - wrapped:被裝飾的函數或類方法
# - instance:
# - 如果被裝飾者爲普通類方法,該值爲類實例
# - 如果被裝飾者爲 classmethod 類方法,該值爲類
# - 如果被裝飾者爲類/函數/靜態方法,該值爲 None
# - args:調用時的位置參數(注意沒有 * 符號)
# - kwargs:調用時的關鍵字參數(注意沒有 ** 符號)
num = random.randint(min_num, max_num)
# 無需關注 wrapped 是類方法或普通函數,直接在頭部追加參數
args = (num, ) + args
return wrapperd(*args, **kwargs)
return wrapper
@random_number(0, 99)
def print_number(num):
print(num)
class Foo:
@random_number(0, 99)
def print_number(self, num):
print(num)
print_number()
Foo().print_number()
這就是使用了wrapt
後的有點,如果不習慣,還是使用上述的一些裝飾器即可
- 嵌套層級少:使用 @wrapt.decorator 可以將兩層嵌套減少爲一層
- 更簡單:處理位置與關鍵字參數時,可以忽略類實例等特殊情況
- 更靈活:針對 instance 值進行條件判斷後,更容易讓裝飾器變得通用
08.裝飾類的裝飾器
Python中單例的實現,有一種就是用單例實現的
instances = {}
def singleton(cls):
def get_instance(*args, **kw):
cls_name = cls.__name__
print('===== 1 ====')
if not cls_name in instances:
print('===== 2 ====')
instance = cls(*args, **kw)
instances[cls_name] = instance
return instances[cls_name]
return get_instance
@singleton
class User:
_instance = None
def __init__(self, name):
print('===== 3 ====')
self.name = name
u1 = User('mkj1')
u1.age = 100
u2 = User('mkj2')
print(u1 == u2)
print(u2.age)
# 日誌如下
===== 1 ====
===== 2 ====
===== 3 ====
===== 1 ====
True
100
09.wraps 裝飾器有啥用
上面介紹了多種裝飾器,而且還引入了functools
庫,除了用到partitial
函數,還有個裝飾器wraps
,看看到底有啥用。
def say_hello(contry):
def wrapper(func):
def inner_wrapper(*args, **kwargs):
if contry == 'china':
print('你好!')
elif contry == 'america':
print('Hello!')
else:
return
return func(*args, **kwargs)
return inner_wrapper
return wrapper
@say_hello('china')
def american():
print("I am from America.")
print(american.__name__)
american()
# 打印日誌
inner_wrapper
你好!
I am from America.
可以看到,按我們上面的分析,其實american
已經不再指向原來的函數地址,因此打印出來的名字也變了。理論上沒什麼問題,但是有時候你定位Bug的時候會很噁心,因此,我們會看到大量的庫用到了系統提供的wraps
裝飾器
import functools
def say_hello(contry):
def wrapper(func):
@functools.wraps(func)
def inner_wrapper(*args, **kwargs):
if contry == 'china':
print('你好!')
elif contry == 'america':
print('Hello!')
else:
return
return func(*args, **kwargs)
return inner_wrapper
return wrapper
@say_hello('china')
def american():
print("I am from America.")
print(american.__name__)
american()
# 打印如下
american
你好!
I am from America.
方法是使用 functools .wraps
裝飾器,它的作用就是將 被修飾的函數(american
) 的一些屬性值賦值給 修飾器函數(inner_wrapper
) ,最終讓屬性的顯示更符合我們的直覺。
準確的來看,functools .wraps
也是一個偏函數對象partial
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
可以看到該裝飾器也是和我們上面演示的一樣,使用了partial
偏函數,其中綁定的類或者方法是update_wrapper
,其中該方法實際上接收四個參數,這裏我們傳了三個,用作裝飾器,默認會把第四個參數,被裝飾的函數inner_wrapper
作爲wrapper
首參數進行裝飾初始化。
wrapper.__wrapped__ = wrapped
底層實現中會把原函數的屬性全部賦值給修飾器函數inner_wrapper
,最終調用__name__
的時候,雖然指針被已經指向被裝飾的函數,但是通過再次裝飾,屬性會被原函數一樣打印出來。
10.裝飾器實戰
1.裝飾器使我們的代碼可讀性更高
2.代碼結構更加清晰,代碼冗餘降低
下面是一個實現控制函數運行超時的裝飾器,如果超時,就會拋出異常。
import signal
import functools
class TimeoutException(Exception):
def __init__(self, error='Timeout waiting for response from Cloud'):
Exception.__init__(self, error)
def timeout_limit(timeout_time):
def wraps(func):
def handler(signum, frame):
raise TimeoutException()
@functools.wraps(func)
def deco(*args, **kwargs):
signal.signal(signal.SIGALRM, handler)
signal.alarm(timeout_time)
return func(*args, **kwargs)
signal.alarm(0)
return deco
return wraps
@timeout_limit(1)
def add(x):
r = 0
for a in range(0, x):
r += a
return r
print(add.__name__)
print(add(10))
print(add(100000000))
該功能可以看到執行add(10)
的時候正常輸出,但是執行add(10000000)
的時候由於超時,就會拋出異常崩潰,實現了我們給函數進行裝飾的功能。
總結:
- 裝飾器實現方式很多種,除了常用的函數+閉包,也可以用類來裝飾,概括爲一切callable的對象都可以被用來實現裝飾器
- 混合使用函數和類,能更好的實現裝飾器
- 裝飾器只是一種語法糖,你也可以自己拆開來一步步寫,它不是裝飾器模式。
- 裝飾器會改變原函數的所有信息(例如簽名),我們需要
functools.wraps
來進行交換回去 - 類裝飾器帶參數的可以用偏函數
partial
來實現更優雅的方案,推薦使用
參考文獻:
Python 工匠:使用裝飾器的技巧
搞懂裝飾器所有用法
Python中*args 和**kwargs的用法