Domanda

Mentre stavo indagando su 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)

Nota che questo esempio evita consapevolmente lambda . Stampa "4 4 4", il che è sorprendente. Mi aspetterei "0 2 4".

Questo codice Perl equivalente fa bene:

my @flist = ();

foreach my $i (0 .. 2)
{
    push(@flist, sub {$i * 

Mentre stavo indagando su 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)

Nota che questo esempio evita consapevolmente lambda . Stampa "4 4 4", il che è sorprendente. Mi aspetterei "0 2 4".

Questo codice Perl equivalente fa bene:

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)

" 0 2 4 " è stampato.

Puoi per favore spiegare la differenza?


Aggiornamento:

Il problema non è con i essendo globale. Questo mostra lo stesso comportamento:

<*>

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

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

" 0 2 4 " è stampato.

Puoi per favore spiegare la differenza?


Aggiornamento:

Il problema non è con i essendo globale. Questo mostra lo stesso comportamento:

<*>

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

È stato utile?

Soluzione

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

Ecco la migliore soluzione che posso trovare: crea un creatore di funzioni e invoca invece che . Questo forzerà ambienti diversi per ciascuna delle funzioni create, con una diversa i 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.

Altri suggerimenti

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

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

Il seguente 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)

Ecco come lo fai usando la libreria functools (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)

Output 0 2 4, come previsto.

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

Quello che sta accadendo è 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 nell'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 in merito.

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

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

es. l'equivalente del pitone sottostante:

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 (es. il parametro del 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.

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 " (tieni presente che qui l'elenco è compreso tra 1 e 3 e incorporato 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)

Stampa " 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 esattamente quale sia la decisione.

Il problema è che tutte le funzioni locali si legano allo stesso ambiente e quindi alla stessa variabile i . 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

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

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 di per sé che sta causando 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 i è preso dall'ambito della funzione kkk e nulla cambia quando si chiamano le funzioni su flist .

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 "nascosto". argomento con un valore predefinito (funzionerà bene, ma non è " pythonic ").

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top