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

