python 裝飾器和閉包

講解閉包前得先講下變量做用域的問題python

閉包

變量做用域規則

理解閉包裏引用的變量,就要理解變量做用域規則,請看下面的例子git

# coding: utf-8
b = 6
def f2(a):
    print(a)
    print(b)

f2(1)

很顯然,返回api

1
6

但假如這樣呢bash

# coding: utf-8
b = 6
def f2(a):
    print(a)
    print(b)
    b = 9

f2(1)

就會報錯閉包

1
Traceback (most recent call last):
  File "E:/code/git/source/master/coreTasker/api/controllers/a.py", line 8, in <module>
    f2(1)
  File "E:/code/git/source/master/coreTasker/api/controllers/a.py", line 5, in f2
    print(b)
UnboundLocalError: local variable 'b' referenced before assignment

首先輸出了1,接着print(b)執行不了報錯了,一開始覺得會打印6,由於b是個全局變量。app

可事實是,python編譯函數的定義體時,它判斷b是局部變量,由於在函數中給他賦值了(b=9),那麼python就會嘗試從本地環境獲取b,而本地環境還沒給其賦值。函數

若是在函數中賦值時想讓解釋器把b看成全局變量,那麼用global聲明:spa

# coding: utf-8
b = 6
def f2(a):
    global b
    print(a)
    print(b)
    b = 9

f2(1)

輸出:
1
6

輸出正常.net

好,下面切入閉包的概念3d

以實例來講明,假若有名爲avg的函數,它不斷返回系列值得平均值,好比下面方式使用

>>> avg(10) # 10/1
10.0
>>> avg(11) # (10+11)/2
10.5
>>> avg(12) # (10+11+12)/3
11.0

avg從何而來,又如何保存歷史值呢?

咱們很快可能會想到類-屬性的方式, average_oo.py

class Averager():
    def __init__(self):
        self.series = []
    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

Averager 的實例是可調用對象:

>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

或以下函數式實現,average.py

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager

以下方式調用

>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

很明顯,Averager 類的實例 avg 在哪裏存儲歷史值很明顯:self.series 實例屬性。可是第二個示例中的 avg 函數在哪裏尋找 series 呢?

注意,series 是 make_averager 函數的局部變量,由於那個函數的定義體中初始化了series:series = []。但是,調用 avg(10) 時,make_averager 函數已經返回了,而它的本地做用域也一去不復返了。
這時就要引出閉包裏的一個關鍵概念,自由變量(free variable),這是一個技術術語,指未在本地做用域中綁定的變量。在 averager 函數中,series 是自由變量(free variable),見下圖

也即閉包的概念就出來了:

一個函數返回了一個內部函數,該內部函數引用了外部函數的相關參數和變量,咱們把該返回的內部函數稱爲閉包(Closure)(實際上這個引用的變量叫自由變量)

繼續,python把局部變量,自由變量放在__code__屬性裏,見下

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)

實際上,series 的綁定在返回的 avg 函數的__closure__ 屬性中。avg.__closure__ 中的各個元素對應於 avg.__code__.co_freevars 中的一個名稱。這些元素是 cell 對象,有個cell_contents 屬性,保存着真正的值,以下:

>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

# 可見前面的10,11,12在這裏

綜上,閉包是一種函數,它會保留定義函數時存在的自由變量的綁定,這樣調用函數時,雖然定義做用域不可用了,可是仍能使用那些綁定。
注意,只有嵌套在其餘函數中的函數纔可能須要處理不在全局做用域中的外部變量。

注意,在python2中自由變量的認定是有必定條件的,看下面例子

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    return averager

這裏咱們保存總值以及總個數,按理說ok,但執行時

>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
  ...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

由於咱們在averager內部函數中給count賦值了,這會把count變爲局部變量,total也同樣

而上面的series=[]沒遇到這個問題,是由於咱們沒有給series賦值,只是調用append,是基於列表是可變對象的事實

但對數字,字符串,元組等不可變類型來講,只能讀取,不能更新。若是嘗試綁定,例如count = count + 1,其實會隱式建立局部變量count,這樣count就不是自由變量了,所以不會保存在閉包中。

python3中已經解決了這個問題,引入了nonlocal聲明。它的做用是把變量標記爲自由變量,即便在函數中爲變量賦予新值了,也會變成自由變量。以下:

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    return averager

以上閉包的概念基本講完了,接下來說裝飾器

 

裝飾器

裝飾器本質上就是一個函數,這個函數接收其餘函數做爲參數,並將其以一個新的修改後的函數進行替換並返回它。後來python提供@語法糖,使程序看起來更簡潔。

看以下簡單的例子,咱們定義2個普通函數,其中函數d1裝飾給f1

# coding: utf-8
def d1():
    print('deco d1')
@d1
def f1():
    print('func f1')

