Por que as funções aninhadas do python não são chamadas de fechamento?


249

Eu já vi e usei funções aninhadas no Python, e elas correspondem à definição de encerramento. Então, por que eles são chamados em nested functionsvez de closures?

As funções aninhadas não são encerramentos porque não são usadas pelo mundo externo?

ATUALIZAÇÃO: Eu estava lendo sobre fechamentos e isso me fez pensar sobre esse conceito em relação ao Python. Pesquisei e encontrei o artigo mencionado por alguém em um comentário abaixo, mas não consegui entender completamente a explicação desse artigo; é por isso que estou fazendo essa pergunta.


8
Curiosamente, alguns pesquisadores do Google me acharam isso, datado de dezembro de 2006: effbot.org/zone/closure.htm . Não tenho certeza - "duplicatas externas" são desaprovadas pelo SO?
HBW

1
PEP 227 - Escopos estaticamente aninhados para obter mais informações.
Honest Abe

Respostas:


394

Um fechamento ocorre quando uma função tem acesso a uma variável local a partir de um escopo que encerrou sua execução.

def make_printer(msg):
    def printer():
        print msg
    return printer

printer = make_printer('Foo!')
printer()

Quando make_printeré chamado, um novo quadro é colocado na pilha com o código compilado para a printerfunção como constante e o valor msgcomo local. Em seguida, ele cria e retorna a função. Como a função faz printerreferência à msgvariável, ela é mantida ativa após o make_printerretorno da função.

Portanto, se suas funções aninhadas não

  1. acessar variáveis ​​locais para os escopos anexos,
  2. faça-o quando for executado fora desse escopo,

então eles não são fechamentos.

Aqui está um exemplo de uma função aninhada que não é um fechamento.

def make_printer(msg):
    def printer(msg=msg):
        print msg
    return printer

printer = make_printer("Foo!")
printer()  #Output: Foo!

Aqui, estamos vinculando o valor ao valor padrão de um parâmetro. Isso ocorre quando a função printeré criada e, portanto, nenhuma referência ao valor de msgexterno a printer precisa ser mantida após make_printerretornos. msgé apenas uma variável local normal da função printerneste contexto.


2
Você responde que é muito melhor que o meu, você faz um bom argumento, mas se vamos seguir as mais estritas definições de programação funcional, seus exemplos são mesmo funcionais? Já faz um tempo, e não me lembro se a programação funcional estrita permite funções que não retornam valores. O ponto é discutível, se você considerar o valor de retorno como Nenhum, mas esse é outro tópico.
mikerobi

6
@mikerobi, não tenho certeza de que precisamos levar em consideração a programação funcional, já que python não é realmente uma linguagem funcional, embora certamente possa ser usada como tal. Mas, não, as funções internas não são funções nesse sentido, pois seu objetivo é criar efeitos colaterais. É fácil criar uma função que ilustra os pontos da mesma forma:
aaronasterling

31
@mikerobi: se um blob de código é ou não um fechamento, depende se ele fecha ou não sobre o ambiente, e não o que você chama. Pode ser uma rotina, função, procedimento, método, bloco, sub-rotina, qualquer que seja. No Ruby, métodos não podem ser fechamentos, apenas blocos. Em Java, métodos não podem ser fechamentos, mas classes podem. Isso não os torna menos fechados. (Embora o fato de que eles fechem apenas algumas variáveis ​​e não possam modificá-las, as torna quase inúteis.) Você pode argumentar que um método é apenas um procedimento encerrado self. (Em JavaScript / Python, isso é quase verdade.) #
314 Jörg W Mittag

3
@ JörgWMittag Por favor, defina "fecha".
Evgeni Sergeev

4
@EvgeniSergeev "fecha sobre" ou seja, refere-se "a uma variável local [digamos i] a partir de um escopo". refere-se, isto é, pode inspecionar (ou alterar) io valor de, mesmo se / quando esse escopo "tiver terminado sua execução", isto é, a execução de um programa foi transferida para outras partes do código. O bloco onde iestá definido não existe mais, mas as funções referentes a iainda podem fazê-lo. Isso é comumente descrito como "fechando sobre a variável i". Para não lidar com as variáveis ​​específicas, ele pode ser implementado como fechamento por todo o quadro de ambiente em que a variável está definida.
Will Ness

