Python之函式裝飾器

無風聽海發表於2021-12-02

一、什麼是函式裝飾器

1.函式裝飾器是Python提供的一種增強函式功能的標記函式;

2.裝飾器是可呼叫的函式物件,其引數是另一個函式(被裝飾的函式);

  1. 我們可以使用修飾器來封裝某個函式,從而讓程式在執行這個函式之前與執行完這個函式之後,分別執行某些程式碼。這意味著,呼叫者傳給函式的引數值、函式返回給呼叫者的值,以及函式丟擲的異常,都可以由修飾器訪問並修改。這是個很有用的機制,能夠確保使用者以正確的方式使用函式,也能夠用來除錯程式或實現函式註冊功能,此外還有許多用途。

二、函式裝飾器的執行時機

函式裝飾器在被裝飾函式編譯解析之後直接執行,裝飾器是按照從上到下執行的;
函式裝飾器內部定義的返回函式依附在裝飾器的執行環境中;
函式裝飾器每次執行都會生成新的返回函式;

import sys

def dec1(m):
    print(f'{sys._getframe().f_code.co_name} is execute, arg {m.__name__}')
    def newm1():
        print(f'{sys._getframe().f_code.co_name}')

    return newm1;

@dec1
def m1():
    print(f'{sys._getframe().f_code.co_name}')

@dec1
def m11():
    print(f'{sys._getframe().f_code.co_name}')

if __name__ == '__main__':
    print(m1)
    print(m11)
    print(f'm1==m11:{m1==m11}')
    
# dec1 is execute, arg m1
# dec1 is execute, arg m11
# <function dec1.<locals>.newm1 at 0x7fdfa97d9160>
# <function dec1.<locals>.newm1 at 0x7fdfa97d91f0>
# m1==m11:False

三、變數作用域

Python中將變數宣告和賦值操作合一,很容易導致函式區域性變數覆蓋函式外的變數

b=6
def f():
    print(b)

f()

# 6

b=6
def f():
    print(b)
    b = 9

f()

# UnboundLocalError: local variable 'b' referenced before assignment

通過生成的位元組碼可以看到兩者對變數b的處理的差異,前者直接LOAD_GLOBAL,後者是LOAD_FAST,但是給b賦值卻在print之後導致報錯;

from dis import dis

b=6
def f():
    print(b)
    # b = 9

dis(f)

 # 5           0 LOAD_GLOBAL              0 (print)
 #              2 LOAD_GLOBAL              1 (b)
 #              4 CALL_FUNCTION            1
 #              6 POP_TOP
 #              8 LOAD_CONST               0 (None)
 #             10 RETURN_VALUE
from dis import dis

b=6
def f():
    print(b)
    b = 9
    
#  5          0 LOAD_GLOBAL              0 (print)
#             2 LOAD_FAST                0 (b)
#             4 CALL_FUNCTION            1
#             6 POP_TOP

#  6          8 LOAD_CONST               1 (9)
#             10 STORE_FAST               0 (b)
#             12 LOAD_CONST               0 (None)
#             14 RETURN_VALUE

可以使用global來強制宣告b是全域性變數,然後就可以重新賦值了;

b=6
def f():
    global b
    print(b)
    b = 9

f()

# 6

四、閉包

閉包是是指可以訪問非在函式體內定義的非全域性變數的函式;
通過函式的__code__及__closure__可以看到區域性變數和自由變數及閉包的情況;

def makesum():
    sum = [0]

    def s(val):
        sum[0] += val
        return sum[0]

    return s



s = makesum()
print(s(1))
print(s(2))
print(s.__code__.co_varnames)
print(s.__code__.co_freevars)
print(s.__closure__)
print(s.__closure__[0].cell_contents)


# 1
# 3
# ('val',)
# ('sum',)
# (<cell at 0x7f63321f1b20: list object at 0x7f63321e8a00>,)
# [3]

基於三中Python變數作用域的緣故,上邊的sum只能使用列表物件,python提供的nonlocal關鍵字可以直接使用int型別的變數;

def makesum():
    sum = 0

    def s(val):
        nonlocal sum
        sum += val
        return sum

    return s

s = makesum()
print(s(1))
print(s(2))
print(s.__code__.co_varnames)
print(s.__code__.co_freevars)
print(s.__closure__)
print(s.__closure__[0].cell_contents)


