¿Por qué las funciones anidadas de Python no se llaman cierres?


249

He visto y usado funciones anidadas en Python, y coinciden con la definición de un cierre. Entonces, ¿por qué se les llama en nested functionslugar de closures?

¿Las funciones anidadas no son cierres porque no las utiliza el mundo externo?

ACTUALIZACIÓN: Estaba leyendo sobre cierres y me hizo pensar en este concepto con respecto a Python. Busqué y encontré el artículo mencionado por alguien en un comentario a continuación, pero no pude entender completamente la explicación en ese artículo, por eso estoy haciendo esta pregunta.


8
Curiosamente, algunos Google me encontraron esto, con fecha de diciembre de 2006: effbot.org/zone/closure.htm . No estoy seguro: ¿los "duplicados externos" están mal vistos en SO?
hbw

1
PEP 227: ámbitos anidados estáticamente para obtener más información.
Honesto Abe

Respuestas:


394

Un cierre se produce cuando una función tiene acceso a una variable local desde un ámbito de cierre que ha finalizado su ejecución.

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

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

Cuando make_printerse llama, se coloca un nuevo marco en la pila con el código compilado para la printerfunción como una constante y el valor de msgcomo local. Luego crea y devuelve la función. Debido a que la función hace printerreferencia a la msgvariable, se mantiene viva después de que la make_printerfunción ha regresado.

Entonces, si sus funciones anidadas no

  1. Acceder a variables que son locales a los ámbitos de cierre,
  2. hacerlo cuando se ejecutan fuera de ese alcance,

entonces no son cierres.

Aquí hay un ejemplo de una función anidada que no es un cierre.

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

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

Aquí, estamos vinculando el valor al valor predeterminado de un parámetro. Esto ocurre cuando printerse crea la función y, por lo tanto, no es necesario mantener una referencia al valor de msgexterno printerdespués de los make_printerretornos. msges solo una variable local normal de la función printeren este contexto.


2
Tu respuesta es mucho mejor que la mía, haces un buen punto, pero si vamos a seguir las definiciones de programación funcional más estrictas, ¿son tus ejemplos incluso funciones? Ha pasado un tiempo, y no puedo recordar si la programación funcional estricta permite funciones que no devuelven valores. El punto es discutible, si considera que el valor de retorno es Ninguno, pero ese es un tema completamente diferente.
mikerobi

66
@mikerobi, no estoy seguro de que debamos tener en cuenta la programación funcional, ya que python no es realmente un lenguaje funcional, aunque ciertamente puede usarse como tal. Pero no, las funciones internas no son funciones en ese sentido, ya que su objetivo es crear efectos secundarios. Es fácil crear una función que ilustra los puntos igual de bien, sin embargo,
aaronasterling

31
@mikerobi: El hecho de que un blob de código sea o no un cierre depende de si se cierra o no en su entorno, no de cómo lo llames. Podría ser una rutina, función, procedimiento, método, bloque, subrutina, lo que sea. En Ruby, los métodos no pueden ser cierres, solo los bloques sí. En Java, los métodos no pueden ser cierres, pero las clases sí. Eso no los hace menos de un cierre. (Aunque el hecho de que solo se cierren sobre algunas variables, y no puedan modificarlas, las vuelve casi inútiles). Se podría argumentar que un método es solo un procedimiento cerrado self. (En JavaScript / Python eso es casi cierto.)
Jörg W Mittag

3
@ JörgWMittag Defina "cierra".
Evgeni Sergeev

44
@EvgeniSergeev "cierra", es decir, se refiere "a una variable local [digamos, i] desde un ámbito de inclusión". se refiere, es decir, puede inspeccionar (o cambiar) iel valor, incluso si / cuando ese alcance "ha terminado su ejecución", es decir, la ejecución de un programa ha pasado a otras partes del código. El bloque donde iestá definido ya no existe, pero las funciones a las que ise hace referencia todavía pueden hacerlo. Esto se describe comúnmente como "cierre sobre la variable i". Para no tratar con las variables específicas, se puede implementar como cierre sobre todo el marco del entorno donde se define esa variable.
Will Ness

