Come funzionano le chiusure lessicali?


149

Mentre stavo studiando un problema che avevo con le chiusure lessicali nel codice Javascript, ho riscontrato questo problema in Python:

flist = []

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

for f in flist:
    print f(2)

Si noti che questo esempio evita consapevolmente lambda. Stampa "4 4 4", il che è sorprendente. Mi aspetto "0 2 4".

Questo codice Perl equivalente fa bene:

my @flist = ();

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

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

Viene stampato "0 2 4".

Puoi per favore spiegare la differenza?


Aggiornare:

Il problema non è con l' iessere globale. Questo mostra lo stesso comportamento:

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)

Come mostra la riga commentata, iè sconosciuto a quel punto. Tuttavia, stampa "4 4 4".



3
Ecco un buon articolo su questo problema. me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

Risposte:


151

Python si sta effettivamente comportando come definito. Vengono create tre funzioni separate , ma ognuna di esse ha la chiusura dell'ambiente in cui sono definite - in questo caso, l'ambiente globale (o l'ambiente della funzione esterna se il loop è inserito in un'altra funzione). Questo è esattamente il problema, però - in questo ambiente, sono mutato e le chiusure si riferiscono tutte allo stesso io .

Qui è la soluzione migliore che posso venire con - creare una funzione creater e invocare che , invece. Ciò costringerà ambienti diversi per ciascuna delle funzioni create, con un diverso io in ciascuna.

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)

Questo è ciò che accade quando mescoli effetti collaterali e programmazione funzionale.


5
La tua soluzione è anche quella utilizzata in Javascript.
Eli Bendersky,

9
Questo non è un comportamento scorretto. Si sta comportando esattamente come definito.
Alex Coventry,

6
IMO piro ha una soluzione migliore stackoverflow.com/questions/233673/…
jfs

2
Vorrei forse cambiare la "i" più interna in "j" per chiarezza.
eggsyntax,

7
che dire di definire soltanto in questo modo:def inner(x, i=i): return x * i
dashesy

152

Le funzioni definite nel loop continuano ad accedere alla stessa variabile imentre il suo valore cambia. Alla fine del loop, tutte le funzioni puntano alla stessa variabile, che contiene l'ultimo valore nel loop: l'effetto è quello riportato nell'esempio.

Per valutare ie utilizzare il suo valore, un modello comune è quello di impostarlo come parametro predefinito: i valori predefiniti dei parametri vengono valutati quando defviene eseguita l' istruzione e quindi il valore della variabile loop viene congelato.

Quanto segue funziona come previsto:

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 / in fase di compilazione / nel momento in cui defviene eseguita l'istruzione /
jfs

23
Questa è una soluzione geniale, che la rende orribile.
Stavros Korokithakis,

C'è un problema con questa soluzione: func ha ora due parametri. Ciò significa che non funziona con una quantità variabile di parametri. Peggio ancora, se si chiama func con un secondo parametro, questo sovrascriverà l'originale idalla definizione. :-(
Pascal

34

Ecco come lo fai usando la functoolslibreria (che non sono sicuro fosse disponibile al momento in cui è stata posta la domanda).

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)

Uscite 0 2 4, come previsto.


Volevo davvero usarlo, ma la mia funzione è in realtà un metodo di classe e il primo valore passato è self. C'è un modo per aggirare questo?
Michael David Watson,

1
Assolutamente. Supponiamo di avere una classe Math con un metodo add (self, a, b) e che si desidera impostare a = 1 per creare il metodo 'increment'. Quindi, crea un'istanza della tua classe 'my_math' e il tuo metodo di incremento sarà 'increment = partial (my_math.add, 1)'.
Luca Invernizzi,

2
Per applicare questa tecnica a un metodo puoi usare anche a functools.partialmethod()partire da Python 3.4
Matt Eding,

13

guarda questo:

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

Significa che puntano tutti alla stessa istanza della variabile i, che avrà un valore di 2 una volta terminato il ciclo.

Una soluzione leggibile:

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

