2012-09-14 31 views
86

好的,請耐心等待,我知道它會看起來非常複雜,但請幫我理解發生了什麼。Python嵌套函數中的局部變量

from functools import partial 

class Cage(object): 
    def __init__(self, animal): 
     self.animal = animal 

def gotimes(do_the_petting): 
    do_the_petting() 

def get_petters(): 
    for animal in ['cow', 'dog', 'cat']: 
     cage = Cage(animal) 

     def pet_function(): 
      print "Mary pets the " + cage.animal + "." 

     yield (animal, partial(gotimes, pet_function)) 

funs = list(get_petters()) 

for name, f in funs: 
    print name + ":", 
    f() 

給出:

cow: Mary pets the cat. 
dog: Mary pets the cat. 
cat: Mary pets the cat. 

所以基本上,爲什麼我沒有得到三種不同的動物嗎?是不是將cage'打包'到嵌套函數的本地範圍中?如果不是,調用嵌套函數如何查找局部變量?

我知道遇到這種問題通常意味着一個人「做錯了」,但我想知道發生了什麼。

+1

嘗試'爲[ '貓', '狗', '牛']'動物。 ..我敢肯定有人會一起來解釋這一點,但它是Python gotcha的其中一個:) –

回答

93

嵌套函數在執行時從父範圍查找變量,而不是在定義時查找。

函數體被編譯,並且'自由'變量(未在函數本身中按賦值定義)被驗證,然後作爲閉包單元綁定到函數,代碼使用索引引用每個單元格。 pet_function因此具有一個自由變量(cage),然後通過封閉單元索引0引用該自由變量。封閉本身指向get_petters函數中的局部變量cage

當您實際調用該函數時,在您調用函數時,該閉包將用於查看周圍範圍中的值cage。這就是問題所在。在您調用函數時,get_petters函數已經完成了計算結果。 cage局部變量在執行過程中的某個點被分配了'cow','dog''cat'字符串中的每一個,但在該函數結尾處,cage包含最後一個值'cat'。因此,當您調用每個動態返回的函數時,您將獲得打印值'cat'

解決方法是不依賴閉包。您可以使用部分函數代替,創建新函數範圍,或將該變量綁定爲關鍵字參數的默認值

  • 部分函數例如,使用functools.partial()

    from functools import partial 
    
    def pet_function(cage=None): 
        print "Mary pets the " + cage.animal + "." 
    
    yield (animal, partial(gotimes, partial(pet_function, cage=cage))) 
    
  • 創建一個新的範圍例如:

    def scoped_cage(cage=None): 
        def pet_function(): 
         print "Mary pets the " + cage.animal + "." 
        return pet_function 
    
    yield (animal, partial(gotimes, scoped_cage(cage))) 
    
  • 綁定變量作爲一個關鍵字參數的默認值:

    def pet_function(cage=cage): 
        print "Mary pets the " + cage.animal + "." 
    
    yield (animal, partial(gotimes, pet_function)) 
    

沒有必要在循環中定義scoped_cage函數,編譯只發生一次,而不是循環的每次迭代。

+0

今天我用一個腳本工作,在這面牆上打了3個小時。你的最後一點非常重要,而且是我遇到這個問題的主要原因。我在我的代碼中使用了閉包優化的回調函數,但是在循環中嘗試相同的技術是讓我感到滿意的。 – DrEsperanto

5

這從以下

for i in range(2): 
    pass 

print i is 1 

莖迭代作爲其最終值懶惰地存儲的i值之後。

作爲發電機的功能,將工作(即印刷依次在每個值),但轉化到它運行在發電機列表時,因此對cagecage.animal)的所有調用返回的貓。

11

我的理解是,當實際調用了yield的pet_function時,在父函數名稱空間中尋找籠子,而不是之前。

所以,當你做

funs = list(get_petters()) 

您生成3種功能,這將找到最後創建籠。

如果更換與去年環:

for name, f in get_petters(): 
    print name + ":", 
    f() 

你會真正得到:

cow: Mary pets the cow. 
dog: Mary pets the dog. 
cat: Mary pets the cat.