103

La pregunta ya ha sido respondida por aaronasterling

Sin embargo, alguien podría estar interesado en cómo se almacenan las variables bajo el capó.

Antes de llegar al fragmento:

Los cierres son funciones que heredan variables de su entorno envolvente. Cuando pasa una devolución de llamada de función como argumento a otra función que hará E / S, esta función de devolución de llamada se invocará más tarde, y esta función, casi mágicamente, recordará el contexto en el que se declaró, junto con todas las variables disponibles en ese contexto

  • Si una función no utiliza variables libres, no forma un cierre.

  • Si hay otro nivel interno que utiliza variables libres, todos los niveles anteriores guardan el entorno léxico (ejemplo al final)

  • Los atributos de función func_closureen python <3.X o __closure__en python> 3.X guardan las variables libres.

  • Cada función en python tiene los atributos de este cierre, pero no guarda ningún contenido si no hay variables libres.

ejemplo: de atributos de cierre pero sin contenido dentro ya que no hay variable libre.

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

NB: LA VARIABLE GRATUITA DEBE CREAR UN CIERRE.

Explicaré usando el mismo fragmento que el anterior:

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

Y todas las funciones de Python tienen un atributo de cierre, así que examinemos las variables de cierre asociadas con una función de cierre.

Aquí está el atributo func_closurepara la funciónprinter

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

El closureatributo devuelve una tupla de objetos de celda que contienen detalles de las variables definidas en el ámbito de inclusión.

El primer elemento en el func_closure que podría ser None o una tupla de celdas que contienen enlaces para las variables libres de la función y es de solo lectura.

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

Aquí, en el resultado anterior, puede ver cell_contents, veamos qué almacena:

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

Entonces, cuando llamamos a la función printer(), accede al valor almacenado dentro de cell_contents. Así es como obtuvimos la salida como 'Foo!'

Nuevamente, explicaré el uso del fragmento anterior con algunos cambios:

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

En el fragmento anterior, no imprimo msg dentro de la función de impresora, por lo que no crea ninguna variable libre. Como no hay una variable libre, no habrá contenido dentro del cierre. Eso es exactamente lo que vemos arriba.

Ahora explicaré otro fragmento diferente para aclarar todo Free Variablecon 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')

Entonces, vemos que una func_closurepropiedad es una tupla de celdas de cierre , podemos referirlas explícitamente y su contenido: una celda tiene la propiedad "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
>>>

Aquí, cuando llamamos inn, referirá todas las variables libres de guardado para que obtengamosI am free variable

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

99
En Python 3, func_closureahora se llama __closure__, de manera similar a los otros func_*atributos.
lvc

3
También __closure_está disponible en Python 2.6+ para compatibilidad con Python 3.
Pierre

Cierre se refiere al registro que almacena las variables cerradas, adjuntas al objeto de función. No es la función en sí. En Python, es el __closure__objeto el cierre.
Martijn Pieters

Gracias @MartijnPieters por tu aclaración.
James Sapam

71

Python tiene un soporte débil para el cierre. Para ver a qué me refiero, tome el siguiente ejemplo de un contador que usa cierre con 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

El cierre es bastante elegante ya que le da a las funciones escritas así la capacidad de tener "memoria interna". A partir de Python 2.7 esto no es posible. Si intentas

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

count = initCounter();

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

Obtendrá un error que dice que x no está definido. Pero, ¿cómo puede ser eso si otros han demostrado que puedes imprimirlo? Esto se debe a cómo Python administra el alcance variable de las funciones. Si bien la función interna puede leer las variables de la función externa, no puede escribirlas .

Esto es una pena realmente. Pero con solo el cierre de solo lectura, al menos puede implementar el patrón decorador de funciones para el cual Python ofrece azúcar sintáctico.

