本節(jié)課綱:
- 魔法方法之_call_
- 閉包
- 裝飾器
- 裝飾器實例
一、魔法方法之_call_
在Python中,函數(shù)其實是一個對象:
>>> f = abs
>>> f.__name__
'abs'
>>> f(-123)
123
由于 f 可以被調(diào)用,所以,f 被稱為可調(diào)用對象。
所有的函數(shù)都是可調(diào)用對象。
一個類實例也可以變成一個可調(diào)用對象,只需要實現(xiàn)一個特殊方法_call_()。
我們把 Person 類變成一個可調(diào)用對象:
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
def __call__(self, friend):
print 'My name is %s...' % self.name
print 'My friend is %s...' % friend
現(xiàn)在可以對 Person 實例直接調(diào)用:
>>> p = Person('Bob', 'male')
>>> p('Tim')
My name is Bob...
My friend is Tim...
單看 p('Tim') 你無法確定 p 是一個函數(shù)還是一個類實例,所以,在Python中,函數(shù)也是對象,對象和函數(shù)的區(qū)別并不顯著。
可以把實例對象用類似函數(shù)的形式表示,進一步模糊了函數(shù)和對象之間的概念
class Fib(object):
def __init__(self):
pass
def __call__(self,num):
a,b = 0,1;
self.l=[]
for i in range (num):
self.l.append(a)
a,b= b,a+b
return self.l
def __str__(self):
return str(self.l)
f = Fib()
print(f(10))
在函數(shù)內(nèi)部再定義一個函數(shù),并且內(nèi)部函數(shù)用到了外部函數(shù)作用域里的變量(enclosing),那么將這個內(nèi)部函數(shù)以及用到的外部函數(shù)內(nèi)的變量一起稱為閉包(Closure)。裝飾器(decorator)接受一個callable對象(可以是函數(shù)或者實現(xiàn)了call方法的類)作為參數(shù),并返回一個callable對象,它經(jīng)常用于有切面需求的場景,比如:插入日志、性能測試(函數(shù)執(zhí)行時間統(tǒng)計)、事務處理、緩存、權限校驗等場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函數(shù)功能本身無關的雷同代碼并繼續(xù)重用
二、閉包
Python中函數(shù)也是對象,允許把函數(shù)本身作為參數(shù)傳入另一個函數(shù),還可以把函數(shù)作為結果值返回
在函數(shù)內(nèi)部再定義一個函數(shù),并且內(nèi)部函數(shù)用到了外部函數(shù)作用域里的變量(enclosing),那么將這個內(nèi)部函數(shù)以及用到的外部函數(shù)內(nèi)的變量一起稱為閉包(Closure):
In [1]: def line(a, b):
...: def get_y_axis(x):
...: return a * x + b # 內(nèi)部函數(shù)使用了外部函數(shù)的變量a和b
...: return get_y_axis # 返回值是閉包函數(shù)名,注意不是函數(shù)調(diào)用沒有小括號
...:
...:
In [2]: L1 = line(1, 1) # 創(chuàng)建一條直線: y=x+1
In [3]: L1
Out[3]: <function __main__.line.<locals>.get_y_axis(x)>
In [4]: L1(3) # 獲取第一條直線中,橫坐標是3時,縱坐標的值
Out[4]: 4
In [5]: L2 = line(2, 3) # 創(chuàng)建一條直線: y=2x+3
In [6]: L2
Out[6]: <function __main__.line.<locals>.get_y_axis(x)>
In [7]: L2(3) # 獲取第二條直線中,橫坐標是3時,縱坐標的值
Out[7]: 9
In [8]: L3 = line(2, 3) # 再創(chuàng)建一條直線: y=2x+3
In [9]: L3
Out[9]: <function __main__.line.<locals>.get_y_axis(x)>
In [10]: L2 == L3 # 每次調(diào)用line()返回的都是不同的函數(shù),即使傳入相同的參數(shù)
Out[10]: False
In [11]: L2 is L3
Out[11]: False
注意: 閉包中不要引用外部函數(shù)中任何循環(huán)變量或后續(xù)會發(fā)生變化的變量
# 1. 錯誤的用法
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
# f1()、f2()和f3()的輸出結果都是9,原因是調(diào)用這三個函數(shù)時,閉包中引用的外部函數(shù)中變量i的值已經(jīng)變成3
# 2. 正確的用法
def count():
def f(j):
return lambda: j * j
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被執(zhí)行,因此i的當前值被傳入閉包lambda: j * j
return fs
f1, f2, f3 = count()
print(f1()) # 輸出1
print(f2()) # 輸出4
print(f3()) # 輸出9
三、裝飾器
裝飾器(decorator)接受一個
callable對象(可以是函數(shù)或者實現(xiàn)了
call方法的類)作為參數(shù),并返回一個
callable對象
它經(jīng)常用于有切面需求的場景,比如:插入日志、性能測試(函數(shù)執(zhí)行時間統(tǒng)計)、事務處理、緩存、權限校驗等場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函數(shù)功能本身無關的雷同代碼并繼續(xù)重用。
舉個實例,假設你寫好了100個Flask中的路由函數(shù),現(xiàn)在要在訪問這些路由函數(shù)前,先判斷用戶是否有權限,你不可能去這100個路由函數(shù)中都添加一遍權限判斷的代碼(如果權限判斷代碼為5行,你得加500行)。那么,你可能想把權限認證的代碼抽離為一個函數(shù),然后去那100個路由函數(shù)中調(diào)用這個權限認證函數(shù),這樣只要加100行。但是,根據(jù)開放封閉
原則,對于已經(jīng)實現(xiàn)的功能代碼建議不能修改, 但可以擴展,因為可能你在這些路由函數(shù)中直接添加代碼會導致原函數(shù)出現(xiàn)問題,那么最佳實踐是使用裝飾器
3.1. 被裝飾的函數(shù)無參數(shù)
如果原來是調(diào)用f1()、f2() ... ,我們只要讓用戶還是調(diào)用f1()、f2() ... ,即他們調(diào)用的函數(shù)名還是保持不變,但實際執(zhí)行的函數(shù)體代碼已經(jīng)變了(Python中函數(shù)也是對象,函數(shù)名只是變量,可能改變它引用的函數(shù)體對象):
沒有使用裝飾器之前:
def f1():
print('function f1...')
def f2():
print('function f1...')
f1() # 輸出function f1...
f2() # 輸出function f2...
創(chuàng)建裝飾器,接收函數(shù)參數(shù)
,返回一個閉包
函數(shù)inner:
def login_required(func):
def inner(): # inner是一個閉包,它使用了外部函數(shù)的變量func,即傳入的原函數(shù)引用f1、f2...
if func.__name__ == 'f1': # 這里是權限驗證的邏輯判斷,此處簡化為只能調(diào)用f1
print(func.__name__, ' 權限驗證成功')
func() # 執(zhí)行原函數(shù),相當于f1()或f2()...
else:
print(func.__name__, ' 權限驗證失敗')
return inner
使用裝飾器:
def f1():
print('function f1...')
def f2():
print('function f1...')
new_f1 = login_required(f1) # 將f1傳入裝飾器,返回inner引用,并賦值給新的變量new_f1
new_f1() # 執(zhí)行函數(shù),即執(zhí)行inner(),這個閉包中使用的func變量指向原f1函數(shù)體
new_f2 = login_required(f2) # 將f2傳入裝飾器,返回inner引用,并賦值給新的變量new_f2
new_f2() # 執(zhí)行函數(shù),即執(zhí)行inner(),func變量指向原f2,所以它不會通過權限驗證,即不會執(zhí)行func()
# 輸出結果:
f1 權限驗證成功
function f1...
f2 權限驗證失敗
上面使用裝飾器有個問題,就是用戶原來是調(diào)用f1()、f2()... ,現(xiàn)在你讓他們調(diào)用new_f1()、new_f2()... , 這樣肯定不行,所以需要修改如下:
f1 = login_required(f1) # 將f1引用傳入裝飾器,此時func指向了原f1函數(shù)體。返回inner引用,并賦值給f1,即現(xiàn)在是func指向原函數(shù)體,而f1重新指向了返回的inner閉包
f1() # 執(zhí)行函數(shù),即執(zhí)行inner(),這個閉包中使用的func變量指向原f1函數(shù)體
上述兩個步驟可以用@
Python語法糖簡寫為:
# 1\. 定義時
@login_required
def f1():
print('function f1...')
# 2\. 調(diào)用時
f1()
3.2. 被裝飾的函數(shù)有參數(shù)
def login_required(func):
def inner():
if func.__name__ == 'f1':
print(func.__name__, ' 權限驗證成功')
func()
else:
print(func.__name__, ' 權限驗證失敗')
return inner
@login_required
def f1(a):
print('function f1, args: a=', a)
f1(10)
如果被裝飾的函數(shù)有參數(shù),調(diào)用f1(10)
此時實際調(diào)用的是inner(10)
,而裝飾器中的閉包inner沒有定義參數(shù),所以會報錯:
Traceback (most recent call last):
File "test.py", line 14, in <module>
f1(10)
TypeError: inner() takes 0 positional arguments but 1 was given
那么如果我給inner定義一個形參呢?
def login_required(func):
def inner(a): # inner定義了一個形參,名字隨意
if func.__name__ == 'f1':
print(func.__name__, ' 權限驗證成功')
func()
else:
print(func.__name__, ' 權限驗證失敗')
return inner
@login_required
def f1(a):
print('function f1, args: a=', a)
f1(10)
還是會報錯,原因是執(zhí)行inner函數(shù)體時,當執(zhí)行到func()
時,它指向傳入裝飾器的原函數(shù)f1的引用,而原f1需要一個位置參數(shù):
f1 權限驗證成功
Traceback (most recent call last):
File "test.py", line 14, in <module>
f1(10)
File "test.py", line 5, in inner
func()
TypeError: f1() missing 1 required positional argument: 'a'
所以正確的做法是:
def login_required(func):
def inner(a):
if func.__name__ == 'f1':
print(func.__name__, ' 權限驗證成功')
func(a)
else:
print(func.__name__, ' 權限驗證失敗')
return inner
@login_required
def f1(a):
print('function f1, args: a=', a)
f1(10)
# 輸出結果:
f1 權限驗證成功
function f1, args: a= 10
現(xiàn)在的裝飾器可以正確裝飾f1(a)函數(shù),但是假如有一個f2(a, b, c)有三個參數(shù)呢,肯定報錯,所以還要修改裝飾器,使用Python中的*args
和**kwargs
來匹配任意長度的位置參數(shù)或關鍵字參數(shù):
def login_required(func):
def inner(*args, **kwargs):
if func.__name__ == 'f1':
print(func.__name__, ' 權限驗證成功')
func(*args, **kwargs)
else:
print(func.__name__, ' 權限驗證失敗')
return inner
3.3. 被裝飾的函數(shù)有返回值
如果使用2.2的裝飾器,修改f1()函數(shù)定義,它里面有return返回值,將會有問題:
def login_required(func):
def inner(*args, **kwargs):
if func.__name__ == 'f1':
print(func.__name__, ' 權限驗證成功')
func(*args, **kwargs)
else:
print(func.__name__, ' 權限驗證失敗')
return inner
@login_required
def f1(a, b, c):
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
# 輸出結果中返回的是None,而不是hello, world
f1 權限驗證成功
function f1, args: a=10, b=20, c=30
None
原因是調(diào)用f1(10, 20, 30),實際是調(diào)用inner(10, 20, 30),然后執(zhí)行inner閉包的函數(shù)體,在執(zhí)行到func(*args, **kwargs)
后,沒有接收原f1函數(shù)體的返回值。當inner閉包執(zhí)行完畢,Python解釋器也沒有發(fā)現(xiàn)有return語句,就默認返回None
在inner中接收func函數(shù)的返回值,然后return返回它,本示例中裝飾器的inner執(zhí)行完func(*args, **kwargs)
后沒有其它代碼了,所以可以直接修改為return func(*args, **kwargs)
,如果還有其它邏輯,則用變量保存func的返回值res = func(*args, **kwargs)
,inner最后一行返回return res
def login_required(func):
def inner(*args, **kwargs):
if func.__name__ == 'f1':
print(func.__name__, ' 權限驗證成功')
return func(*args, **kwargs)
else:
print(func.__name__, ' 權限驗證失敗')
return inner
@login_required
def f1(a, b, c):
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
# 輸出結果:
f1 權限驗證成功
function f1, args: a=10, b=20, c=30
hello, world
3.4. 裝飾器帶參數(shù)
像Flask的@route('/index')
就是帶參數(shù)的,其實route只是一個函數(shù),它返回真正的裝飾器,即在原來的裝飾器外面再加一層函數(shù):
def logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
print('[日志級別 {}]: 被裝飾的函數(shù)名是 {}'.format(level, func.__name__))
return func(*args, **kwargs)
return wrapper
return decorator
@logging('DEBUG') # 等價于 f1 = logging('DEBUG')(f1) ,即先執(zhí)行l(wèi)oggin('DEBUG'),返回decorator引用(真正的裝飾器),再用decorator裝飾f1,返回wrapper
def f1(a, b, c):
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
@logging('INFO')
def f2():
print('function f2...')
res = f1(10, 20, 30)
print(res)
f2()
# 輸出結果:
[日志級別 DEBUG]: 被裝飾的函數(shù)名是 f1
function f1, args: a=10, b=20, c=30
hello, world
[日志級別 INFO]: 被裝飾的函數(shù)名是 f2
function f2...
3.5. 使用@wraps
def logging(level='INFO'):
def decorator(func):
def wrapper(*args, **kwargs):
"""print log before a function."""
print('[日志級別 {}]: 被裝飾的函數(shù)名是 {}'.format(level, func.__name__))
return func(*args, **kwargs)
return wrapper
return decorator
@logging('DEBUG')
def f1(a, b, c):
"""This is f1 function"""
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
print('錯誤的函數(shù)簽名:', f1.__name__)
print('錯誤的函數(shù)文檔:', f1.__doc__)
# 輸出結果:
[日志級別 DEBUG]: 被裝飾的函數(shù)名是 f1
function f1, args: a=10, b=20, c=30
hello, world
錯誤的函數(shù)簽名: wrapper
錯誤的函數(shù)文檔: print log before a function.
原因是調(diào)用f1(10, 20, 30),實際是調(diào)用裝飾器中的wrapper(),所以打印出來的函數(shù)簽名和文檔都是wrapper的,可以使用functools
模塊的wraps
裝飾器解決這個問題:
from functools import wraps
def logging(level='INFO'):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""print log before a function."""
print('[日志級別 {}]: 被裝飾的函數(shù)名是 {}'.format(level, func.__name__))
return func(*args, **kwargs)
return wrapper
return decorator
@logging('DEBUG')
def f1(a, b, c):
"""This is f1 function"""
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
print('正確的函數(shù)簽名:', f1.__name__)
print('正確的函數(shù)文檔:', f1.__doc__)
# 輸出結果:
[日志級別 DEBUG]: 被裝飾的函數(shù)名是 f1
function f1, args: a=10, b=20, c=30
hello, world
正確的函數(shù)簽名: f1
正確的函數(shù)文檔: This is f1 function
3.6. 多個裝飾器裝飾同一個函數(shù)
# 裝飾器1
def makeBold(func):
print('這是加粗裝飾器')
def blod_wrapped():
print('---1---')
return '<b>' + func() + '</b>'
return blod_wrapped
# 裝飾器2
def makeItalic(func):
print('這是斜體裝飾器')
def italic_wrapped():
print('---2---')
return '<i>' + func() + '</i>'
return italic_wrapped
@makeBold
@makeItalic
def test():
print('---3---')
return 'Hello, world'
res = test()
print(res)
# 輸出結果:
這是斜體裝飾器 # 這在調(diào)用res = test()之前就會輸出
這是加粗裝飾器 # 這在調(diào)用res = test()之前就會輸出
---1---
---2---
---3---
<b><i>Hello, world</i></b>
注意輸出結果中,多個裝飾器的順序,包裝時,是從下往上
的,先包裝一個小箱子@makeItalic
,會進入makeItalic裝飾器中函數(shù)體執(zhí)行(注意它里面的func指向test函數(shù)體),print('這是斜體裝飾器')
會輸出,然后返回閉包italic_wrapped
。再包裝一個大箱子@makeBold
,會進入makeBold裝飾器中函數(shù)體執(zhí)行(注意它里面的func指向italic_wrapped),print('這是粗體裝飾器')
會輸出,然后返回閉包blod_wrapped
。
而調(diào)用res = test()
時,先執(zhí)行右邊的test()
,相當于拆包裝,肯定是從最外層開始拆。此時的test
指向的是blod_wrapped
,所以執(zhí)行test()
會執(zhí)行blod_wrapped()
,即print('---1---')
會輸出。然后執(zhí)行到return '<b>' + func() + '</b>'
,由于這里的func
指向italic_wrapped
,所以先去執(zhí)行italic_wrapped()
,即print('---2---')
會輸出。再執(zhí)行到return '<i>' + func() + '</i>'
,同樣這里的func
指向原test函數(shù)體
,所以先去執(zhí)行它,即print('---3---')
會輸出,并且返回'Hello, world'
。這時,回到italic_wrapped
函數(shù)體返回'<i>Hello, world</i>'
,然后再回到blod_wrapped
函數(shù)體返回'<b><i>Hello, world</i></b>'
,即調(diào)用test()
的最終返回值,并賦值給res
變量,然后打印輸出到控制臺。
建議使用pythontutor在線調(diào)試,可以清楚的看到裝飾器內(nèi)部是如何執(zhí)行的。
3.7. 基于類實現(xiàn)的裝飾器
只要類實現(xiàn)了__call__
方法,那么類實例化后的對象就是callable
,即擁有了被直接調(diào)用的能力:
class Test():
def __call__(self):
print('call me!')
t = Test()
t() # 類實例化后的對象可以直接調(diào)用,輸出:call me!
裝飾器接受一個callable
對象作為參數(shù),并返回一個callable
對象,那么我們可以讓類的構造函數(shù)__init__ ()
接受一個函數(shù),然后重載__call__ ()
并返回一個函數(shù),也可以達到裝飾器函數(shù)的效果:
class logging(object):
def __init__(self, func):
self._func = func
def __call__(self, *args, **kwargs):
print('[DEBUG]: 被裝飾的函數(shù)名是 {}'.format(self._func.__name__))
return self._func(*args, **kwargs)
@logging
def f1(a, b, c):
"""This is f1 function"""
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
# 輸出結果:
[DEBUG]: 被裝飾的函數(shù)名是 f1
function f1, args: a=10, b=20, c=30
hello, world
帶參數(shù)的類裝飾器:
class logging(object):
def __init__(self, level='INFO'):
self._level = level
def __call__(self, func): # 接受函數(shù)
def wrapper(*args, **kwargs):
print('[日志級別 {}]: 被裝飾的函數(shù)名是 {}'.format(self._level, func.__name__))
return func(*args, **kwargs)
return wrapper # 返回閉包
@logging('DEBUG')
def f1(a, b, c):
"""This is f1 function"""
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
# 輸出結果:
[日志級別 DEBUG]: 被裝飾的函數(shù)名是 f1
function f1, args: a=10, b=20, c=30
hello, world
4. 裝飾器實例
更多實例請參考:http://python3-cookbook.readthedocs.io/zh_CN/latest/chapters/p09_meta_programming.html
4.1. 函數(shù)執(zhí)行時間統(tǒng)計
import time
from functools import wraps
def timethis(func):
'''
Decorator that reports the execution time.
'''
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)
return result
return wrapper
@timethis
def countdown(n):
'''Counts down'''
while n > 0:
n -= 1
return 'done'
res = countdown(100000)
print(res)
# 輸出結果:
countdown 0.0120086669921875
done
@timethis
def countdown(n):
pass
等價于:
def countdown(n):
pass
countdown = timethis(countdown)
內(nèi)置的裝飾器比如@staticmethod
、@classmethod
、@property
原理也是一樣的,下面兩個代碼片段是等價的:
class A:
@classmethod
def method(cls):
pass
class B:
# Equivalent definition of a class method
def method(cls):
pass
method = classmethod(method)
4.2. 插入日志
from functools import wraps
import logging
def logged(level, name=None, message=None):
"""
Add logging to a function. level is the logging
level, name is the logger name, and message is the
log message. If name and message aren't specified,
they default to the function's module and name.
"""
def decorate(func):
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate
# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
print(add(3, 5))
spam()
# 輸出結果:
2018-06-05 14:58:49,195 - test.py[line:19] - DEBUG: add
8
2018-06-05 14:58:49,237 - test.py[line:19] - CRITICAL: spam
Spam!