103

A pergunta já foi respondida por aaronasterling

No entanto, alguém pode estar interessado em como as variáveis ​​são armazenadas sob o capô.

Antes de vir para o trecho:

Encerramentos são funções que herdam variáveis ​​de seu ambiente anexo. Quando você passa um retorno de chamada de função como argumento para outra função que executará E / S, essa função de retorno de chamada será chamada posteriormente, e essa função - quase magicamente - lembrará o contexto em que foi declarado, juntamente com todas as variáveis ​​disponíveis. nesse contexto.

  • Se uma função não usa variáveis ​​livres, ela não forma um fechamento.

  • Se houver outro nível interno que utilize variáveis ​​livres - todos os níveis anteriores salvam o ambiente lexical (exemplo no final)

  • atributos de função func_closureem python <3.X ou __closure__em python> 3.X salvam as variáveis ​​livres.

  • Toda função no python possui esses atributos de fechamento, mas não salva nenhum conteúdo se não houver variáveis ​​livres.

exemplo: de atributos de fechamento, mas nenhum conteúdo interno, pois não há variável livre.

>>> def foo():
...     def fii():
...         pass
...     return fii
...
>>> f = foo()
>>> f.func_closure
>>> 'func_closure' in dir(f)
True
>>>

NB: VARIÁVEL GRÁTIS É DEVE CRIAR UM ENCERRAMENTO.

Vou explicar usando o mesmo trecho como acima:

>>> def make_printer(msg):
...     def printer():
...         print msg
...     return printer
...
>>> printer = make_printer('Foo!')
>>> printer()  #Output: Foo!

E todas as funções Python têm um atributo de fechamento, então vamos examinar as variáveis ​​anexas associadas a uma função de fechamento.

Aqui está o atributo func_closurepara a funçãoprinter

>>> 'func_closure' in dir(printer)
True
>>> printer.func_closure
(<cell at 0x108154c90: str object at 0x108151de0>,)
>>>

o closure atributo retorna uma tupla de objetos de célula que contêm detalhes das variáveis ​​definidas no escopo em anexo.

O primeiro elemento no func_closure que poderia ser None ou uma tupla de células que contêm ligações para as variáveis ​​livres da função e é somente leitura.

