1. 程式人生 > 程式設計 >python閉包與引用以及需要注意的陷阱

python閉包與引用以及需要注意的陷阱

python閉包

關於閉包, 很多blog中都這樣解釋 :對於一個巢狀定義的函式,外層的函式的返回值是內層函式,而在內層函式中又引用了外層函式的區域性變數,在外層函式執行後,其區域性變數並非被回收,而會同返回的內層函式一同存在,而這一現象被稱為閉包(closure)。

不過以上的理解有些繁瑣和侷限, 在電腦科學中 ,閉包(Closure)詞法閉包(Lexical Closure)的簡稱,是引用了自由變數的函式。 這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函式和與其相關的引用環境組合而成的實體。 也即對於第一段中的定義可以適當放開一些限制條件,python中的閉包實現也並非那麼侷限。

引用

通過上文介紹可以對於python閉包有大概的瞭解, 但是有些看似簡單的細節卻需要進一步闡述 。

python中變數的概念,這是與C/C++中極為不同的,在C/C++中變數是一個名稱與記憶體合一的實體,改變一個變數的值,並不改變其記憶體的地址。 而變數這個概念在python中並不合用,很多場合它的運用都會讓人混淆 。

python中所使用的概念是引用和物件,即如a=123,a即是一個引用名稱,123是記憶體中所儲存的物件值。這其實更像是C/C++中的指標與其所指向的記憶體,可以看作python在此之上對語法進行了包裝。

回到之前討論的閉包話題,在其中用到了 變數 的概念,即函式引用的 變數 將與函式一同存在,這裡的 變數 其實是引用名稱與記憶體物件的複合概念。我們這裡對其進行進一步的闡明:

函式中所使用的外層函式引用名稱(指標),在外層函式退出後其所指向的記憶體物件並不回收,而該引用名稱(指標)會與內層函式一同存在,雖然此時該引用名稱(指標)對於內層函式不是“可見的”。

陷阱

def count(): 
  fs = [] 
  for i in range(1,4): 
    def f(): 
      return j*j 
    fs.append(f)
  return fs

f1,f2,f3 = count()
print(f1())
print(f2())
print(f3())

對於以上程式碼,假如按照C/C++中的概念去理解python中的變數,就會以為其輸出依次為1、2、3。其實不然,真正輸出為:3、3、3。根據上一小節中對於python中引用與閉包的闡述,在記憶體f函式中使用外層的引用名稱i,在迴圈中雖然將不同的f函式加入到列表fs中,但是它們都使用的是同一個引用i,而該引用最後對應的值為3。

再看一段程式碼,這個會稍微複雜一點

def test():
  for i in range(4):
    yield i
    
g=test()

for n in [1,10]:
  g=(n+i for i in g)
  
print(list(g))

上面這段程式碼的輸出,一時不查之下也會以為是11、12、13、14,而其真實結果卻是20、21、22、23,讓人一時抓不到頭腦。首先在for迴圈中的生成器表示式(n+i for i in g),它其實本質上是一個函式,寫成表示式的形式不過是一種語法糖,其函式形式為:

def gen(n):
  # g是外面全域性的那個生成器g
  for i in g:
    yield n+i

即生成器generator本身是一種演算法或是函式,只有在“呼叫”它的時候,也就是對其進行for或是list或是next之類的操作時,才會真正的有值流動。

那麼對於以上第二例子中的程式碼,在for迴圈內n=1時,g這個生成器被重新賦值,但注意它此時只是一個特殊的函式,此時的n與i並沒有真正相加,在for迴圈的第二輪n=10的時候,(n+i for i in g)表示式中對g才進行了呼叫,那麼此時流進函式的n值其實是10,也就是此時g這個生成器對應的值為10、11、12、13,也就是i所引用的是這些值,下面又以相同的n+i的形式創造一個新的生成器對g重新賦值,並退出迴圈。則自然,此時g中對應的值為20、21、22、23.

以上就是python閉包與引用以及需要注意的陷阱的詳細內容,更多關於python 閉包與引用的資料請關注我們其它相關文章!