首頁 >後端開發 >Python教學 >Python裝飾器-閉包與函數裝飾器

Python裝飾器-閉包與函數裝飾器

PHPz
PHPz轉載
2023-04-10 14:51:071685瀏覽

一、閉包

在學習裝飾前,需要先了解閉包的概念。形成閉包的要點:

  • 函數巢狀
  • 將內部函數作為外部函數的回傳值
  • 內部函數必須使用到外部函數的變數

下面以一個計算列表平均值的案例來講解閉包:

def make_average():
# 创建一个列表,用来保存数值
nums = []

# 定义一个内部函数,用来计算列表的平均值
def average(n):
# 将数值添加到列表中
nums.append(n)
# 返回平均值
return sum(nums) / len(nums)

return average
  1. 首先,定義一個函數make_average;
  2. 其次,在make_average函數內定義一個空列表,用來儲存數值;
  3. 再次,定義一個內部函數,用來計算列表平均值;
  4. 最後,將這個內函數作為外函數make_average的回傳值,注意不要加( ),加( )就變成呼叫這個函數了。
# 调用外部函数,并将其复制给一个变量,注意:此时返回的是内函数的内存地址
a = make_average()
# 给这个变量加(),就相当于调用了内函数average
print(a(20))
print(a(30))

運行結果如下:當傳入的數值為20時,列表中只有一個數,所以計算結果是20;再傳入一個數值30時,此時列表中就有兩個數字20和30,所以平均值的計算結果是25.

Python裝飾器-閉包與函數裝飾器

#二、裝飾器

1.裝飾器引入

例如,有以下兩個函數,分別計算兩個數的和以及成績:

def add(a, b):
"""计算两数之和"""
res = a + b
return res

def mul(a, b):
"""计算两数之积"""
res = a * b
return res

現在有個需求:我想要在每個函數的計算開始前列印「開始計算... ”,在計算結束後列印“計算結束...”。我們可以透過直接修改函數程式碼的方式來滿足這個需求,但這樣會面臨以下問題:

  1. 如果要修改的函數過多,十個甚至一百個函數,未免不切實際;
  2. 不便於後期維護,例如我不想打印“開始計算...”了,而是要打印“begin...”,豈不是又要重新修改一遍;
  3. 違反開閉原則(OCP),也就是程式的設計,要求開放程式的擴充、關閉程式的修改;

所以,上述直接修改函數程式碼的方式不可行。我們希望在不修改原函數的情況下,實現對函數的擴展。例如:

def new_add(a, b):
print("开始计算...")
r = add(a, b)
print("计算结束...")
return r


print(new_add(22, 33))

執行結果如下:

Python裝飾器-閉包與函數裝飾器

這種新建立一個函數的方式雖然沒有修改原函數,但面臨一個很嚴重的問題:

只能擴展指定函數,不能通用於其它函數,例如擴展上述的add函數,而不能擴展mul函數,如果想要擴展mul函數,只能再建立一個擴展函數;

因為,我們希望可以定義一個通用的擴充函數,可以作用域所有函數。這類不改變原始函數程式碼的通用函數就是:裝飾器。

2.函數裝飾器

裝飾器本質上是一個python函數或類,它可以讓其他函數或類別在不需要做任何程式碼修改的前提下增加額外功能,也就是為已經存在的物件添加額外功能,裝飾器的回傳值也是一個函數/類別物件。它常用於有切面需求的場景,例如:插入日誌、效能測試、交易處理、快取、權限校驗等場景。

1)被裝飾函數不帶參數

例如:

def wrapper_info(func):
def inner():
print("开始介绍...")
res = func()
print("介绍结束...")
return res

return inner

def introduce1():
print("我是周润发,我来自HONG KONG")

info = wrapper_info(introduce1)
info()

運行結果如下:

Python裝飾器-閉包與函數裝飾器

可見,在沒有改變原始函數程式碼的情況下,也就是為原函數增加了一些額外的功能,func是要修飾的函數,作為一個變數傳入裝飾函數,能夠通用於其他函數,這個wrapper_info就是一個裝飾器。但目前面臨的問題是,被裝飾函數如果帶參數怎麼辦?例如:

def introduce2(name, age):
print(f"我叫{name}, 我今年{age}岁了")

2)被裝飾函數帶參數

儘管可以在裝飾器wrapper_info中傳入name、age,但並不是每個被裝飾的函數都只有name、age,亦或是指定類型的參數,還有可能傳入的是字典、列表、元組等。也就是傳入參數的型別和數量不固定怎麼辦?

此時就需要用到不定長參數:(*args, **kwargs)

def wrapper_info(func):
"""
用来对其他函数进行扩展,使其他函数可以在执行前做一些额外的动作
:param func: 要扩展的函数对象
:return:
"""
def inner(*args, **kwargs):
print("开始介绍...")
res = func(*args, **kwargs)
print("介绍结束...")
return res

return inner

例如:

def introduce3(name, age, city):
print(f"我叫{name}, 我今年{age}岁了, 我来自{city}")

運行結果如下:

Python裝飾器-閉包與函數裝飾器

3)装饰器带参数

上述提到的是装饰器,一种是应用于被装饰的函数不带参数,一种是被装饰的函数带参数,那装饰器本身能否带参数呢?比如我定义一个变量,想通过传入不同的值来控制这个装饰器实现不同的功能。答案是肯定的,例如:

def use_log(level):
def decorator(func):
def inner(*args, **kwargs):
if level == "warn":
logging.warning("%s is running by warning" % func.__name__)
elif level == "info":
logging.warning("%s is running by info" % func.__name__)
else:
logging.warning("%s is running by other" % func.__name__)
return func(*args, **kwargs)

return inner

return decorator


def introduce4(name, age, city):
print(f"我叫{name}, 我今年{age}岁了, 我来自{city}")


info1 = use_log(introduce4('周星驰', 28, '香港'))
info1('info')
info2 = use_log(introduce4('周润发', 28, '香港'))
info2('warn')
info3 = use_log(introduce4('成龙', 28, '香港'))
info3('xxx')

运行结果如下:

Python裝飾器-閉包與函數裝飾器

3.装饰器调用

方式一:以函数方式调用

info3 = wrapper_info(introduce3)
info3('刘德华', 28, '香港')

如果是装饰器函数带参数,则调用方式为:

info4 = use_log(introduce4('周星驰', 28, '香港'))
info4('info')

方式二:以语法糖方式调用

即在被装饰函数上方以@符号进行修饰

@wrapper_info
def introduce3(name, age, city):
print(f"我叫{name}, 我今年{age}岁了, 我来自{city}")

introduce3('刘德华', 28, '香港')

如果是装饰器函数带参数,例如上述的use_log,则需要在装饰器中传入参数:

@use_log('info')
def introduce4(name, age, city):
print(f"我叫{name}, 我今年{age}岁了, 我来自{city}")

小结

什么是装饰器?

在不改变原函数代码的情况下,给原函数增加了一些额外的功能,并且能够通用于其他函数,这样的函数就称作为装饰器。

装饰器的调用

可以通过传统调用函数的方式进行调用,也可以通过@装饰器的方式调用

装饰器的特点

  • 通过装饰器,可以在不修改原来函数的情况下对函数进行扩展
  • 一个函数可以同时指定多个装饰器

以上是Python裝飾器-閉包與函數裝飾器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:51cto.com。如有侵權,請聯絡admin@php.cn刪除