Sözcüksel kapanışlar nasıl çalışır?


149

Javascript kodunda sözcüksel kapatma ile yaşadığım bir sorunu araştırırken Python'da bu sorunla karşılaştım:

flist = []

for i in xrange(3):
    def func(x): return x * i
    flist.append(func)

for f in flist:
    print f(2)

Bu örneğin dikkatle önlendiğini unutmayın lambda. Şaşırtıcı olan "4 4 4" yazdırır. "0 2 4" beklerdim.

Bu eşdeğer Perl kodu bunu doğru yapar:

my @flist = ();

foreach my $i (0 .. 2)
{
    push(@flist, sub {$i * $_[0]});
}

foreach my $f (@flist)
{
    print $f->(2), "\n";
}

"0 2 4" yazdırılır.

Farkı açıklar mısınız?


Güncelleme:

Sorun değil ile iküresel olmak. Bu aynı davranışı görüntüler:

flist = []

def outer():
    for i in xrange(3):
        def inner(x): return x * i
        flist.append(inner)

outer()
#~ print i   # commented because it causes an error

for f in flist:
    print f(2)

Yorum satırının gösterdiği gibi, io noktada bilinmemektedir. Yine de "4 4 4" yazdırır.


2
jfs

3
İşte bu konuda oldukça iyi bir makale. me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

Yanıtlar:


151

Python aslında tanımlandığı gibi davranıyor. Üç ayrı işlev oluşturulur, ancak her birinin tanımlandıkları ortamın kapanışı vardır - bu durumda, küresel ortam (veya döngü başka bir işlevin içine yerleştirilirse dış işlevin ortamı). Bu tam olarak sorun olsa da - bu ortamda mutasyona uğradım ve kapakların hepsi aynı i'ye atıfta bulunuyor .

İşte gelebileceğim en iyi çözüm - bir işlev yaratıcısı oluşturun ve bunun yerine çağırın . Bu, oluşturulan işlevlerin her biri için farklı ortamları zorlar ve her birinde farklı bir i bulunur .

flist = []

for i in xrange(3):
    def funcC(j):
        def func(x): return x * j
        return func
    flist.append(funcC(i))

for f in flist:
    print f(2)

Yan etkileri ve fonksiyonel programlamayı karıştırdığınızda olan şey budur.


5
Çözümünüz ayrıca Javascript'te kullanılan çözümdür.
Eli Bendersky

9
Bu bir yanlış davranış değildir. Tam olarak tanımlandığı gibi davranıyor.
Alex Coventry

6
IMO piro daha iyi bir çözüme sahiptir stackoverflow.com/questions/233673/…
jfs

2
Belki de anlaşılır olması için en içteki "i" yi "j" olarak değiştirirdim.
eggsyntax

7
sadece bu şekilde tanımlamaya ne dersiniz:def inner(x, i=i): return x * i
dashesy

152

Döngüde tanımlanan işlevler i, değeri değişirken aynı değişkene erişmeye devam eder . Döngünün sonunda, tüm işlevler döngüdeki son değeri tutan aynı değişkeni gösterir: etki, örnekte bildirilen şeydir.

iDeğerini değerlendirmek ve kullanmak için, ortak bir kalıp bunu parametre varsayılanı olarak ayarlamaktır: parametre varsayılanları defifade yürütüldüğünde değerlendirilir ve böylece döngü değişkeninin değeri dondurulur.

Beklendiği gibi aşağıdakiler çalışır:

flist = []

for i in xrange(3):
    def func(x, i=i): # the *value* of i is copied in func() environment
        return x * i
    flist.append(func)

for f in flist:
    print f(2)

7
s / derleme zamanında / defifadenin yürütüldüğü anda /
jfs

23
Bu, onu korkunç yapan ustaca bir çözümdür.
Stavros Korokithakis