Actualizar

Como se ha señalado, hay formas de lidiar con las limitaciones del alcance de Python y expondré algunas.

1. Use la globalpalabra clave (en general no se recomienda).

2. En Python 3.x, use la nonlocalpalabra clave (sugerida por @unutbu y @leewz)

3. Definir una clase modificable simpleObject

class Object(object):
    pass

y crea un Object scopeinside initCounterpara almacenar las variables

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

    return counter

Como scoperealmente es solo una referencia, las acciones tomadas con sus campos no se modifican realmente scope, por lo que no surge ningún error.

4. Una forma alternativa, como señaló @unutbu, sería definir cada variable como una matriz ( x = [0]) y modificar su primer elemento ( x[0] += 1). Nuevamente, no surge ningún error porque xno se modifica por sí mismo.

5. Según lo sugerido por @raxacoricofallapatorius, puede hacer xuna propiedad decounter

def initCounter ():

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

    counter.x = 0
    return counter

27
Hay formas de evitar esto. En Python2, puede realizar x = [0]en el ámbito externo y utilizar x[0] += 1en el ámbito interno. En Python3, puede mantener su código tal como está y usar la palabra clave no local .
unutbu

"Si bien la función interna puede leer las variables de la función externa, no puede escribirlas". - Esto es inexacto según el comentario de unutbu. El problema es que cuando Python encuentra algo como x = ..., x se interpreta como una variable local, que por supuesto aún no está definida en ese punto. OTOH, si x es un objeto mutable con un método mutable, puede modificarse perfectamente, por ejemplo, si x es un objeto que admite el método inc () que se muta, x.inc () funcionará sin problemas.
Thanh DK

@ThanhDK ¿No significa eso que no puedes escribir en la variable? Cuando usa llamar a un método desde un objeto mutable, solo le está diciendo que se modifique a sí mismo, en realidad no está modificando la variable (que simplemente contiene una referencia al objeto). En otras palabras, la referencia a la que xapunta la variable permanece exactamente igual incluso si llama inc()o lo que sea, y no escribió efectivamente en la variable.
user193130

44
Hay otra opción, estrictamente mejor que # 2, imv, de hacer xuna propiedad decounter .
orome

99
Python 3 tiene la nonlocalpalabra clave, que es como globalpero para las variables de una función externa. Esto permitirá que una función interna vuelva a vincular un nombre de sus funciones externas. Creo que "vincular al nombre" es más preciso que "modificar la variable".
leewz

16

Python 2 no tenía cierres, tenía soluciones que parecían cierres.

Hay muchos ejemplos en las respuestas ya dadas: copiar variables en la función interna, modificar un objeto en la función interna, etc.

En Python 3, el soporte es más explícito y 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

La nonlocalpalabra clave vincula la función interna a la variable externa mencionada explícitamente, de hecho, la encierra. Por lo tanto, más explícitamente un 'cierre'.


1
Interesante, para referencia: docs.python.org/3/reference/… . No sé por qué no es fácil encontrar más información sobre los cierres (y cómo podría esperar que se comporten, viniendo de JS) en la documentación de python3.
usuario3773048

9

Tuve una situación en la que necesitaba un espacio de nombre separado pero persistente. Usé clases Yo no de otra manera. Los nombres segregados pero persistentes son cierres.

>>> 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

Da:

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 es un ejemplo de qué es un cierre y cómo se puede usar.


0

Me gustaría ofrecer otra comparación simple entre Python y JS, si esto ayuda a aclarar las cosas.

JS:

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

y ejecutando:

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

Pitón:

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

y ejecutando:

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

Motivo: Como muchos otros dijeron anteriormente, en python, si hay una asignación en el ámbito interno a una variable con el mismo nombre, se crea una nueva referencia en el ámbito interno. No es así con JS, a menos que declare explícitamente uno con la varpalabra clave.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.