深入理解Python閉包與裝飾器從入門到進階
在Python編程中,閉包和裝飾器是兩個緊密相連且非常強大的特性。閉包是實現(xiàn)裝飾器的基礎(chǔ),而裝飾器則是對閉包的經(jīng)典應(yīng)用。在學習它們之前,我們先再重溫一下全局變量和局部變量。
一、全局變量和局部變量
1. 作用域
全局變量是函數(shù)內(nèi)外都能訪問,局部變量是只能在函數(shù)內(nèi)訪問。
2. 生命周期
全局變量伴隨著當前主程序的調(diào)用而創(chuàng)建,伴隨著主程序的結(jié)束而銷毀。
局部變量伴隨著當前所在函數(shù)的調(diào)用而創(chuàng)建,伴隨著函數(shù)的結(jié)束而銷毀。
3. 全局變量與局部變量的訪問范圍
① 在全局作用域中可以訪問全局變量,在局部作用域中可以訪問局部變量
# 全局作用域(全局變量)
num1 = 10
def func():
# 局部作用域(局部變量)
num2 = 20
# 在局部訪問局部變量
print(num2) #20
# 在全局訪問全局變量
print(num1) #10
# 調(diào)用函數(shù)
func()② 在局部作用域中可以訪問全局變量
# 全局作用域(全局變量)
num1 = 10
def func():
# 局部作用域(局部變量)
# 在局部作用域中可以訪問全局變量
print(num1) #10
# 調(diào)用函數(shù)
func()③ 在全局作用域中不能訪問局部變量
# 全局作用域(全局變量)
num1 = 10
def func():
# 局部作用域(局部變量)
num2 = 20
# 調(diào)用函數(shù)
func()
# 在全局作用域中調(diào)用局部變量num2
print(num2) # 報錯4. 問題:為什么在全局作用域中無法訪問局部變量呢?
答:主要原因在于,在Python的底層存在一個“垃圾回收機制”,主要的作用就是回收內(nèi)存空間,加快計算機的運行。我們在Python代碼中定義的變量也是需要占用內(nèi)存的,所以Python為了回收已經(jīng)被使用過的內(nèi)存,會自動將函數(shù)運行以后的內(nèi)部變量和程序直接回收。
當調(diào)用完函數(shù)后,函數(shù)內(nèi)定義的變量就銷毀了,那么如何讓局部變量再多待一會,不立刻銷毀呢?即可不可以改變函數(shù)內(nèi)變量的生命周期呢?
答案是肯定的———可以通過閉包來實現(xiàn)
二、閉包
1. 閉包的概念和作用
?概念:在函數(shù)嵌套的前提下,內(nèi)部函數(shù)使用了外部函數(shù)的變量,并且外部函數(shù)返回了內(nèi)部函數(shù),我們把這個使用外部函數(shù)變量的內(nèi)部函數(shù)稱為閉包。
作用:閉包可以保存函數(shù)內(nèi)的變量,而不會隨著調(diào)用完函數(shù)而被銷毀。
2.閉包的格式
def 外部函數(shù)名(局部變量): ?? ?def 內(nèi)部函數(shù)名(): ?? ??? ?# 內(nèi)部函數(shù)使用了外部函數(shù)的局部變量 ?? ?return 內(nèi)部函數(shù)地址
3. 閉包需要滿足三個條件
- 有嵌套:外部函數(shù)內(nèi)嵌套了內(nèi)部函數(shù)
- 有引用:內(nèi)部函數(shù)用了外部函數(shù)中的局部變量
- 有返回:外部函數(shù)返回了內(nèi)部函數(shù)名(實際上就是返回內(nèi)部函數(shù)的地址)
'''
閉包程序三步走:1、有嵌套 2、有引用 3、有返回
'''
def func():
num = 20 # 局部變量
def inner():
print(num)
return inner # 實際上inner函數(shù)并沒有執(zhí)行,只是返回了inner函數(shù)在內(nèi)存中的地址
f = func() # 相當于把inner在內(nèi)存中的地址0x...賦值給變量f
f() # 找到inner函數(shù)的內(nèi)存地址,并執(zhí)行器內(nèi)部的代碼(num=20),在于閉包函數(shù)保留了num=20這個局部變量
# 輸出結(jié)果
20閉包的作用:正常情況下,當執(zhí)行func()的時候,函數(shù)內(nèi)部的變量num = 20,會隨著函數(shù)的func函數(shù)的結(jié)束而被垃圾回收機制所回收。所以閉包的真正作用:就是可以在全局作用域中,實現(xiàn)間接對局部變量進行訪問。
4. 注意事項
由于閉包引用了外部函數(shù)的變量,所以外部函數(shù)的變量并沒有及時釋放,消耗內(nèi)存。
5. 在閉包的內(nèi)部實現(xiàn)對外部變量的修改
錯誤版本:
'''
Python閉包:① 有嵌套 ② 有引用 ③ 有返回
'''
def outer():
num = 10
def inner():
# 這種寫法無法實現(xiàn)通過閉包修改外部的局部變量
num = 20
print('outer函數(shù)中的num:', num) # 10
inner() # 執(zhí)行函數(shù)inner,讓num=20生效
print('outer函數(shù)中的num:', num) # 10
return inner
f = outer()
f()
# 運行結(jié)果
outer函數(shù)中的num: 10
outer函數(shù)中的num: 10正確版本:
nonlocal關(guān)鍵字:在函數(shù)內(nèi)部修改函數(shù)外部的變量,這個變量非全局變量
global關(guān)鍵字:在函數(shù)內(nèi)部聲明變量,代表引用全局作用域中的全局變量
'''
Python閉包:① 有嵌套 ② 有引用 ③ 有返回
'''
def outer():
num = 10
def inner():
# 這種寫法無法實現(xiàn)通過閉包修改外部的局部變量'
nonlocal num
num = 20
print('outer函數(shù)中的num:', num) # 10
inner() # 執(zhí)行函數(shù)inner,讓num=20生效
print('outer函數(shù)中的num:', num) # 20
return inner
f = outer()
f()
# 運行結(jié)果
outer函數(shù)中的num: 10
outer函數(shù)中的num: 20global和nonlocal核心區(qū)別:global定義全局變量,在任意函數(shù)內(nèi)修改全局變量。onlocal在有嵌套函數(shù)的前提下,只能在內(nèi)部函數(shù)中修改外部函數(shù)的局部變量。
6. 閉包的綜合案例
閉包的作用:可以在全局作用域中間接訪問局部變量(在函數(shù)執(zhí)行以后)
def func():
result = 0
def inner(num):
nonlocal result
result += num
print(result)
return inner
f = func()
f(1) # 1
f(2) # 3分析:
執(zhí)行f = func()的時候,result賦值為0,然后定義inner,返回inner,最終結(jié)果f = inner函數(shù)的內(nèi)存地址
執(zhí)行f(1),相當于執(zhí)行inner函數(shù),nonlocal引用局部變量result=0,然后進行+1操作,彈出0+1=1
繼續(xù)執(zhí)行
執(zhí)行f(2),相當于執(zhí)行inner函數(shù),聲明nonlocal result,代表還是引用外部的局部變量,由于此時外部的result已經(jīng)被f(1)更改為1了,所以由于局部變量一直沒有消失,所以此時result=1,執(zhí)行+2操作,最終結(jié)果為3
注意:閉包會延長外部變量的生命周期,如果濫用可能導致內(nèi)存占用增加。
三、裝飾器入門
1. 什么是裝飾器
裝飾器:在不改變現(xiàn)有函數(shù)源代碼以及函數(shù)調(diào)用方式的前提下,實現(xiàn)給函數(shù)增加額外的功能,使用裝飾器中的內(nèi)部函數(shù)充當原有函數(shù)使用。
裝飾器的本質(zhì)就是一個閉包函數(shù)。
2. 格式
def 外部函數(shù)名(局部變量): ?? ?def 內(nèi)部函數(shù)名(): ?? ??? ?# TODO 在不改變原始函數(shù)基礎(chǔ)上,添加額外功能 ?? ??? ?內(nèi)部函數(shù)使用了外部函數(shù)的局部變量 ?? ?return 內(nèi)部函數(shù)地址
3. 裝飾器雛形(傳統(tǒng)方式)
語法:變量名 = 裝飾器名(原有函數(shù)名)
變量名()
假設(shè)我們有一個評論功能,需要先登錄才能執(zhí)行。我們可以用閉包包裝一下:
# 要求:把登錄功能封裝起來(比如封裝成一個函數(shù),添加這個登錄不能影響現(xiàn)有功能函數(shù))
'''
裝飾器:本質(zhì)是一個閉包,有嵌套、有引用、有返回(返回的是函數(shù)的內(nèi)存地址)
參數(shù)fn在check中也是一個局部變量
參數(shù)fn:就是要裝飾的函數(shù)的函數(shù)名,如comment,如download
'''
def check(fn):
def inner():
# 開發(fā)登錄功能
print('登錄功能')
# 調(diào)用原函數(shù)
fn()
return inner
# 評論功能(前提:登錄)
def comment():
print('評論功能')
comment = check(comment)
comment()
# 下載功能(前提:登錄)
def download():
print('下載功能')
download = check(download)
download()
# 運行結(jié)果
登錄功能
評論功能
登錄功能
下載功能上面的 comment = check(comment) 可以簡化為 @check 放在函數(shù)定義上方。
4. 裝飾器定義(語法糖方式)
語法:@裝飾器名
def check(fn):
def inner():
# 開發(fā)登錄驗證功能
print('驗證登錄')
# 執(zhí)行原有函數(shù)
fn()
return inner
@check
def comment():
print('發(fā)表評論')
comment()
# 運行結(jié)果
驗證登錄
發(fā)表評論5. 裝飾器案例:獲取程序的執(zhí)行時間
# 定義獲取程序的執(zhí)行時間裝飾器
import time
def get_time(fn):
def inner():
# ① 添加裝飾器修飾功能(獲取程序的執(zhí)行時間)
begin = time.time()
# ② 調(diào)用fn函數(shù),執(zhí)行原函數(shù)代碼
fn()
end = time.time()
print(f'這個函數(shù)的執(zhí)行時間:{end - begin}')
return inner
@get_time
def demo():
sum=0
for i in range(1000000):
sum+=i
print(sum)
demo()
# 運行結(jié)果
499999500000
這個函數(shù)的執(zhí)行時間:0.05813407897949219四、裝飾器進階
1. 帶有參數(shù)裝飾器
'''
帶有參數(shù)的裝飾器:① 有嵌套 ② 有引用 ③ 有返回
'''
def logging(fn):
def inner(*args, **kwargs):
# 添加裝飾器代碼(輸出日志信息)
print('-- 日志信息... --')
# 執(zhí)行要修飾的函數(shù)
fn(*args, **kwargs) # sum_num(a, b)
return inner
@logging
def sum_num(*args, **kwargs):
result = 0
# *args代表不定長元組參數(shù),args = (10, 20)
for i in args:
result += i
# **kwargs代表不定長字典參數(shù), kwargs = {a:30, b:40}
for i in kwargs.values():
result += i
print(result)
# sum_num帶4個參數(shù),而且類型不同,10和20以元組形式傳遞,a=30,b=40以字典形式傳遞
sum_num(10, 20, a=30, b=40)
# 運行結(jié)果
-- 日志信息... --
1002. 帶有返回值裝飾器
'''
帶有返回值的裝飾器:① 有嵌套 ② 有引用 ③ 有返回
如果一個函數(shù)執(zhí)行完畢后,沒有return返回值,則默認返回None
'''
def logging(fn):
def inner(*args, **kwargs):
print('-- 日志信息... --')
return fn(*args, **kwargs) # fn() = sub_num(20, 10) = result
return inner
@logging
def sub_num(a, b):
result = a - b
return result
print(sub_num(20, 10))
# 運行結(jié)果
-- 日志信息... --
103. 通用版本的裝飾器
'''
通用裝飾器:① 有嵌套 ② 有引用 ③ 有返回 ④ 有不定長參數(shù) ⑤ 有return返回值
'''
def logging(fn):
def inner(*args, **kwargs):
# 輸出裝飾器功能
print('-- 正在努力計算 --')
# 調(diào)用fn函數(shù)
return fn(*args, **kwargs)
return inner
@logging
def sum_num1(a, b):
result = a + b
return result
print(sum_num1(20, 10))
@logging
def sum_num2(a, b, c):
result = a + b + c
return result
print(sum_num2(10, 20, 30))
# 運行結(jié)果
-- 正在努力計算 --
30
-- 正在努力計算 --
604. 裝飾器高級:使用裝飾器傳遞參數(shù)
注意:裝飾器一次只能接收一個參數(shù)
基本語法:
def 裝飾器(fn):
...
@裝飾器('參數(shù)')
def 函數(shù)():
# 函數(shù)代碼實例代碼:根據(jù)傳遞參數(shù)不同,打印不同的日志信息
'''
通用裝飾器:① 有嵌套 ② 有引用 ③ 有返回 ④ 有不定長參數(shù) ⑤ 有return返回值
真正問題:通過裝飾器傳遞參數(shù),我們應(yīng)該如何接收這個參數(shù)呢?
答:在logging方法的外側(cè)在添加一個函數(shù),專門用于接收傳遞過來的參數(shù)
'''
def logging(flag):
# flag = + 或 flag = -
def decorator(fn):
def inner(*args, **kwargs):
if flag == '+':
print('-- 日志信息:正在進行加法運算 --')
elif flag == '-':
print('-- 日志信息:正在進行減法運算 --')
return fn(*args, **kwargs)
return inner
return decorator
@logging('+')
def sum_num(a, b):
result = a + b
return result
@logging('-')
def sub_num(a, b):
result = a - b
return result
print(sum_num(10, 20))
print(sub_num(100, 80))
# 運行結(jié)果
-- 日志信息:正在進行加法運算 --
30
-- 日志信息:正在進行減法運算 --
20五、補充內(nèi)容
1. 多個裝飾器的執(zhí)行順序
多個裝飾器疊加時,靠近函數(shù)的裝飾器先執(zhí)行(由內(nèi)向外包裝,由外向內(nèi)執(zhí)行)。
python
def deco1(fn):
? ? def inner():
? ? ? ? print('deco1 開始')
? ? ? ? fn()
? ? ? ? print('deco1 結(jié)束')
? ? return inner
def deco2(fn):
? ? def inner():
? ? ? ? print('deco2 開始')
? ? ? ? fn()
? ? ? ? print('deco2 結(jié)束')
? ? return inner
@deco1
@deco2
def hello():
? ? print('Hello')
hello()
# 運行結(jié)果:
deco1 開始
deco2 開始
Hello
deco2 結(jié)束
deco1 結(jié)束2. 使用 functools.wraps 保留原函數(shù)信息
裝飾器會覆蓋原函數(shù)的 __name__、__doc__ 等屬性。使用 @wraps 可以解決這個問題。
python
from functools import wraps
def my_decorator(fn):
? ? @wraps(fn)
? ? def inner(*args, **kwargs):
? ? ? ? return fn(*args, **kwargs)
? ? return inner
@my_decorator
def say_hello():
? ? """這是一個打招呼的函數(shù)"""
? ? print('Hello')
print(say_hello.__name__) ? # 輸出 say_hello,而不是 inner
print(say_hello.__doc__) ? ?# 輸出文檔字符串3. 類裝飾器
除了函數(shù),類也可以用作裝飾器。需要實現(xiàn) __call__ 方法。
python
class CountCalls:
? ? def __init__(self, fn):
? ? ? ? self.fn = fn
? ? ? ? self.count = 0
? ? def __call__(self, *args, **kwargs):
? ? ? ? self.count += 1
? ? ? ? print(f'調(diào)用次數(shù):{self.count}')
? ? ? ? return self.fn(*args, **kwargs)
@CountCalls
def test():
? ? print('執(zhí)行函數(shù)')
test()
test()
# 運行結(jié)果:
調(diào)用次數(shù):1
執(zhí)行函數(shù)
調(diào)用次數(shù):2
執(zhí)行函數(shù)4. 裝飾器的典型應(yīng)用場景
日志記錄:自動記錄函數(shù)調(diào)用信息
權(quán)限校驗:檢查用戶是否登錄或具有權(quán)限
性能計時:計算函數(shù)執(zhí)行時間
緩存:緩存函數(shù)返回值
事務(wù)處理:數(shù)據(jù)庫操作的自動提交/回滾
輸入驗證:檢查參數(shù)合法性
六、總結(jié)
1. 閉包是函數(shù)嵌套、引用外部變量、返回內(nèi)部函數(shù)的組合體,它可以保留外部函數(shù)的局部變量,實現(xiàn)在全局作用域中間接訪問局部變量。
2. 裝飾器是閉包最經(jīng)典的應(yīng)用,它可以在不修改原函數(shù)代碼和調(diào)用方式的前提下,動態(tài)地添加額外功能。
3. 從簡單的無參裝飾器,到處理參數(shù)和返回值的通用裝飾器,再到接收參數(shù)的裝飾器工廠,理解裝飾器的關(guān)鍵在于理解函數(shù)也是對象以及閉包的作用域規(guī)則。
4. 使用 functools.wraps 可以避免裝飾器覆蓋原函數(shù)的元數(shù)據(jù),是一個良好的編程習慣。
到此這篇關(guān)于深入理解Python閉包與裝飾器從入門到進階的文章就介紹到這了,更多相關(guān)Python閉包與裝飾器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Django 解決上傳文件時,request.FILES為空的問題
這篇文章主要介紹了Django 解決上傳文件時,request.FILES為空的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-05-05
Python實現(xiàn)報警信息實時發(fā)送至郵箱功能(實例代碼)
這篇文章主要介紹了Python實現(xiàn)報警信息實時發(fā)送至郵箱,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-11-11
python內(nèi)置函數(shù)breakpoint()與bytearray()示例詳解
本文給大家分享的是python內(nèi)置函數(shù)breakpoint()與bytearray()的相關(guān)資料,并給大家附上了詳細代碼,有需要的小伙伴可以參考下2017-04-04
pandas刪除部分數(shù)據(jù)后重新生成索引的實現(xiàn)
這篇文章主要介紹了pandas刪除部分數(shù)據(jù)后重新生成索引的實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07
Python可視化Matplotlib折線圖plot用法詳解
這篇文章主要為大家介紹了Python可視化中Matplotlib折線圖plot用法的詳解,有需要的朋友可以借鑒參考下,希望可以有所幫助,祝大家多多進步2021-09-09

