Como fazer fechamentos lexicais trabalho?
-
04-07-2019 - |
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".
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:
- Definir um lambda tendo um único parâmetro (i), com um corpo definido pelo corpo do laço,
- 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 ").