1. 程式人生 > 其它 >閉包和裝飾器的關係

閉包和裝飾器的關係

因為最近想總結一下閉包和裝飾器,有點細節總是理不順,於是找了一下B站上播放量大的,其中一個下面評論很多都說講的很好,但是我聽了一下,關於閉包的地方講解的就有明顯錯誤。與fluent python和effective python上有矛盾,其實python cookbook上也沒說一定是函式作為引數,只是說可以。但是B站有些視訊講解時,竟然說閉包一定是傳入的引數是函式,其實這個就差遠了。所以大家看一些東西時,最好還是看經典教材,畢竟網上一些講解的視訊,沒有經過稽核,再者講解者自身水平參差不齊。

總結後,發現裝飾器真是個好東西,靈活方便。

fluent python 2nd中關於閉包的說法。

A closure is a function-let’s call it f – with an extend scope that encompasses variables referenced in the body of f that are not global variables nor local variables of f. Such variables must come from the local scope of an outer function which encompasses f. It does not matter whether the function is anonymous or not; what matters is that it can access nonglobal variables that are defined outside of its body.

閉包是一個函式 f +一個/些變數,這些變數在閉包內引用,但是不是global變數也不是f的區域性變數。這些變數必須來自包含函式f的外部函式的區域性區域。函式是不是匿名函式無所謂,關鍵是f可以訪問那些定義在 f 外部的非全域性變數。書中給了一個圖例,很清晰,到底什麼是閉包。

從這個圖的定義來看,閉包是函式並且這個函式可以訪問非global的自由變數。當然一般閉包首先涉及到巢狀函式(函式內有函式),也涉及到高階函式(傳入的引數是函式或者返回值是函式)。但是並不像有些人講的那樣,閉包一定是傳入函式。

如下是閉包的一個典型用法,這裡閉包外的函式沒有引數的。

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count,total
        count += 1
        total += new_value
        return total/count 
    return averager   # 返回的是函式,帶括號返回的函式執行結果
        
avg = make_averager()  # an object of function make_averager
print(avg.__name__) # the name is averager
avg(10)
avg(11)
res = avg(13)
res = avg(14)
print(res)

閉包外的函式傳入的引數也可以是函式。這種形式是很多裝飾器的基本形式。

def f(n):
    return n**2
    
def make_averager(func):
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count,total
        count += 1        
        res = func(new_value)
        print(res)
        total += res
        return total/count 
    return averager   # 返回的是函式,帶括號返回的函式執行結果
        
avg = make_averager(f)  # an object of function make_averager
print(avg.__name__) # the name is averager
avg(1)
avg(2)
res = avg(3)
res = avg(4)
print(res)

裝飾器什麼時候會用到呢?比如你要做計時統計,要記錄日誌,這些都是共有的一些功能,可以摘出來單獨用,然後作為包裝在別的函式上,這樣別的函式主要功能既清晰明瞭,又能夠實現對函式的一些記錄和統計。

前面講到閉包,其實從概念上閉包沒有說一定是傳入函式和返回函式,但是可能通常情況下這麼用比較多。但是像上面的圖中,並不需要傳入函式。所以不能說閉包傳入的引數是函式,就是裝飾器也不一定的。

那麼閉包和裝飾器是什麼關係呢?可以這麼說,裝飾器通過閉包的形式來實現的。通過下面幾個例子,體會到了閉包和裝飾器的關係,裝飾器真是好用。從這個層面上就可以看到,為什麼很多部落格說裝飾器就是給函式增加了一項功能,其實從功能上確實是這樣的。

上面的函式可以用裝飾器的形式。

'''
雖然這麼用不太好,但是也算是可以的。所以裝飾器和閉包之間是什麼關係呢?
有沒有一目瞭然呢?
'''
    
def make_averager(func):
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count,total
        count += 1            
        res = func(new_value)
        total += res
        return total/count 
    return averager   # 返回的是函式,帶括號返回的函式執行結果
        
@make_averager
def f(n):
    return n**2

'''
the decorator can be detailed below:
def f(n):
    return n**2
avg = make_averager(f)  # an object of function make_averager

'''
print(f.__name__)
f(10)
f(20)

  

那麼下面看看裝飾器常常用的情形,就是計時統計和日誌記錄。

'''
closure
the free vairable is func
here the inner function clocked() can call func and  
'''

import time

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))        
        arg_str = ', '.join(arg_lst)
        print(arg_str)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked


'''
decorator
'''
@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)
'''
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)
factorial = clock(factorial)
'''

if __name__ == '__main__':
    print('*' * 20, 'Calling factorial(6)')
    print('6! =', factorial(6))
    

  

你想實現一個功能,然後想計時,那麼就用類似於上述的裝飾器,不想計時時,只需要把裝飾器註釋掉就可以了,比如下面的例子,把裝飾器註釋掉了,就是沒有計時了,別的不影響。

import time

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))        
        arg_str = ', '.join(arg_lst)
        print(arg_str)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked


'''
decorator
'''
#@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)
'''
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)
factorial = clock(factorial)
'''

if __name__ == '__main__':
   # print('*' * 20, 'Calling factorial(6)')
    print('6! =', factorial(6))   # 6! = 720

  

日誌記錄例子也差不多,想記錄日誌就加上裝飾器,不想用就註釋掉。

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):
        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)
def plus(x, y):
    return x * y


@logged(logging.CRITICAL, 'example')
def spam():
    print('Spam log test!')

spam()


add(3,4)

plus(3,4)

  

Sophie的世界,轉載請註明出處,謝謝。