f1()

此時執行會報錯

Traceback (most recent call last):
  File "t.py", line 4, in <module>
    @d1
TypeError: d1() takes 0 positional arguments but 1 was given

因此此處對應了裝飾器函數的第一點定義:裝飾器本質是個函數,須要接收其餘被裝飾函數做爲參數

咱們加上此參數

# coding: utf-8
def d1(func):
    print('deco d1')

@d1
def f1():
    print('func f1')

f2()

執行仍是報錯

deco d1
Traceback (most recent call last):
  File "t.py", line 8, in <module>
    f1()
TypeError: 'NoneType' object is not callable

裝飾器函數d1執行了,有deco d1輸出,但下面的報錯了,由於d1裝飾器函數並無返回可執行的函數對象,這也就對應了第二點定義:並將其以一個新的修改後的函數進行替換並返回它

此處咱們加上返回

# coding: utf-8
def d1(func):
    print('deco d1')
    return func

@d1
def f1():
    print('func f1')

f1()

執行以下:

deco d1
func f1

可見成功了

因此裝飾函數的兩點:

1. 以被裝飾函數爲參數

2. 須要返回可執行函數對象

通常裝飾器寫法

def decorator(func):
    def wrapper(*args, **kwargs):
        # before do something(like: add_log, cache, check..)
        res = func(*args, **kwargs)
        # end do something
        return res
    return wrapper

但此時返回的是包裝後的函數,__name__屬性不是原函數名稱,要改變這個狀態可經過python內置的 functools模塊解決

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # before do something(like: add_log, cache, check..)
        res = func(*args, **kwargs)
        # end do something
        return res
    return wrapper

若是裝飾器須要處理被裝飾函數參數時,用inspect.getcallargs來處理會更友好,他會轉變爲字典形式

# coding: utf-8
import inspect
from functools import wraps


def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        func_args = inspect.getcallargs(func, *args, **kwargs)
        print(func_args)
        # before do something(like: add_log, cache, check..)
        res = func(*args, **kwargs)
        # end do something
        return res

    return wrapper

@decorator
def f1(c, e, *args, **kwargs):
    print('func f1')

f1(2, 'a', 1, 2, 3, a='1', b='2')

輸出

{'c': 2, 'e': 'a', 'args': (1, 2, 3), 'kwargs': {'a': '1', 'b': '2'}}
func f1

裝飾器還一個重要特性:

在加載模塊時當即執行,且只執行一次。此特性長被用做函數註冊或組件註冊

# coding: utf-8
registry = {}

def register(func):
    print('running register(%s)' % func)
    registry[func.__name__]=func
    return func

@register
def f1():
    print('running f1')

@register
def f2():
    print('running f2')

@register
def f3():
    print('running f3')

def main():
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()
    registry.get('f1')()

if __name__=='__main__':
    main()

看成腳本運行時,輸出

E:\virtualenv\nowamagic_venv\coreTasker_env1\Scripts\python.exe E:/code/git/source/master/coreTasker/api/controllers/c.py
running register(<function f1 at 0x0000000002712840>)
running register(<function f2 at 0x0000000002990400>)
running register(<function f3 at 0x0000000002990378>)
running main()
registry -> {'f1': <function f1 at 0x0000000002712840>, 'f2': <function f2 at 0x0000000002990400>, 'f3': <function f3 at 0x0000000002990378>}
running f1
running f2
running f3
running f1

可見後面單獨執行f1()時並無輸出running register ..., 只在加載模塊時初始化一次

 

多個裝飾器的調用順序

咱們知道一個裝飾器

@d
def f():
    pass

等同於

f = d(f)

多個就是疊加的關係

@d1
@d2
def f():
    pass

等同於

f = d1(d2(f))

 

帶參數的裝飾器

裝飾器如何帶參數?很簡單,再把裝飾器封裝一層,返回裝飾器

# coding: utf-8
import inspect
from functools import wraps

def decorator_with_param(args1='', args2=''):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            func_args = inspect.getcallargs(func, *args, **kwargs)
            print(func_args)
            # before do something(like: add_log, cache, check..)
            res = func(*args, **kwargs)
            # end do something
            return res

        return wrapper
    return decorator

@decorator_with_param()
def f1(c, e, *args, **kwargs):
    print('func f1')


f1(2, 'a', 1, 2, 3, a='1', b='2')
print(f1.__name__)

實際就是@後面的函數加上()後,就會調用它,若是這個返回是裝飾器就能夠

簡化下就是下面這樣

@d(p1, p2)
def f():
    pass

等同於

f = d(p1, p2)(f)

多個的狀況

@d1(p1)
@d2(p2)
def f():
    pass

等同於

f = d1(p1)(d2(p2)(f))

見下面