# 1
# 3
# ('val',)
# ('sum',)
# (<cell at 0x7f73e6a4ab20: int object at 0x7f73e6b47970>,)
# 3

五、保留函式的後設資料

函式裝飾器預設會使用返回的函式完全取代被裝飾的函式,這樣可能會導致序列化或者開發工具智慧提示的問題;可以使用functools.wraps來保留原始函式的標準屬性(namemodule、__annotations__等);

import functools

def dec(func):
    def real():
        '''this is real function'''
        pass
    return real

def wrapsdec(func):
    @functools.wraps(func)
    def real():
        '''this is real function'''
        pass
    return real

@dec
def max(nums):
    '''this is max function'''
    pass

@wrapsdec
def sum(nums):
    '''this is sum function'''

print(max)
print(max.__name__)
print(max.__doc__)
print(help(max))
print()
print(sum)
print(sum.__name__)
print(sum.__doc__)
print(help(sum))


# <function dec.<locals>.real at 0x7f1bfd4241f0>
# real
# this is real function
# Help on function real in module __main__:
# 
# real()
#     this is real function
# 
# None
# 
# <function sum at 0x7f1bfd424280>
# sum
# this is sum function
# Help on function sum in module __main__:
# 
# sum(nums)
#     this is sum function
# 
# None

六、支援關鍵字引數、位置引數

由於函式裝飾器可以應用到各種函式上,可以支援位置引數和關鍵字引數;

def dec(func):
    def real(*args, **kwargs):
        result = func(*args, **kwargs)
        return result

    return real

@dec
def sum(*nums, **kwargs):
    s = 0
    for n in nums:
        s = s + n

    for a in kwargs.values():
        s = s + a
    return s

print(sum(1,2,3,first=1))

七、使用lru_cache快取函式執行結果

lru_cache內部使用函式的引數作為key,使用dict進行快取執行結果,減少重複計算;
預設支援快取128條記錄,超過後採用LRU方式丟棄多餘記錄;
需要注意執行中不要改變引數,否則會影響字典key的有效性;

from functools import lru_cache

@lru_cache()
def fibonacci(n):
    print(f'fibonacci({n})')
    if n<2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(6))

# fibonacci(6)
# fibonacci(5)
# fibonacci(4)
# fibonacci(3)
# fibonacci(2)
# fibonacci(1)
# fibonacci(0)
# 8

八、使用singledispatch實現泛型函式

python不支援方法或者函式的過載,我們無法單獨定義不同引數型別的同名函式;
singledispatch提供了這樣一種能力,其通過註冊具體的引數型別和關聯的函式;
我們可以在自己的模組定義自己的型別,並實現自己的自定義函式;

import math
import numbers
from functools import singledispatch



@singledispatch
def absall(obj):
    return abs(obj)

@absall.register(numbers.Number)
def numabs(num):
    return abs(num)

@absall.register(numbers.Complex)
def cabs(c):
    return math.sqrt(c.real*c.real + c.imag* c.imag)

class Line:

    def __init__(self, startx, starty, endx, endy):
        self.startx = startx
        self.starty = starty
        self.endx = endx
        self.endy = endy

@absall.register(Line)
def lineabs(line):
    y =line.endy - line.starty
    x = line.endx - line.startx
    return math.sqrt(x*x + y*y)

print(absall(-1.1))
print(absall(3+4j))

l = Line(1,2,4,6)
print(absall(l))

# 1.1
# 5.0
# 5.0

九、通過引數控制函式裝飾器的行為

函式裝飾器本身不支援傳遞引數,解析原始碼的時候,python會將被裝飾的函式物件作為第一個引數傳遞給裝飾器;

我們可以通過在函式裝飾器外再巢狀一個函式工廠來承載裝飾特定函式的時候設定的引數;


def accesscontrol(checkuser=True, updatesession=True):

    def dec(func):
        def checkuserf():
            print('check user')
            return True

        def updatesessionf():
            print('update session')
            return True

        def access():
            if checkuser:
                checkuserf()

            if updatesession:
               updatesessionf()

            func()

        return access

    return dec

@accesscontrol()
def pushitem():
    print('pushitem')
    return True

@accesscontrol(checkuser=False)
def getitem():
    print('getitem')
    return True

# pushitem()
# print()
# getitem()
# 
# check user
# update session
# pushitem
# 
# update session
# getitem

相關文章