Variabel lokal dalam fungsi bertingkat

105

Oke, bersabarlah dengan ini, saya tahu ini akan terlihat sangat berbelit-belit, tapi tolong bantu saya memahami apa yang terjadi.

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()

Memberikan:

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

Jadi pada dasarnya, mengapa saya tidak mendapatkan tiga hewan yang berbeda? Bukankah cage'dikemas' ke dalam lingkup lokal dari fungsi bersarang? Jika tidak, bagaimana panggilan ke fungsi bersarang mencari variabel lokal?

Saya tahu bahwa mengalami masalah seperti ini biasanya berarti seseorang 'melakukannya dengan salah', tetapi saya ingin memahami apa yang terjadi.

noio
sumber
1
Coba for animal in ['cat', 'dog', 'cow']... Saya yakin seseorang akan datang dan menjelaskan hal ini - ini salah satu dari Python gotcha :)
Jon Clements

Jawaban:

114

Fungsi bertingkat mencari variabel dari lingkup induk saat dijalankan, bukan saat ditentukan.

Badan fungsi dikompilasi, dan variabel 'bebas' (tidak ditentukan dalam fungsi itu sendiri oleh penetapan), diverifikasi, lalu diikat sebagai sel penutup ke fungsi, dengan kode menggunakan indeks untuk mereferensikan setiap sel. pet_functionsehingga memiliki satu variabel bebas ( cage) yang kemudian direferensikan melalui sel penutup, indeks 0. Penutup itu sendiri menunjuk ke variabel lokal cagedalam get_pettersfungsi tersebut.

Ketika Anda benar-benar memanggil fungsi tersebut, closure tersebut kemudian digunakan untuk melihat nilai cagedalam lingkup sekitarnya pada saat Anda memanggil fungsi tersebut . Di sinilah letak masalahnya. Pada saat Anda memanggil fungsi Anda, get_pettersfungsi tersebut sudah selesai menghitung hasilnya. The cagevariabel lokal di beberapa titik selama eksekusi yang ditugaskan masing-masing 'cow', 'dog'dan 'cat'string, tapi pada akhir fungsi, cageberisi bahwa nilai terakhir 'cat'. Jadi, saat Anda memanggil setiap fungsi yang dikembalikan secara dinamis, Anda mendapatkan nilai yang 'cat'dicetak.

Solusinya adalah tidak bergantung pada penutupan. Sebagai gantinya, Anda dapat menggunakan fungsi parsial , membuat cakupan fungsi baru , atau mengikat variabel sebagai nilai default untuk parameter kata kunci .

  • Contoh fungsi parsial, menggunakan 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)))
  • Membuat contoh lingkup baru:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
  • Mengikat variabel sebagai nilai default untuk parameter kata kunci:

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

Tidak perlu mendefinisikan scoped_cagefungsi dalam loop, kompilasi hanya dilakukan satu kali, tidak pada setiap iterasi loop.

Martijn Pieters
sumber
1
Aku membenturkan kepalaku di dinding ini selama 3 jam hari ini pada naskah untuk pekerjaan. Poin terakhir Anda sangat penting, dan merupakan alasan utama mengapa saya mengalami masalah ini. Saya memiliki callback dengan penutupan yang berlimpah di seluruh kode saya, tetapi mencoba teknik yang sama dalam satu lingkaran adalah apa yang membuat saya.
DrEsperanto
12

Pemahaman saya adalah bahwa kandang dicari di namespace fungsi induk ketika pet_function yang dihasilkan sebenarnya dipanggil, bukan sebelumnya.

Jadi, saat Anda melakukannya

funs = list(get_petters())

Anda menghasilkan 3 fungsi yang akan menemukan kandang yang terakhir dibuat.

Jika Anda mengganti loop terakhir Anda dengan:

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

Anda benar-benar akan mendapatkan:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.
Nicolas Barbey
sumber
6

Ini berasal dari berikut ini

for i in range(2): 
    pass

print(i)  # prints 1

setelah mengulang nilai i disimpan dengan malas sebagai nilai akhirnya.

Sebagai generator, fungsi akan bekerja (yaitu mencetak setiap nilai secara bergiliran), tetapi ketika mentransformasikannya ke daftar, fungsi tersebut berjalan di atas generator , maka semua panggilan ke cage( cage.animal) mengembalikan kucing.

Andy Hayden
sumber
0

Mari sederhanakan pertanyaannya. Menetapkan:

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        def pet_function():
            return "Mary pets the " + animal + "."

        yield (animal, pet_function)

Kemudian, seperti dalam pertanyaan, kita mendapatkan:

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

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

Tetapi jika kita menghindari membuat yang list()pertama:

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

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

Apa yang sedang terjadi? Mengapa perbedaan halus ini benar-benar mengubah hasil kami?


Jika kita lihat list(get_petters()), jelas dari alamat memori yang berubah bahwa kita memang menghasilkan tiga fungsi yang berbeda:

>>> list(get_petters())

[('cow', <function get_petters.<locals>.pet_function at 0x7ff2b988d790>),
 ('dog', <function get_petters.<locals>.pet_function at 0x7ff2c18f51f0>),
 ('cat', <function get_petters.<locals>.pet_function at 0x7ff2c14a9f70>)]

Namun, perhatikan cellbahwa fungsi-fungsi ini terikat:

>>> for _, f in list(get_petters()):
...     print(f(), f.__closure__)

Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)

>>> for _, f in get_petters():
...     print(f(), f.__closure__)

Mary pets the cow. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a95670>,)
Mary pets the dog. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a952f0>,)
Mary pets the cat. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c3f437f0>,)

Untuk kedua loop, cellobjek tetap sama selama iterasi. Namun, seperti yang diharapkan, strreferensi spesifik itu bervariasi di loop kedua. The cellobjek mengacu pada animal, yang dibuat ketika get_petters()disebut. Namun, animalmengubah apastr objek yang dirujuknya saat fungsi generator berjalan .

Di loop pertama, selama setiap iterasi, kami membuat semua file f s, tetapi kita hanya memanggilnya setelah generator get_petters()benar-benar habis dan a listdari fungsi sudah dibuat.

Di loop kedua, selama setiap iterasi, kami menjeda file get_petters() generator dan memanggil fsetelah setiap jeda. Jadi, kami akhirnya mengambil nilai animalpada saat fungsi generator dihentikan sementara.

Seperti yang @Claudiu berikan untuk menjawab pertanyaan serupa :

Tiga fungsi terpisah dibuat, tetapi masing-masing memiliki penutupan lingkungan tempat mereka didefinisikan - dalam hal ini, lingkungan global (atau lingkungan fungsi luar jika loop ditempatkan di dalam fungsi lain). Namun, inilah masalahnya - di lingkungan ini,animal bermutasi, dan closure semuanya mengacu pada hal yang sama animal.

[Catatan editor: itelah diubah menjadi animal.]

Mateen Ulhaq
sumber