O que os fechamentos de função (lambda) capturam?

249

Recentemente, comecei a brincar com o Python e descobri algo peculiar na maneira como os fechamentos funcionam. Considere o seguinte código:

adders=[0,1,2,3]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

Ele cria uma matriz simples de funções que recebem uma única entrada e retornam essa entrada adicionada por um número. As funções são construídas em forloop onde o iterador ié executado de 0para 3. Para cada um desses números lambda, é criada uma função que captura ie a adiciona à entrada da função. A última linha chama a segunda lambdafunção com 3como parâmetro. Para minha surpresa, a saída foi 6.

Eu esperava um 4. Meu raciocínio era: no Python tudo é um objeto e, portanto, toda variável é um ponteiro essencial para ele. Ao criar os lambdafechamentos para i, eu esperava que ele armazenasse um ponteiro para o objeto inteiro atualmente apontado por i. Isso significa que, quando iatribuído um novo objeto inteiro, ele não deve afetar os fechamentos criados anteriormente. Infelizmente, inspecionar a addersmatriz em um depurador mostra que sim. Todas as lambdafunções referem-se ao último valor i, 3que resulta em adders[1](3)retorno 6.

O que me faz pensar sobre o seguinte:

  • O que os fechamentos capturam exatamente?
  • Qual é a maneira mais elegante de convencer as lambdafunções a capturar o valor atual de iuma maneira que não seja afetada quando io valor for alterado?
Boaz
fonte
35
Eu tive esse problema no código da interface do usuário. Me deixou louco. O truque é lembrar que os loops não criam um novo escopo.
detly
3
@TimMB Como isai do espaço para nome?
detly
3
@ Detly Bem, eu ia dizer que print inão funcionaria após o loop. Mas eu testei por mim mesmo e agora entendo o que você quer dizer - funciona. Eu não tinha ideia de que variáveis ​​de loop permaneciam após o corpo do loop em python.
Tim MB
1
@ TimMB - Sim, foi o que eu quis dizer. Mesmo para if, with, tryetc.
detly
13
Isso está no FAQ oficial do Python, em Por que os lambdas definidos em um loop com valores diferentes retornam o mesmo resultado? , com uma explicação e a solução alternativa usual.
Abarnert

Respostas:

161

Sua segunda pergunta foi respondida, mas quanto à sua primeira:

o que o fechamento captura exatamente?

O escopo em Python é dinâmico e lexical. Um fechamento sempre lembrará o nome e o escopo da variável, não o objeto para o qual está apontando. Como todas as funções no seu exemplo são criadas no mesmo escopo e usam o mesmo nome de variável, elas sempre se referem à mesma variável.

EDIT: Em relação à sua outra questão de como superar isso, há duas maneiras que vêm à mente:

  1. A maneira mais concisa, mas não estritamente equivalente, é a recomendada por Adrien Plisson . Crie uma lambda com um argumento extra e defina o valor padrão do argumento extra para o objeto que você deseja preservar.

  2. Um pouco mais detalhado, mas menos hacky, seria criar um novo escopo cada vez que você criar o lambda:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5

    O escopo aqui é criado usando uma nova função (um lambda, por questões de concisão), que vincula seu argumento e transmite o valor que você deseja vincular como argumento. No código real, porém, você provavelmente terá uma função comum em vez do lambda para criar o novo escopo:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]
Max Shawabkeh
fonte
1
Max, se você adicionar uma resposta para minha outra (pergunta mais simples), eu posso marcar isso como uma resposta aceita. THX!
Boaz
3
O Python tem escopo estático, não dinâmico. São apenas todas as variáveis ​​que são referências; portanto, quando você define uma variável para um novo objeto, a própria variável (a referência) tem o mesmo local, mas aponta para outra coisa. a mesma coisa acontece no esquema se você set!. veja aqui o que realmente é o escopo dinâmico: voidspace.org.uk/python/articles/code_blocks.shtml .
Claudiu
6
A opção 2 se parece com o que os idiomas funcionais chamariam de "função ao curry".
Crashworks
205

