İç içe geçmiş işlevlerdeki yerel değişkenler


105

Tamam, bu konuda bana katlanın, korkunç derecede karmaşık görüneceğini biliyorum, ama lütfen neler olduğunu anlamama yardım edin.

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

Verir:

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

Yani temel olarak, neden üç farklı hayvan almıyorum? cageİç içe geçmiş işlevin yerel kapsamı içinde 'paketlenmiş' değil mi? Değilse, yuvalanmış işleve yapılan çağrı yerel değişkenleri nasıl arar?

Bu tür sorunlarla karşılaşmanın genellikle 'yanlış yapmak' anlamına geldiğini biliyorum, ancak ne olduğunu anlamak isterim.


1
Deneyin for animal in ['cat', 'dog', 'cow']... Eminim birisi gelip bunu açıklayacaktır - bu Python gotcha'lardan biridir :)
Jon Clements

Yanıtlar:


114

Yuvalanmış işlev, tanımlandığında değil çalıştırıldığında üst kapsamdaki değişkenleri arar.

İşlev gövdesi derlenir ve 'serbest' değişkenler (işlevin kendisinde atanarak tanımlanmayan) doğrulanır, ardından her hücreye başvurmak için bir indeks kullanan kodla, işleve kapanış hücreleri olarak bağlanır. pet_functionböylelikle bir serbest değişkene ( cage) sahiptir ve bu daha sonra bir kapatma hücresi, indeks O aracılığıyla referans alınır. Kapanışın kendisi cage, get_pettersfonksiyondaki yerel değişkene işaret eder .

Aslında işlevini çağırdığınızda, kapatma sonra değerine bakmak için kullanılır cageçevreleyen kapsamda işlevi çağırmak anda . Sorun burada yatıyor. Fonksiyonlarınızı çağırdığınızda, fonksiyon get_petterssonuçlarını hesaplamak için zaten bitmiştir. cageBu yürütme sırasında bir noktada yerel değişken her birine tahsis edilmiş 'cow', 'dog've 'cat'yaylar, ancak işlevi sonunda, cageson değeri içerir 'cat'. Böylece, dinamik olarak döndürülen işlevlerin her birini çağırdığınızda, 'cat'yazdırılan değeri alırsınız .

Çözüm, kapanışlara dayanmamaktır. Bunun yerine kısmi bir işlev kullanabilir , yeni bir işlev kapsamı oluşturabilir veya değişkeni bir anahtar kelime parametresi için varsayılan değer olarak bağlayabilirsiniz .

  • Kısmi işlev örneği, şunu kullanarak 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)))
  • Yeni bir kapsam örneği oluşturma:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
  • Değişkeni bir anahtar kelime parametresi için varsayılan değer olarak bağlama:

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

Döngüde scoped_cageişlevi tanımlamaya gerek yoktur , derleme döngünün her yinelemesinde değil yalnızca bir kez gerçekleşir.


1
Bugün 3 saat boyunca bir iş senaryosu üzerine kafamı bu duvara vurdum. Son noktanız çok önemlidir ve bu sorunla karşılaşmamın ana nedeni budur. Kodum boyunca çok sayıda kapanışa sahip geri aramalarım var, ancak aynı tekniği bir döngüde denemek beni sağlayan şeydi.
DrEsperanto

12

Anladığım kadarıyla, verilen pet_function aslında daha önce değil çağrıldığında, kafes ana işlev ad alanında aranır.

Yani ne zaman yaparsan

funs = list(get_petters())

En son oluşturulan kafesi bulacak 3 işlev oluşturursunuz.

Son döngünüzü şu şekilde değiştirirseniz:

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

Aslında alacaksın:

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

6

Bu şunlardan kaynaklanıyor

for i in range(2): 
    pass

print(i)  # prints 1

değeri yinelendikten sonra i, son değeri olarak tembel olarak saklanır.

Bir üreteç olarak işlev çalışır (yani her bir değeri sırayla yazdırır), ancak bir listeye dönüştürüldüğünde, üreteç üzerinden çalışır , dolayısıyla tüm cage( cage.animal) çağrıları kedileri döndürür.


0

Soruyu basitleştirelim. Tanımlamak:

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

        yield (animal, pet_function)

Sonra, aynı soruda olduğu gibi, şunu elde ederiz:

>>> 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.

Ancak bir list()ilk oluşturmaktan kaçınırsak :

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

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

Neler oluyor? Bu ince fark neden sonuçlarımızı tamamen değiştiriyor?


Bakarsak list(get_petters()), değişen bellek adreslerinden gerçekten üç farklı işlev verdiğimiz anlaşılıyor:

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

Ancak, cellbu işlevlerin bağlı olduğu 'lere bir göz atın :

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

Her iki döngü için cellnesne yinelemeler boyunca aynı kalır. Bununla birlikte, beklendiği gibi str, ikinci döngüde referans verdiği spesifikler değişir. cellNesne belirtmektedir animalzaman oluşturulduğu, get_petters()olarak adlandırılır. Bununla birlikte, jeneratör işlevi çalışırken ifade ettiği nesneyi animaldeğiştirir .str

İlk döngüde, her yineleme sırasında, tüm s'leri oluştururuz f, ancak bunları yalnızca jeneratör get_petters()tamamen tükendikten ve bir listişlevler zaten oluşturulduktan sonra çağırırız .

İkinci döngüde, her yineleme sırasında, get_petters()üreteci durduruyoruz ve fher duraklamadan sonra arıyoruz . Böylelikle, animaljeneratör fonksiyonunun duraklatıldığı o andaki değeri elde etmiş oluruz .

@Claudiu'nun benzer bir soruya yanıt verdiği gibi :

Üç ayrı işlev oluşturulur, ancak her biri tanımlandıkları ortamın kapanışına sahiptir - bu durumda, küresel çevre (veya döngü başka bir işlevin içine yerleştirilmişse dış işlevin ortamı). Yine de sorun tam olarak budur - bu ortamda, animalmutasyona uğramıştır ve kapanışların tümü aynıdır animal.

[Editör notu: iolarak değiştirildi animal.]

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.