Bu çözümde bir sorun var: func'un iki parametresi var. Bu, değişken miktarda parametrelerle çalışmadığı anlamına gelir. Daha da kötüsü, func'u ikinci bir parametre ile çağırırsanız i, bu tanım orijinalin üzerine yazılır . :-(
Pascal

34

functoolsKitaplığı kullanarak bunu nasıl yapacağınız (ki sorunun sorulduğu sırada mevcut olduğundan emin değilim).

from functools import partial

flist = []

def func(i, x): return x * i

for i in xrange(3):
    flist.append(partial(func, i))

for f in flist:
    print f(2)

Beklendiği gibi 0 2 4 çıktılar.


Bunu gerçekten kullanmak istedim ama benim fonksiyonum aslında bir sınıf yöntemi ve geçen ilk değer benlik. Bunun etrafında yine de var mı?
Michael David Watson

1
Kesinlikle. Yöntem add (self, a, b) içeren bir Math sınıfınız olduğunu ve 'artış' yöntemini oluşturmak için a = 1 ayarlamak istediğinizi varsayalım. Ardından, 'my_math' sınıfınızın bir örneğini oluşturun; artış yönteminiz 'artış = kısmi (my_math.add, 1)' olacaktır.
Luca Invernizzi

2
Bu tekniği functools.partialmethod()python
3.4'ten itibaren

13

şuna bak:

for f in flist:
    print f.func_closure


(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)

Bu, döngü bittikten sonra 2 değerine sahip olacak olan aynı i değişken örneğine işaret ettikleri anlamına gelir.

Okunabilir bir çözüm:

for i in xrange(3):
        def ffunc(i):
            def func(x): return x * i
            return func
        flist.append(ffunc(i))

1
Sorum daha "genel". Python neden bu kusuru var? Sözcüksel kapanışları destekleyen bir dilin (Perl ve tüm Lisp hanedanı gibi) bunu doğru bir şekilde yapmasını beklerdim.
Eli Bendersky

2
Bir şeyin neden bir kusuru olduğunu sormak, bunun bir kusur olmadığını varsayar.
Null303

7

Olan şey i değişkeninin yakalanması ve fonksiyonların çağrıldığı anda bağlı olduğu değeri döndürmesidir. İşlevsel dillerde, bu tür bir durum hiçbir zaman ortaya çıkmaz, çünkü geri tepmeyeceğim. Ancak python ile ve lisp ile gördüğünüz gibi, bu artık doğru değil.

Şema örneğinizle fark, do döngüsünün anlambilimi ile ilgilidir. Şema, varolan bir i bağlayıcısını diğer dillerde olduğu gibi yeniden kullanmak yerine, döngü boyunca her seferinde yeni bir i değişkeni oluşturuyor. Döngü dışında oluşturulan farklı bir değişken kullanır ve onu değiştirirseniz, aynı davranışı şemada görürsünüz. Döngünüzü aşağıdakilerle değiştirmeyi deneyin:

(let ((ii 1)) (
  (do ((i 1 (+ 1 i)))
      ((>= i 4))
    (set! flist 
      (cons (lambda (x) (* ii x)) flist))
    (set! ii i))
))

Bununla ilgili daha fazla tartışma için buraya bir göz atın .

Bunu tanımlamanın daha iyi bir yolu, do döngüsünü aşağıdaki adımları gerçekleştiren bir makro olarak düşünmektir:

  1. Döngünün gövdesi tarafından tanımlanan bir gövdeyle tek bir parametre (i) alarak bir lambda tanımlayın,
  2. Parametresi olarak uygun i değerleri ile bu lambda'nın derhal çağrılması.

yani. Aşağıdaki python eşdeğeri:

flist = []

def loop_body(i):      # extract body of the for loop to function
    def func(x): return x*i
    flist.append(func)

map(loop_body, xrange(3))  # for i in xrange(3): body

İ artık ana kapsamdan değil, kendi kapsamındaki yepyeni bir değişkendir (yani lambda parametresi) ve böylece gözlemlediğiniz davranışı elde edersiniz. Python'un bu örtülü yeni kapsamı yoktur, bu nedenle for döngüsünün gövdesi sadece i değişkenini paylaşır.


İlginç. Do döngüsünün anlambilimindeki farkın farkında değildim. Teşekkürler
Eli Bendersky

4

Hala neden bazı dillerde bunun bir şekilde ve başka bir şekilde çalıştığına tamamen ikna olmadım. Common Lisp'te Python gibi:

(defvar *flist* '())

(dotimes (i 3 t)
  (setf *flist* 
    (cons (lambda (x) (* x i)) *flist*)))

(dolist (f *flist*)  
  (format t "~a~%" (funcall f 2)))

"6 6 6" (burada listenin 1'den 3'e ve tersine yerleştirilmiş olduğuna dikkat edin) yazdırır.) Şemadayken Perl'de olduğu gibi çalışır:

(define flist '())

(do ((i 1 (+ 1 i)))
    ((>= i 4))
  (set! flist 
    (cons (lambda (x) (* i x)) flist)))

(map 
  (lambda (f)
    (printf "~a~%" (f 2)))
  flist)

"6 4 2" yazdırır

Daha önce de belirttiğim gibi, Javascript Python / CL kampında. Burada farklı dillerin farklı şekillerde yaklaştığı bir uygulama kararı var gibi görünüyor. Kararın ne olduğunu tam olarak anlamak isterim.


8
Fark, kapsam belirleme kurallarından ziyade (do ...) 'dadır. Şemada, döngüden her geçişte yeni bir değişken yaratırken, diğer diller mevcut bağlamayı yeniden kullanır. Daha fazla ayrıntı ve lisp / python ile benzer davranışa sahip bir şema sürümü örneği için cevabımı görün.
Brian

2

Sorun, tüm yerel işlevlerin aynı ortama ve dolayısıyla aynı ideğişkene bağlanmasıdır. Çözüm (geçici çözüm), her işlev (veya lambda) için ayrı ortamlar (yığın kareleri) oluşturmaktır:

t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]

>>> t[1](2)
2
>>> t[2](2)
4

1

Değişken i, işlevi her seferinde değeri 2 olan bir globaldirf her çağrıldığında .

Aşağıdaki davranışları takip etme eğilimindeyim:

>>> class f:
...  def __init__(self, multiplier): self.multiplier = multiplier
...  def __call__(self, multiplicand): return self.multiplier*multiplicand
... 
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]