você pode forçar a captura de uma variável usando um argumento com um valor padrão:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

a ideia é declarar um parâmetro (com nome inteligente i) e fornecer um valor padrão da variável que você deseja capturar (o valor de i)

Adrien Plisson
fonte
7
+1 para usar valores padrão. Ser avaliado quando o lambda é definido os torna perfeitos para esse uso.
quornian
21
+1 também porque esta é a solução recomendada pelo FAQ oficial .
Abarnert 6/11/14
23
Isso é incrível. O comportamento padrão do Python, no entanto, não é.
Cecil Curry
1
Porém, isso simplesmente não parece uma boa solução ... você está realmente alterando a assinatura da função apenas para capturar uma cópia da variável. E também aqueles que invocam a função podem mexer com a variável i, certo?
David Callanan
@DavidCallanan, estamos falando de um lambda: um tipo de função ad-hoc que você normalmente define em seu próprio código para fechar um buraco, não algo que você compartilha através de um sdk inteiro. se você precisar de uma assinatura mais forte, use uma função real.
Adrien Plisson
33

Para completar, outra resposta para sua segunda pergunta: você pode usar parcial no módulo functools .

Com a importação de add from operator como Chris Lutz propôs, o exemplo se torna:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)
Joma
fonte
24

Considere o seguinte código:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

Eu acho que a maioria das pessoas não achará isso confuso. É o comportamento esperado.

Então, por que as pessoas pensam que seria diferente quando isso é feito em um loop? Eu sei que cometi esse erro, mas não sei por quê. É o laço? Ou talvez o lambda?

Afinal, o loop é apenas uma versão mais curta de:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a
truppo
fonte
11
É o loop, porque em muitos outros idiomas um loop pode criar um novo escopo.
detly
1
Essa resposta é boa porque explica por que a mesma ivariável está sendo acessada para cada função lambda.
David Callanan
3

Em resposta à sua segunda pergunta, a maneira mais elegante de fazer isso seria usar uma função que usa dois parâmetros em vez de uma matriz:

add = lambda a, b: a + b
add(1, 3)

No entanto, usar o lambda aqui é um pouco bobo. O Python nos fornece o operatormódulo, que fornece uma interface funcional para os operadores básicos. O lambda acima tem sobrecarga desnecessária apenas para chamar o operador de adição:

from operator import add
add(1, 3)

Entendo que você está brincando, tentando explorar a linguagem, mas não consigo imaginar uma situação em que usaria uma variedade de funções em que a estranheza do escopo do Python atrapalhasse.

Se você quiser, escreva uma classe pequena que use sua sintaxe de indexação de matriz:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)
Chris Lutz
fonte
2
Chris, é claro que o código acima não tem nada a ver com o meu problema original. Foi construído para ilustrar meu argumento de uma maneira simples. É claro que é inútil e bobo.
Boaz
3

Aqui está um novo exemplo que destaca a estrutura de dados e o conteúdo de um fechamento, para ajudar a esclarecer quando o contexto em anexo é "salvo".

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

O que há em um fechamento?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

Notavelmente, my_str não está no fechamento de f1.

O que há no fechamento da f2?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

Observe (nos endereços de memória) que os dois fechamentos contêm os mesmos objetos. Portanto, você pode começar a pensar na função lambda como tendo uma referência ao escopo. No entanto, my_str não está no fechamento de f_1 ou f_2 e i não está no fechamento de f_3 (não mostrado), o que sugere que os próprios objetos de fechamento são objetos distintos.

Os objetos de fechamento são eles mesmos o mesmo objeto?

>>> print f_1.func_closure is f_2.func_closure
False
Jeff
fonte
Nota: A saída int object at [address X]>me fez pensar que o fechamento está armazenando [endereço X] AKA uma referência. No entanto, [endereço X] será alterado se a variável for reatribuída após a instrução lambda.
Jeff