Pergunta

Enquanto eu estava investigando um problema que tive com fechos lexicais em código Javascript, eu vim ao longo deste problema em Python:

flist = []

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

for f in flist:
    print f(2)

Note que este exemplo conscientemente evita lambda. Ela imprime "4 4 4", que é surpreendente. Eu esperaria "0 2 4".

Este código Perl equivalente faz isso mesmo:

my @flist = ();

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

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

"0 2 4" é impresso.

Pode me explicar a diferença?


Update:

O problema não com i ser global. Isso exibe o mesmo 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)

Como mostra a linha comentada, i é desconhecida nesse ponto. Ainda assim, ele imprime "4 4 4".

Foi útil?

Solução

Python é realmente comportando como definido. Três funções separadas são criados, mas cada um tem o encerramento do ambiente em que está definida em - neste caso, o ambiente da função externa do ambiente global (ou se o laço é colocado dentro de uma outra função). Este é exatamente o problema, embora -. Neste ambiente, i é mutante , e os fechos tudo se referem ao mesmo i

Aqui é a melhor solução que pode chegar a - criar um creater função e invocar que em seu lugar. Isto irá forçar ambientes diferentes para cada uma das funções criadas, com a diferente i em cada um.

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)

Este é o que acontece quando você mistura efeitos colaterais e programação funcional.

Outras dicas

As funções definidas no sustento de loop acessando o mesmo i variável, enquanto o seu valor muda. No final do ciclo, todas as funções apontar para a mesma variável, que está segurando o último valor no loop:. O efeito é o que relatou no exemplo

Para avaliar i e usar o seu valor, um padrão comum é configurá-lo como um padrão parâmetro:. Defaults parâmetros são avaliados quando a instrução def é executado, e, portanto, o valor da variável do laço é congelado

As seguintes obras como esperado:

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)

Aqui está como fazê-lo utilizando a biblioteca functools (que eu não tenho certeza estava disponível no momento da pergunta foi feita).

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)

Saídas 0 2 4, como esperado.

olhada nisso:

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

Isso significa que eles apontam para o mesmo exemplo i variável, que terá um valor de 2, uma vez o laço é longo.

Uma solução legível:

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

O que está acontecendo é que a variável i é capturado, e as funções estão retornando o valor que ele é obrigado a no momento em que for chamado. Em linguagens funcionais neste tipo de situação nunca surge, como eu não seria rebote. No entanto, com python, e também como você viu com lisp, isso não é mais verdade.

A diferença com o seu exemplo esquema tem a ver com a semântica do laço do. Esquema é eficaz criar uma nova variável i cada vez através do laço, em vez de reutilização de um existente i de ligação como com os outros idiomas. Se você usar uma variável diferente criado externa ao loop e transformar-lo, você verá o mesmo comportamento no esquema. Tente substituir o seu laço com:

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

Dê uma olhada aqui para alguns mais discussão sobre isso.

[Edit] Possivelmente a melhor maneira de descrevê-lo é pensar do fazer laço como uma macro que executa as seguintes etapas:

  1. Definir um lambda tendo um único parâmetro (i), com um corpo definido pelo corpo do laço,
  2. Uma chamada imediata de que lambda com valores apropriados de i como seu parâmetro.

ie. o equivalente ao abaixo python:

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

O i não é mais a do escopo pai, mas uma nova variável marca em seu próprio escopo (ie. O parâmetro para o lambda) e de modo a obter o comportamento que você observar. Python não tem esse novo escopo implícito, para que o corpo do loop for apenas compartilha a variável i.

Estou ainda não inteiramente convencido por que em algumas línguas isso funciona de uma maneira, e de alguma outra maneira. Em Lisp comum é como Python:

(defvar *flist* '())

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

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

Prints "6 6 6" (note que aqui a lista é de 1 a 3, e construído em reverso "). Enquanto no esquema funciona como em 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)

Prints "6 4 2"

E, como eu já mencionei, Javascript está no campo Python / CL. Parece que há uma decisão de implementação aqui, que línguas diferentes se aproximam de formas distintas. Gostaria muito de entender o que é a decisão, exatamente.

O problema é que todas as funções locais ligam-se ao mesmo ambiente e, portanto, para a mesma variável i. A solução (solução) é criar ambientes separados (quadros de pilha) para cada função (ou lambda):

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

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

O i variável é um global, cujo valor é de 2 em cada tempo o f função é chamada.

Eu estaria inclinado a implementar o comportamento que você está depois da seguinte forma:

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

Resposta a sua atualização : Não é a globalidade da i per se que está causando esse comportamento, é o fato de que é uma variável a partir de um escopo envolvente, que tem uma valor fixo ao longo dos tempos em que f é chamado. Em seu segundo exemplo, o valor de i é retirado do escopo da função kkk, e nada está mudando que quando você chamar as funções em flist.

O raciocínio por trás do comportamento já foi explicado, e várias soluções têm sido publicadas, mas eu acho que este é o mais pythônico (lembre-se, tudo em Python é um objeto!):

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)

A resposta de Claudiu é bom bonita, usando um gerador de função, mas a resposta de piro é um hack, para ser honesto, como ele está fazendo i em um argumento "escondido" com um valor padrão (que vai funcionar bem, mas não é " pythônico ").

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top