Como funcionam os fechamentos lexicais?

149

Enquanto eu estava investigando um problema que tive com fechamentos lexicais no código Javascript, me deparei com esse problema no Python:

flist = []

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

for f in flist:
    print f(2)

Observe que este exemplo evita conscientemente lambda. Imprime "4 4 4", o que é surpreendente. Eu esperaria "0 2 4".

Esse código Perl equivalente faz o certo:

my @flist = ();

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

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

"0 2 4" é impresso.

Você pode explicar a diferença?


Atualizar:

O problema não é com iestar 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é desconhecido naquele momento. Ainda assim, imprime "4 4 4".

Eli Bendersky
fonte
3
Aqui está um artigo muito bom sobre esse assunto. me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

Respostas:

151

Python está realmente se comportando conforme definido. Três funções separadas são criadas, mas cada uma possui o fechamento do ambiente em que está definida - nesse caso, o ambiente global (ou o ambiente da função externa, se o loop for colocado dentro de outra função). Porém, este é exatamente o problema - nesse ambiente, i é modificado e todos os fechamentos se referem ao mesmo i .

Aqui está a melhor solução que eu posso encontrar - crie um criador de funções e invoque- o . Isso forçará ambientes diferentes para cada uma das funções criadas, com um i diferente em cada uma.

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)

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

Claudiu
fonte
5
Sua solução também é a usada em Javascript.
Eli Bendersky
9
Isso não é mau comportamento. Está se comportando exatamente como definido.
Alex Coventry
6
O piro da IMO tem uma solução melhor stackoverflow.com/questions/233673/…
jfs 25/10/2008
2
Talvez eu mudasse o "i" mais interno para "j" para maior clareza.
Ovosyntax
7
que tal apenas defini-lo assim:def inner(x, i=i): return x * i
traçado
152

As funções definidas no loop continuam acessando a mesma variável ienquanto seu valor é alterado. No final do loop, todas as funções apontam para a mesma variável, que mantém o último valor no loop: o efeito é o relatado no exemplo.

Para avaliar ie usar seu valor, um padrão comum é defini-lo como padrão: parâmetros padrão são avaliados quando a definstrução é executada e, portanto, o valor da variável de loop é congelado.

O seguinte funciona 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)
piro
fonte
7
s / em tempo de compilação / no momento em que a definstrução é executada /
jfs 25/10/08
23
Esta é uma solução engenhosa, o que a torna horrível.
Stavros Korokithakis
Há um problema com esta solução: func agora possui dois parâmetros. Isso significa que não funciona com uma quantidade variável de parâmetros. Pior, se você chamar func com um segundo parâmetro, isso substituirá o original ida definição. :-(
Pascal
34

Veja como você faz isso usando a functoolsbiblioteca (que não tenho certeza de que estava disponível no momento em que a 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, conforme o esperado.

Luca Invernizzi
fonte
Eu realmente queria usar isso, mas minha função é realmente um método de classe e o primeiro valor passado é o próprio. Existe alguma maneira de contornar isso?
Michael David Watson
1
Absolutamente. Suponha que você tenha uma classe Math com um método add (self, a, b) e deseje definir a = 1 para criar o método 'increment'. Em seguida, crie uma instância da sua classe 'my_math' e seu método de incremento será 'increment = parcial (my_math.add, 1)'.
Luca Invernizzi
2
Para aplicar esta técnica para um método você também pode usar functools.partialmethod()a partir de python 3.4
Matt Eding
13

Veja isso:

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 todos apontam para a mesma instância variável i, que terá o valor 2 quando o loop terminar.

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))
Nulo303
fonte
1
Minha pergunta é mais "geral". Por que Python tem essa falha? Eu esperaria que uma linguagem que suporta fechamentos lexicais (como Perl e toda a dinastia Lisp) funcione corretamente.
Eli Bendersky
2
Perguntar por que algo tem uma falha está assumindo que não é uma falha.
Null303
7