>>> dir(printer.func_closure[0])
['__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__getattribute__',
 '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
 '__setattr__',  '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
>>>

Aqui na saída acima, você pode ver cell_contents, vamos ver o que ele armazena:

>>> printer.func_closure[0].cell_contents
'Foo!'    
>>> type(printer.func_closure[0].cell_contents)
<type 'str'>
>>>

Então, quando chamamos a função printer(), ela acessa o valor armazenado dentro do cell_contents. Foi assim que obtivemos o resultado como 'Foo!'

Mais uma vez, explicarei o uso do snippet acima com algumas alterações:

 >>> def make_printer(msg):
 ...     def printer():
 ...         pass
 ...     return printer
 ...
 >>> printer = make_printer('Foo!')
 >>> printer.func_closure
 >>>

No snippet acima, não imprimo msg dentro da função da impressora, para que não crie nenhuma variável livre. Como não há variável livre, não haverá conteúdo dentro do fechamento. Isso é exatamente o que vemos acima.

Agora vou explicar outro trecho diferente para limpar tudo Free Variablecom Closure:

>>> def outer(x):
...     def intermediate(y):
...         free = 'free'
...         def inner(z):
...             return '%s %s %s %s' %  (x, y, free, z)
...         return inner
...     return intermediate
...
>>> outer('I')('am')('variable')
'I am free variable'
>>>
>>> inter = outer('I')
>>> inter.func_closure
(<cell at 0x10c989130: str object at 0x10c831b98>,)
>>> inter.func_closure[0].cell_contents
'I'
>>> inn = inter('am')

Portanto, vemos que uma func_closurepropriedade é uma tupla de células de fechamento , podemos referenciá-las e seu conteúdo explicitamente - uma célula tem a propriedade "cell_contents"

>>> inn.func_closure
(<cell at 0x10c9807c0: str object at 0x10c9b0990>, 
 <cell at 0x10c980f68: str object at   0x10c9eaf30>, 
 <cell at 0x10c989130: str object at 0x10c831b98>)
>>> for i in inn.func_closure:
...     print i.cell_contents
...
free
am 
I
>>>

Aqui quando chamamos inn, ele se refere a todas as variáveis ​​livres de salvamento, paraI am free variable

>>> inn('variable')
'I am free variable'
>>>

9
No Python 3, func_closureagora é chamado __closure__, de maneira semelhante aos vários outros func_*atributos.
Lvc

3
Também __closure_está disponível em Python 2.6+ para compatibilidade com Python 3.
Pierre

Encerramento refere-se ao registro que armazena as variáveis ​​fechadas, anexadas ao objeto de função. Não é a função em si. No Python, é o __closure__objeto que é o fechamento.
Martijn Pieters

Obrigado @MartijnPieters pelo seu esclarecimento.
James Sapam

71

O Python tem um suporte fraco para o fechamento. Para entender o que quero dizer, use o seguinte exemplo de contador usando fechamento com JavaScript:

function initCounter(){
    var x = 0;
    function counter  () {
        x += 1;
        console.log(x);
    };
    return counter;
}

count = initCounter();

count(); //Prints 1
count(); //Prints 2
count(); //Prints 3

O fechamento é bastante elegante, pois fornece às funções escritas assim a capacidade de ter "memória interna". A partir do Python 2.7, isso não é possível. Se você tentar

def initCounter():
    x = 0;
    def counter ():
        x += 1 ##Error, x not defined
        print x
    return counter

count = initCounter();

count(); ##Error
count();
count();

Você receberá um erro dizendo que x não está definido. Mas como pode ser isso, se tiver sido demonstrado por outras pessoas que você pode imprimi-lo? Isso ocorre devido à maneira como o Python gerencia o escopo da variável de funções. Embora a função interna possa ler as variáveis ​​da função externa, ela não pode escrevê- las.

É realmente uma pena. Mas com apenas o fechamento somente leitura, você pode pelo menos implementar o padrão do decorador de funções para o qual o Python oferece açúcar sintático.

Atualizar

Como foi apontado, existem maneiras de lidar com as limitações de escopo do python e vou expor algumas.

1. Use oglobal palavra chave (em geral não recomendada).

2. No Python 3.x, use a nonlocalpalavra - chave (sugerida por @unutbu e @leewz)

3. Defina uma classe modificável simplesObject

class Object(object):
    pass

e crie um Object scopeinterior initCounterpara armazenar as variáveis

def initCounter ():
    scope = Object()
    scope.x = 0
    def counter():
        scope.x += 1
        print scope.x

    return counter

Como scopeé realmente apenas uma referência, as ações executadas com seus campos não se modificam realmente scope, portanto, nenhum erro ocorre.

4. Uma maneira alternativa, como apontou @unutbu, seria definir cada variável como uma matriz ( x = [0]) e modificar seu primeiro elemento ( x[0] += 1). Novamente, nenhum erro surge porquex ele próprio não é modificado.

5. Como sugerido por @raxacoricofallapatorius, você pode fazer xuma propriedade decounter

def initCounter ():

    def counter():
        counter.x += 1
        print counter.x

    counter.x = 0
    return counter

27
Existem maneiras de contornar isso. No Python2, você poderia criar x = [0]no escopo externo e usá-lo x[0] += 1no escopo interno. No Python3, você pode manter seu código como está e usar a palavra-chave não - local .
Unutbu

"Embora a função interna possa ler as variáveis ​​da função externa, ela não pode escrevê-las." - Isso é impreciso conforme o comentário do unutbu. O problema é que, quando o Python encontra algo como x = ..., x é interpretado como uma variável local, o que, é claro, ainda não está definido nesse ponto. OTOH, se x é um objeto mutável com um método mutável, ele pode ser modificado perfeitamente, por exemplo, se x é um objeto que suporta o método inc () que se modifica, x.inc () funcionará sem problemas.
Thanh DK

@ThanhDK Isso não significa que você não pode gravar na variável? Quando você usa um método mutável para chamar um método, você está apenas dizendo a ele para se modificar, na verdade não está modificando a variável (que apenas contém uma referência ao objeto). Em outras palavras, a referência para a qual a variável xaponta permanece exatamente a mesma, mesmo que você chame inc()ou seja o que for, e você não gravou efetivamente na variável.
usar o seguinte comando

4
Existe outra opção, estritamente melhor do que o nº 2, imv, de criar xuma propriedade decounter .
Orome 12/11/2015

9
Python 3 tem a nonlocalpalavra - chave, que é parecida globalcom as variáveis ​​de uma função externa. Isso permitirá que uma função interna reconecte um nome de suas funções externas. Eu acho que "vincular ao nome" é mais preciso do que "modificar a variável".
leewz

16

O Python 2 não tinha encerramentos - tinha soluções alternativas que se assemelhavam fechamentos.

Já existem muitos exemplos nas respostas - copiar variáveis ​​para a função interna, modificar um objeto na função interna etc.

No Python 3, o suporte é mais explícito - e sucinto:

def closure():
    count = 0
    def inner():
        nonlocal count
        count += 1
        print(count)
    return inner

Uso:

start = closure()
start() # prints 1
start() # prints 2
start() # prints 3

A nonlocalpalavra-chave vincula a função interna à variável externa mencionada explicitamente, incluindo-a. Daí mais explicitamente um 'fechamento'.


1
Interessante, para referência: docs.python.org/3/reference/… . Não sei por que não é fácil encontrar mais informações sobre fechamentos (e como você pode esperar que eles se comportem, vindo de JS) na documentação do python3?
user3773048

9

Eu tive uma situação em que precisava de um espaço para nome separado, mas persistente. Eu usei aulas. Eu não faço de outra maneira. Nomes segregados mas persistentes são fechamentos.

>>> class f2:
...     def __init__(self):
...         self.a = 0
...     def __call__(self, arg):
...         self.a += arg
...         return(self.a)
...
>>> f=f2()
>>> f(2)
2
>>> f(2)
4
>>> f(4)
8
>>> f(8)
16

# **OR**
>>> f=f2() # **re-initialize**
>>> f(f(f(f(2)))) # **nested**
16

# handy in list comprehensions to accumulate values
>>> [f(i) for f in [f2()] for i in [2,2,4,8]][-1] 
16

6
def nested1(num1): 
    print "nested1 has",num1
    def nested2(num2):
        print "nested2 has",num2,"and it can reach to",num1
        return num1+num2    #num1 referenced for reading here
    return nested2

Dá:

In [17]: my_func=nested1(8)
nested1 has 8

In [21]: my_func(5)
nested2 has 5 and it can reach to 8
Out[21]: 13

Este é um exemplo do que é um fechamento e como ele pode ser usado.


0

Eu gostaria de oferecer outra comparação simples entre exemplo python e JS, se isso ajudar a tornar as coisas mais claras.

JS:

function make () {
  var cl = 1;
  function gett () {
    console.log(cl);
  }
  function sett (val) {
    cl = val;
  }
  return [gett, sett]
}

e executando:

a = make(); g = a[0]; s = a[1];
s(2); g(); // 2
s(3); g(); // 3

Pitão:

def make (): 
  cl = 1
  def gett ():
    print(cl);
  def sett (val):
    cl = val
  return gett, sett

e executando:

g, s = make()
g() #1
s(2); g() #1
s(3); g() #1

Razão: Como muitos outros disseram acima, em python, se houver uma atribuição no escopo interno para uma variável com o mesmo nome, uma nova referência no escopo interno será criada. Não é assim com JS, a menos que você declare explicitamente um com a varpalavra - chave.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.