Variáveis ​​locais em funções aninhadas

105

Ok, tenha paciência comigo, eu sei que vai parecer terrivelmente complicado, mas por favor me ajude a entender o que está acontecendo.

from functools import partial

class Cage(object):
    def __init__(self, animal):
        self.animal = animal

def gotimes(do_the_petting):
    do_the_petting()

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        cage = Cage(animal)

        def pet_function():
            print "Mary pets the " + cage.animal + "."

        yield (animal, partial(gotimes, pet_function))

funs = list(get_petters())

for name, f in funs:
    print name + ":", 
    f()

Dá:

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

Então, basicamente, por que não estou recebendo três animais diferentes? Não está cage'empacotado' no escopo local da função aninhada? Se não, como uma chamada à função aninhada pesquisa as variáveis ​​locais?

Eu sei que encontrar esse tipo de problema geralmente significa que alguém está 'fazendo tudo errado', mas gostaria de entender o que acontece.

noio
fonte
1
Tente for animal in ['cat', 'dog', 'cow']... Tenho certeza de que alguém vai aparecer e explicar isso - é um daqueles pegadinhas do Python :)
Jon Clements

Respostas:

114

A função aninhada procura variáveis ​​do escopo pai quando executada, não quando definida.

O corpo da função é compilado e as variáveis ​​'livres' (não definidas na própria função por atribuição), são verificadas e, em seguida, vinculadas como células de fechamento à função, com o código usando um índice para fazer referência a cada célula. pet_functionportanto, tem uma variável livre ( cage) que é então referenciada por meio de uma célula de fechamento, índice 0. O próprio fechamento aponta para a variável local cagena get_pettersfunção.

Quando você realmente chama a função, esse encerramento é usado para observar o valor de cageno escopo circundante no momento em que você chama a função . Aqui está o problema. No momento em que você chama suas funções, a get_pettersfunção já está computando seus resultados. A cagevariável local em algum momento durante essa execução foi atribuído cada um dos 'cow', 'dog'e 'cat'cordas, mas no final da função, cagecontém o último valor 'cat'. Assim, quando você chama cada uma das funções retornadas dinamicamente, você obtém o valor'cat' impresso.

A solução é não depender de fechamentos. Você pode usar uma função parcial em vez disso, criar um novo escopo de função ou vincular a variável como um valor padrão para um parâmetro de palavra-chave .

  • Exemplo de função parcial, usando functools.partial():

    from functools import partial
    
    def pet_function(cage=None):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, partial(pet_function, cage=cage)))
  • Criando um novo exemplo de escopo:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
  • Vinculando a variável como um valor padrão para um parâmetro de palavra-chave:

    def pet_function(cage=cage):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, pet_function))

Não há necessidade de definir a scoped_cagefunção no loop, a compilação ocorre apenas uma vez, não em cada iteração do loop.

Martijn Pieters
fonte
1
Eu bati minha cabeça nesta parede por 3 horas hoje em um roteiro de trabalho. Seu último ponto é muito importante e é o principal motivo pelo qual encontrei esse problema. Tenho retornos de chamada com muitos encerramentos em todo o meu código, mas tentar a mesma técnica em um loop foi o que me pegou.
DrEsperanto,
12

Meu entendimento é que cage é procurado no namespace da função pai quando a pet_function produzida é realmente chamada, não antes.

Então, quando você faz

funs = list(get_petters())

Você gera 3 funções que encontrarão a gaiola criada por último.

Se você substituir seu último loop por:

for name, f in get_petters():
    print name + ":", 
    f()

Você realmente obterá:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.
Nicolas Barbey
fonte
6

Isso decorre do seguinte

for i in range(2): 
    pass

print(i)  # prints 1

depois de iterar o valor de i é armazenado lentamente como seu valor final.

Como um gerador, a função funcionaria (ou seja, imprimir cada valor por vez), mas ao transformar para uma lista, ela é executada sobre o gerador , portanto, todas as chamadas para cage( cage.animal) retornam gatos.

Andy Hayden
fonte
0

Vamos simplificar a pergunta. Definir:

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        def pet_function():
            return "Mary pets the " + animal + "."

        yield (animal, pet_function)

Então, assim como na pergunta, temos:

>>> for name, f in list(get_petters()):
...     print(name + ":", f())

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

Mas se evitarmos criar um list()primeiro:

>>> for name, f in get_petters():
...     print(name + ":", f())

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

O que está acontecendo? Por que essa diferença sutil muda completamente nossos resultados?


Se olharmos list(get_petters()), fica claro pela mudança de endereços de memória que realmente produzimos três funções diferentes:

>>> list(get_petters())

[('cow', <function get_petters.<locals>.pet_function at 0x7ff2b988d790>),
 ('dog', <function get_petters.<locals>.pet_function at 0x7ff2c18f51f0>),
 ('cat', <function get_petters.<locals>.pet_function at 0x7ff2c14a9f70>)]

No entanto, dê uma olhada nos cells aos quais essas funções estão vinculadas:

>>> for _, f in list(get_petters()):
...     print(f(), f.__closure__)

Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)

>>> for _, f in get_petters():
...     print(f(), f.__closure__)

Mary pets the cow. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a95670>,)
Mary pets the dog. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a952f0>,)
Mary pets the cat. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c3f437f0>,)

Para ambos os loops, o cellobjeto permanece o mesmo ao longo das iterações. No entanto, como esperado, a strreferência específica varia no segundo loop. O cellobjeto se refere a animal, que é criado quando get_petters()é chamado. No entanto, animalmuda a qual strobjeto ele se refere quando a função do gerador é executada .

No primeiro loop, durante cada iteração, criamos todos os fs, mas só os chamamos depois que o gerador get_petters()está completamente esgotado e umlist de funções já tiver sido criado.

No segundo loop, durante cada iteração, estamos pausando o get_petters()gerador e chamando fapós cada pausa. Assim, acabamos recuperando o valor deanimal naquele momento no tempo em que a função do gerador está pausada.

Como @Claudiu responde a uma pergunta semelhante :

Três funções separadas são criadas, mas cada uma tem o fechamento do ambiente em que estão definidas - neste caso, o ambiente global (ou o ambiente da função externa se o loop for colocado dentro de outra função). Este é exatamente o problema, entretanto - neste ambiente, animalé mutado, e todos os encerramentos referem-se ao mesmo animal.

[Nota do editor: ifoi alterado para animal.]

Mateen Ulhaq
fonte