閉包算是編程語言里一個比較常見的概念,但說實話,這個名詞有點晦澀,在查看了半天網上的資料后,還是有點不明就里,
我疑惑的點主要是:這個東西是用來解決什么問題的?或者說,他的作用是什么?
先說作用
查閱了很多資料后,總結有下面幾個作用:
- 在某區域變數的作用外,依然可以訪問到此區域變數
- 可以避免使用全域變數,從而減少可能帶來的影響(很多文章把此項也稱之為:保存當前的運行環境)
- 可以把多引數的函式變成單引數的函式(大多數文章把此項稱之為:可以根據外部作用域的區域變數得到不同的結果)
下面舉例子來分別說一下這幾個功能,
1. 訪問
In [33]: def test_1():
...: name = 'Tom'
...:
In [35]: name
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-35-9bc0cb2ed6de> in <module>
----> 1 name
NameError: name 'name' is not defined
In [48]: def test_2():
...: name = 'Tom'
...: def test_3():
...: print(name)
...: return test_3
In [49]: test = test_2()
In [50]: test
Out[50]: <function __main__.test_2.<locals>.test_3()>
In [51]: print(test)
<function test_2.<locals>.test_3 at 0x000001F9195D5EE8>
In [52]: test()
Tom
可以看到第一個例子中,是沒辦法獲取到name的值的;
第二個例子中,就可以得到name的值,當然,也不是直接訪問的,
實作的原理就是利用了區域變數的作用域,既然外面訪問不到區域變數name,那么就從函式里去訪問,間接的得到這個值,
在第二個例子中,呼叫test_2()的時候,就產生了一個閉包:test_3(),這個地方其實是閉包的一個典型的表現形式,然而看起來依然晦澀,所以私以為,從作用上來講,邏輯更順暢,
所以也有人這么稱呼閉包:
閉包就是攜帶狀態的函式,并且它的狀態可以完全對外隱藏起來,
2. 可以避免使用全域引數
假如函式的功能是對一個list進行append/pop操作,執行N步后,回傳其結果,我們可以這樣實作:
In [54]: nums = [1 ,2, 3, 4]
In [55]: def append_num(x):
...: nums.append(x)
...:
In [56]: append_num(5)
In [57]: nums
Out[57]: [1, 2, 3, 4, 5]
這個需求中,最重要的一步就是要保存執行的中間結果,所以需要用到全域變數,
但是很顯然,這樣實作并不優雅,當專案龐大或者多人合作的時候,全域變數可能帶來災難性的后果,同時,在函式中對全域變數進行操作,會導致程序不可控,
此時,閉包是一個很好的選擇,也就是說閉包可以保存類似需求中的中間變數,如下:
In [59]: def append_nums(nums):
...: def append_x(x):
...: nums.append(x)
...: return nums
...: return append_x
...:
In [60]: tmp = append_nums([1])
In [61]: tmp.append_x(2)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-61-f86fca8c96cf> in <module>
----> 1 tmp.append_x(2)
AttributeError: 'function' object has no attribute 'append_x'
In [62]: tmp(2)
Out[62]: [1, 2]
In [63]: tmp(3)
Out[63]: [1, 2, 3]
In [64]: tmp
Out[64]: <function __main__.append_nums.<locals>.append_x(x)>
可以看到,在宣告tmp之后,可以像呼叫函式一樣直接把引數直接傳遞給tmp,同時,每次執行,都是在上一次執行的結果上append,達到了我們的目的,同時看起來比較優雅,
再看tmp,會發現tmp本身就是一個函式,所以呼叫的話和函式使用是一樣的,
當然,這里也有一個明顯的好處:可以宣告不同的num,兩者互不干擾,
In [65]: tmp_1 = append_nums([3])
In [66]: tmp_1(0)
Out[66]: [3, 0]
是不是感覺又有點像類和物件的關系~
一般來說,當只有一個內部方法的時候,閉包比類更合適、更簡單,
同時,如果和1中的例子相比的話,是不是感覺復用性提高了很多?
有一個更常見的例子,就是棋牌游戲的
以一個類似棋盤游戲的例子來說明,假設棋盤大小為50*50,左上角為坐標系原點(0,0),我需要一個函式,接收2個引數,分別為方向(direction),步長(step),該函式控制棋子的運動,棋子運動的新的坐標除了依賴于方向和步長以外,當然還要根據原來所處的坐標點,用閉包就可以保持住這個棋子原來所處的坐標,
origin = [0, 0] # 坐標系統原點
legal_x = [0, 50] # x軸方向的合法坐標
legal_y = [0, 50] # y軸方向的合法坐標
def create(pos):
def player(direction,step):
# 這里應該首先判斷引數direction,step的合法性,比如direction不能斜著走,step不能為負等
# 然后還要對新生成的x,y坐標的合法性進行判斷處理,這里主要是想介紹閉包,就不詳細寫了,
new_x = pos[0] + direction[0]*step
new_y = pos[1] + direction[1]*step
pos[0] = new_x
pos[1] = new_y
#注意!此處不能寫成 pos = [new_x, new_y],原因在上文有說過
return pos
return player
player = create(origin) # 創建棋子player,起點為原點
print player([1,0],10) # 向x軸正方向移動10步
print player([0,1],20) # 向y軸正方向移動20步
print player([-1,0],10) # 向x軸負方向移動10步
3. 減少引數
這一點,其實在上面的例子中就能看出來:
在使用了閉包這個特性之后,宣告變數后,再使用的話,只需要傳遞里面函式的引數就好了,
再說定義
維基百科:
在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是參考了自由變數的函式,這個被參考的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外,所以,有另一種說法認為閉包是由函式和與其相關的參考環境組合而成的物體,
參考了自由變數的函式是什么?
def print_msg():
msg = "python"
def printer():
print(msg)
return printer
test = print_msg()
test()
這個例子中的printer其實就是一個參考了自由變數/外部臨時變數的函式,
通常情況來講,msg屬于print_msg的區域變數,只能再print_msg()執行的時候呼叫,
但上面的例子中,test宣告的時候,就是print_msg()執行的時候;但當test()執行的時候,我們依然拿到了msg的值,這就是閉包的一個特點,現在看是不是1中的例子更清楚些了?
那什么時候用閉包呢?除去上面舉例子的幾個場景,還有一個更為常見的:裝飾器,更詳細的說,其實裝飾器是閉包的一個應用場景,
def make_wrap(func):
def wrapper(*args):
print("before function")
func(*args)
print("after function")
return wrapper
@make_wrap
def print_msg(msg):
print(msg)
>>> print_msg("Hello")
before function
Hello
after function
最后說一些坑
1. 閉包無法修改外部函式的區域變數
In [69]: def outer():
...: x = 1
...: def inner():
...: x = 2
...: print(x)
...: print(x)
...: inner()
...: print(x)
...:
In [70]: outer()
1
2
1
2. 閉包無法直接訪問外部函式的區域變數
In [71]: def outer():
...: x = 1
...: def inner():
...: x = x + 2
...: return x
...: return inner
...:
In [72]: f = outer()
In [73]: f()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-73-c43e34e6d405> in <module>
----> 1 f()
<ipython-input-71-4b6c486c99d4> in inner()
2 x = 1
3 def inner():
----> 4 x = x + 2
5 return x
6 return inner
UnboundLocalError: local variable 'x' referenced before assignment
也就是說,inner函式可以訪問x,但無法改變他的值,原因也很好說,Python本來就這樣設計的,,
Python 就是這樣設計的,它認為在函式體中,如果對變數有賦值操作,則證明這個變數是一個區域變數,并且它只會從區域變數中去讀取資料,這樣設計可以避免我們在不知道的情況下,獲取到全域變數的值,從而導致一些錯誤資料的出現,
《流暢的Python》
那么非要這么做,怎么辦?Python3中有2種方法
In [74]: def outer():
...: x = 1
...: def inner(x=x):
...: x = x + 2
...: return x
...: return inner
...:
In [75]:
In [75]: f = outer()
In [76]: f()
Out[76]: 3
In [80]: def outer():
...: x = 1
...: def inner():
...: nonlocal x
...: x = x + 1
...: return x
...: return inner
...:
In [81]: outer()()
Out[81]: 2
3. 回圈體中,不存在作用域的概念
In [82]: nums = []
In [83]: for i in range(3):
...: def f():
...: return i
...: nums.append(f)
...:
In [84]: nums
Out[84]: [<function __main__.f()>, <function __main__.f()>, <function __main__.f()>]
In [85]: nums[1]
Out[85]: <function __main__.f()>
In [86]: nums[1]()
Out[86]: 2
In [87]: nums[0]()
Out[87]: 2
In [88]: nums[2]()
Out[88]: 2
理想的結果是0,1,2,實際結果2,2,2,
這個其實是Python的一個特性,很常見,懶加載,宣告的時候并不執行,當呼叫的時候才執行,
當然也很好解決:
# 直接賦值
In [90]: nums = []
In [91]: for i in range(3):
...: def f():
...: return i
...: nums.append(f())
...:
...:
In [92]: nums[0]
Out[92]: 0
In [93]: nums[1]
Out[93]: 1
In [94]: nums[2]
Out[94]: 2
# 和上述例子一樣的話,可以采用下面的方式
In [97]: nums = []
In [98]: for i in range(3):
...: def f(i=i):
...: return i
...: nums.append(f)
...:
In [99]: nums[0]
Out[99]: <function __main__.f(i=0)>
In [100]: nums[0]()
Out[100]: 0
In [101]: nums[1]()
Out[101]: 1
In [102]: nums[2]()
Out[102]: 2
參考
https://wiki.jikexueyuan.com/project/explore-python/Functional/closure.html
https://segmentfault.com/a/1190000004461404
https://www.liaoxuefeng.com/wiki/1022910821149312/1023021250770016
https://foofish.net/python-closure.html
https://blog.csdn.net/Yeoman92/article/details/67636060
https://zhuanlan.zhihu.com/p/22229197
https://www.imooc.com/article/38716
https://www.jianshu.com/p/3502bdf5485e
https://segmentfault.com/a/1190000008955952
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/115638.html
標籤:Python
上一篇:python對execl 處理