# coding: utf-8
def d1(func):
    def deco(*args, **kwargs):
        print('begin deco d1..........')
        res = func(*args, **kwargs)
        print('end   deco d1..........')
        return res
    return deco

def d2(func):
    def deco(*args, **kwargs):
        print('begin deco d2.......')
        res = func(*args, **kwargs)
        print('end   deco d2.......')
        return res
    return deco

@d1
@d2
def f1(c, e, *args, **kwargs):
    print('func f1')
    return 1


print(f1(2, 'a', 1, 2, 3, a='1', b='2'))
print(f1.__name__)

 返回

begin deco d1..........
begin deco d2.......
func f1
end   deco d2.......
end   deco d1..........
1
deco

注意有點像遞歸的返回,並且每一個裝飾器裏func執行結果不返回的話,最後是取不到f1()執行結果的。

 

基於類的裝飾器

前面都是基於函數的,其實基於類的也是能夠的,利用類的__init__,__call__

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

    def __call__(self, *args, **kwargs):
        return '<b>' + self.func(*args, **kwargs) + '</b>'

@Bold
def hello(name):
    return 'hello %s' % name

>>> hello('world')
'<b>hello world</b>'

能夠看到,類 Bold 有兩個方法:

  • __init__():它接收一個函數做爲參數,也就是被裝飾的函數
  • __call__():讓類對象可調用,就像函數調用同樣,在調用被裝飾函數時被調用

還可讓類裝飾器帶參數:

class Tag(object):
    def __init__(self, tag):
        self.tag = tag

    def __call__(self, func):
        def wrapped(*args, **kwargs):
            return "<{tag}>{res}</{tag}>".format(
                res=func(*args, **kwargs), tag=self.tag
            )
        return wrapped

@Tag('b')
def hello(name):
    return 'hello %s' % name

其實就是@Tag('b')會調用__call__返回wrapped

但若是類裝飾器要裝飾類裏的方法呢?

# coding: utf-8
class Bold(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<b>' + self.func(*args, **kwargs) + '</b>'

class H(object):
    @Bold
    def hello(self, name):
        return 'hello %s' % name

h = H()
print h.hello('jack')

# 輸出
Traceback (most recent call last):
  File "/Users/Teron/Code/Git/Personal/Test/e.py", line 16, in <module>
    print h.hello('jack')
  File "/Users/Teron/Code/Git/Personal/Test/e.py", line 7, in __call__
    return '<b>' + self.func(*args, **kwargs) + '</b>'
TypeError: hello() takes exactly 2 arguments (1 given)

可見報錯了,緣由就是被@Bold包裝的方法變成了unbound method

print h.hello
print h.hello.func

# 輸出
'''
<__main__.Bold object at 0x1023e43d0>
<function hello at 0x1023d4cf8>
'''

這裏的<function hello at 0x1023d4cf8>是須要兩個參數的,一個self,一個name,咱們只傳了一個,因此報錯,下面這種方式調用就能夠

print h.hello(h, 'jack')

# 輸出
'''
<b>hello jack</b>
'''

但咱們不能更改類方法的調用方式呢?只能經過修改類裝飾器,經過描述符

# coding: utf-8
class Bold(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<b>' + self.func(*args, **kwargs) + '</b>'

    def __get__(self, instance, owner):
        return lambda *args, **kwargs: '<b>' + self.func(instance, *args, **kwargs) + '<b>'


@Bold
def hellof(name):
    return 'hello %s' % name

class H(object):
    @Bold
    def hello(self, name):
        return 'hello %s' % name

print hellof('lucy')

h = H()
print h.hello('jack')

# 輸出
'''
<b>hello lucy</b>
<b>hello jack<b>
'''

以上__call__針對函數hellof,__get__針對類H裏的hello方法

或者下面這樣

# coding: utf-8
import types
class Bold(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<b>' + self.func(*args, **kwargs) + '</b>'

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)  # 手動建立一個綁定方法


@Bold
def hellof(name):
    return 'hello %s' % name

class H(object):
    @Bold
    def hello(self, name):
        return 'hello %s' % name

print hellof('lucy')

h = H()
print h.hello('jack')

# 輸出
'''
<b>hello lucy</b>
<b>hello jack</b>
'''

函數裝飾器裝飾在類方法中是否能夠?

def Boldf(func):
    def warp(*args, **kwargs):
        return '<b>' + func(*args, **kwargs) + '</b>'
    return warp

@Boldf
def hellof(name):
    return 'hello %s' % name

class H(object):
    @Boldf
    def hello(self, name):
        return 'hello %s' % name

print hellof('lucy')

h = H()
print h.hello('jack')

# 輸出
'''
<b>hello lucy</b>
<b>hello jack</b>
'''

因此最好是用函數裝飾器,但函數裝飾器有自由變量的問題,這經過nonlocal解決

相關文章
相關標籤/搜索