O que está acontecendo é que a variável i é capturada e as funções estão retornando o valor ao qual está vinculada no momento em que é chamada. Nas linguagens funcionais, esse tipo de situação nunca surge, pois eu não me recuperaria. No entanto, com python, e também como você viu com lisp, isso não é mais verdade.

A diferença no exemplo do seu esquema é a semântica do loop do. O esquema está efetivamente criando uma nova variável i a cada vez no loop, em vez de reutilizar uma ligação i existente, como nos outros idiomas. Se você usar uma variável diferente criada externa ao loop e a modificar, verá o mesmo comportamento no esquema. Tente substituir seu loop por:

(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 uma discussão mais aprofundada sobre isso.

[Editar] Possivelmente, uma maneira melhor de descrevê-lo é pensar no loop do como uma macro que executa as seguintes etapas:

  1. Defina um lambda usando um único parâmetro (i), com um corpo definido pelo corpo do loop,
  2. Uma chamada imediata desse lambda com valores apropriados de i como parâmetro.

ie o equivalente ao python abaixo:

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 aquele do escopo pai, mas uma variável nova em seu próprio escopo (ou seja, o parâmetro para o lambda) e, assim, você obtém o comportamento observado. O Python não possui esse novo escopo implícito; portanto, o corpo do loop for apenas compartilha a variável i.

Brian
fonte
Interessante. Eu não estava ciente da diferença na semântica do loop do. Obrigado
Eli Bendersky
4

Ainda não estou totalmente convencido de que, em alguns idiomas, isso funciona de uma maneira e de outra maneira. No Common Lisp é 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)))

Imprime "6 6 6" (observe que aqui a lista é de 1 a 3 e é construída ao contrário "). Enquanto estiver no Scheme, ele funciona como no 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)

Imprime "6 4 2"

E como já mencionei, o Javascript está no campo Python / CL. Parece que há uma decisão de implementação aqui, que diferentes idiomas abordam de maneiras distintas. Eu adoraria entender qual é a decisão, exatamente.

Eli Bendersky
fonte
8
A diferença está nas regras (do ...) e não no escopo. No esquema, cria uma nova variável a cada passagem pelo loop, enquanto outros idiomas reutilizam a ligação existente. Veja minha resposta para mais detalhes e um exemplo de uma versão de esquema com comportamento semelhante ao lisp / python.
25708 Brian
2

O problema é que todas as funções locais se ligam ao mesmo ambiente e, portanto, à mesma ivariável. A solução (solução alternativa) é 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
Rafał Dowgird
fonte
1

A variável ié uma global, cujo valor é 2 a cada vez que a função fé chamada.

Eu estaria inclinado a implementar o comportamento que você procura da seguinte maneira:

>>> 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 à sua atualização : Não é a globalidade i propriamente dita que está causando esse comportamento, é o fato de ser uma variável de um escopo que possui um valor fixo nos momentos em que f é chamado. No seu segundo exemplo, o valor de ié obtido do escopo da kkkfunção e nada está mudando quando você chama as funções flist.

Alex Coventry
fonte
0

O raciocínio por trás do comportamento já foi explicado e várias soluções foram publicadas, mas acho que essa é a mais pitônica (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 é muito boa, usando um gerador de funções, mas a resposta de piro é um hack, para ser honesto, pois está transformando-o em um argumento "oculto" com um valor padrão (funcionará bem, mas não é "pitônico") .

darkfeline
fonte
Eu acho que depende da sua versão python. Agora tenho mais experiência e não sugeriria mais essa maneira de fazê-lo. O Claudiu's é a maneira correta de fazer um fechamento em Python.
darkfeline
1
Isso não funcionará no Python 2 ou 3 (ambos emitem "4 4 4"). O funcin x * func.isempre se refere à última função definida. Portanto, mesmo que cada função individualmente tenha o número correto preso, todas elas acabam lendo a última.
Lambda Fairy