Güncellemenize yanıt : Bu davranışa neden olan i kendi başına küresellik değil, f çağrıldığı zamanlar boyunca sabit bir değere sahip bir çevreleme kapsamından bir değişken olması gerçeğidir. İkinci örneğinizde, değeri işlevin ikapsamından alınır ve kkkişlevleri çağırdığınızda hiçbir şey değişmez flist.


0

Davranışın ardındaki mantık zaten açıklanmıştır ve birden fazla çözüm yayınlanmıştır, ancak bunun en pitonik olduğunu düşünüyorum (unutmayın, Python'daki her şey bir nesnedir!):

flist = []

for i in xrange(3):
    def func(x): return x * func.i
    func.i=i
    flist.append(func)

for f in flist:
    print f(2)

Claudiu'nun cevabı bir fonksiyon üreteci kullanarak oldukça iyi, ama piro'nun cevabı dürüst olmak gerekirse, varsayılan bir değerle "gizli" bir argümana dönüştürdüğüm için (iyi çalışacak, ancak "pitonik" değil) .


Bence bu senin python sürümüne bağlı. Şimdi daha deneyimliyim ve artık böyle yapmayı önermem. Claudiu's, Python'da bir kapatma yapmanın doğru yoludur.
darkfeline

1
Bu Python 2 veya 3'te çalışmaz (her ikisi de "4 4 4" çıktısı alır). funcİçinde x * func.iher zaman tanımlanmış son fonksiyonuna sevk edecektir. Bu nedenle, her bir işlev ayrı ayrı doğru sayıya sahip olsa da, hepsi yine de sonuncusundan okuma yapar.
Lambda Fairy
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.