Python閉包和裝飾器

本節(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!
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容