1
La mia domanda è più "generale". Perché Python ha questo difetto? Mi aspetterei che un linguaggio che supporti le chiusure lessicali (come Perl e l'intera dinastia Lisp) funzioni correttamente.
Eli Bendersky,

2
Chiedere perché qualcosa ha un difetto è supporre che non sia un difetto.
Null303,

7

Quello che sta succedendo è che la variabile i viene catturata e le funzioni stanno restituendo il valore a cui è legata al momento in cui viene chiamata. Nei linguaggi funzionali questo tipo di situazione non si presenta mai, poiché non sarei rimbalzato. Tuttavia con Python, e anche come hai visto con Lisp, questo non è più vero.

La differenza con l'esempio di schema è relativo alla semantica del ciclo do. Lo schema sta effettivamente creando una nuova variabile i ogni volta attraverso il ciclo, piuttosto che riutilizzare un'associazione i esistente come con le altre lingue. Se usi una variabile diversa creata esterna al ciclo e la muti, vedrai lo stesso comportamento nello schema. Prova a sostituire il ciclo con:

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

Dai un'occhiata qui per ulteriori discussioni su questo.

[Modifica] Forse un modo migliore per descriverlo è pensare al ciclo do come a una macro che esegue i seguenti passi:

  1. Definire un lambda prendendo un singolo parametro (i), con un corpo definito dal corpo del loop,
  2. Una chiamata immediata di quella lambda con i valori appropriati di i come parametro.

vale a dire. l'equivalente al pitone seguente:

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

L'i non è più quello dell'ambito genitore, ma una variabile nuova di zecca nel suo ambito (cioè il parametro alla lambda) e quindi ottieni il comportamento che osservi. Python non ha questo nuovo ambito implicito, quindi il corpo del ciclo for condivide solo la variabile i.


Interessante. Non ero a conoscenza della differenza nella semantica del ciclo do. Grazie
Eli Bendersky il

4

Non sono ancora del tutto convinto del perché in alcune lingue funzioni in un modo e in un altro. In Common Lisp è come Python:

(defvar *flist* '())

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

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

Stampa "6 6 6" (nota che qui l'elenco è compreso tra 1 e 3, e costruito al contrario "). Mentre in Scheme funziona come in Perl:

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

Stampe "6 4 2"

E come ho già detto, Javascript è nel campo Python / CL. Sembra che qui ci sia una decisione di implementazione, che le diverse lingue affrontano in modi distinti. Mi piacerebbe capire qual è la decisione, esattamente.


8
La differenza sta nelle regole (do ...) piuttosto che nelle regole di scoping. Nello schema do crea una nuova variabile ogni passaggio attraverso il ciclo, mentre altre lingue riutilizzano l'associazione esistente. Vedi la mia risposta per maggiori dettagli e un esempio di una versione dello schema con un comportamento simile a lisp / python.
Brian,

2

Il problema è che tutte le funzioni locali si legano allo stesso ambiente e quindi alla stessa ivariabile. La soluzione (soluzione alternativa) consiste nel creare ambienti separati (stack frame) per ciascuna funzione (o lambda):

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

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

1

La variabile iè globale, il cui valore è 2 ogni volta che fviene chiamata la funzione .

Sarei propenso ad attuare il comportamento che stai cercando come segue:

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

Risposta al tuo aggiornamento : non è la globalità di i per sé a causare questo comportamento, è il fatto che è una variabile da un ambito che racchiude un valore fisso nel tempo in cui viene chiamato f. Nel tuo secondo esempio, il valore di iviene preso dall'ambito della kkkfunzione e nulla cambia quando si chiamano le funzioni flist.


0

Il ragionamento alla base del comportamento è già stato spiegato e sono state pubblicate più soluzioni, ma penso che questa sia la più pitonica (ricorda, tutto in Python è un oggetto!):

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)

La risposta di Claudiu è piuttosto buona, usando un generatore di funzioni, ma la risposta di piro è un hack, a dire il vero, perché mi sta trasformando in un argomento "nascosto" con un valore predefinito (funzionerà bene, ma non è "pythonic") .


Penso che dipenda dalla tua versione di Python. Ora ho più esperienza e non consiglierei più questo modo di farlo. Claudiu è il modo corretto di chiudere Python.
darkfeline

1
Questo non funzionerà su Python 2 o 3 (entrambi producono "4 4 4"). In funcin x * func.ifarà sempre riferimento all'ultima funzione definita. Quindi, anche se ogni funzione individualmente ha il numero corretto bloccato, finiscono comunque per leggere dall'ultima.
Lambda Fairy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.