¿Puede explicar los cierres (en relación con Python)?

He estado leyendo mucho sobre cierres y creo que los entiendo, pero sin nublar la imagen para mí y para otros, espero que alguien pueda explicar los cierres de la manera más sucinta y clara posible. Estoy buscando una explicación simple que pueda ayudarme a entender dónde y por qué querría usarlos.

Cierre de cierres.

Los objetos son datos con métodos adjuntos, los cierres son funciones con datos adjuntos.

def make_counter(): i = 0 def counter(): # counter() is a closure nonlocal i i += 1 return i return counter c1 = make_counter() c2 = make_counter() print (c1(), c1(), c2(), c2()) # -> 1 2 1 2 

Es simple: una función que hace referencia a variables de un ámbito contenedor, potencialmente después de que el flujo de control haya abandonado ese ámbito. Ese último bit es muy útil:

 >>> def makeConstantAdder(x): ... constant = x ... def adder(y): ... return y + constant ... return adder ... >>> f = makeConstantAdder(12) >>> f(3) 15 >>> g = makeConstantAdder(4) >>> g(3) 7 

Tenga en cuenta que 12 y 4 han “desaparecido” dentro de f y g, respectivamente, esta característica es la que hace que f y g se cierren correctamente.

Me gusta esta definición aproximada, sucinta :

Una función que puede referirse a entornos que ya no están activos.

Yo agregaria

Un cierre le permite unir variables en una función sin pasarlas como parámetros .

Los decoradores que aceptan parámetros son un uso común para los cierres. Los cierres son un mecanismo de implementación común para ese tipo de “fábrica de funciones”. Con frecuencia, elijo usar cierres en el Patrón de estrategia cuando la estrategia es modificada por los datos en tiempo de ejecución.

En un lenguaje que permite la definición de bloques anónimos (p. Ej., Ruby, C #) los cierres pueden usarse para implementar (lo que equivale a) nuevas estructuras de control nuevas. La falta de bloques anónimos es una de las limitaciones de los cierres en Python .

Para ser honesto, entiendo perfectamente los cierres, excepto que nunca he tenido claro qué es exactamente lo que es el “cierre” y lo que es tan “cerrado” al respecto. Te recomiendo que dejes de buscar cualquier lógica detrás de la elección del término.

De todos modos, aquí está mi explicación:

 def foo(): x = 3 def bar(): print x x = 5 return bar bar = foo() bar() # print 5 

Una idea clave aquí es que el objeto de función devuelto por foo retiene un enlace a la var ‘x’ local, incluso aunque ‘x’ haya quedado fuera del scope y esté desactivada. Este gancho es para la var en sí, no solo el valor que var tenía en ese momento, así que cuando se llama a la barra, se imprime 5, no 3.

También tenga en cuenta que Python 2.x tiene un cierre limitado: no hay forma de que pueda modificar ‘x’ dentro de ‘barra’ porque escribir ‘x = bla’ declararía una ‘x’ local en la barra, no asignarla a ‘x’ de foo . Este es un efecto secundario de la asignación = statement de Python. Para solucionar esto, Python 3.0 introduce la palabra clave no local:

 def foo(): x = 3 def bar(): print x def ack(): nonlocal x x = 7 x = 5 return (bar, ack) bar, ack = foo() ack() # modify x of the call to foo bar() # print 7 

Nunca he escuchado que las transacciones se usen en el mismo contexto para explicar qué es un cierre y realmente no hay ninguna semántica de transacciones aquí.

Se llama cierre porque “cierra sobre” la variable externa (constante), es decir, no es solo una función, sino un recinto del entorno donde se creó la función.

En el siguiente ejemplo, llamar al cierre g después de cambiar x también cambiará el valor de x dentro de g, ya que g se cierra sobre x:

 x = 0 def f(): def g(): return x * 2 return g closure = f() print(closure()) # 0 x = 2 print(closure()) # 4 

Este es un caso de uso típico de los cierres: devoluciones de llamada para elementos GUI (esto sería una alternativa a la subclasificación de la clase de botón). Por ejemplo, puede construir una función que se llamará en respuesta a una pulsación de botón y “cerrar” sobre las variables relevantes en el ámbito principal que son necesarias para procesar el clic. De esta manera puede conectar interfaces bastante complicadas desde la misma función de inicialización, construyendo todas las dependencias en el cierre.

En Python, un cierre es una instancia de una función que tiene variables vinculadas a ella de manera inmutable.

De hecho, el modelo de datos explica esto en su descripción del atributo de funciones __closure__ :

Ninguna o una tupla de celdas que contienen enlaces para las variables libres de la función. Solo lectura

Para demostrar esto:

 def enclosure(foo): def closure(bar): print(foo, bar) return closure closure_instance = enclosure('foo') 

Claramente, sabemos que ahora tenemos una función apuntada desde el nombre de variable closure_instance . Ostensiblemente, si lo llamamos con un objeto, bar , debería imprimir la cadena, 'foo' y cualquiera que sea la representación de la cadena de bar .

De hecho, la cadena ‘foo’ está vinculada a la instancia de la función, y podemos leerla directamente aquí, accediendo al atributo cell_contents de la primera (y única) celda en la tupla del atributo __closure__ :

 >>> closure_instance.__closure__[0].cell_contents 'foo' 

Además, los objetos de celda se describen en la documentación de la API de C:

Los objetos “celulares” se utilizan para implementar variables referenciadas por múltiples ámbitos.

Y podemos demostrar el uso de nuestro cierre, observando que 'foo' está bloqueado en la función y no cambia:

 >>> closure_instance('bar') foo bar >>> closure_instance('baz') foo baz >>> closure_instance('quux') foo quux 

Y nada puede cambiarlo:

 >>> closure_instance.__closure__ = None Traceback (most recent call last): File "", line 1, in  TypeError: readonly attribute 

Funciones parciales

El ejemplo dado utiliza el cierre como una función parcial, pero si este es nuestro único objective, el mismo objective se puede lograr con functools.partial

 >>> from __future__ import print_function # use this if you're in Python 2. >>> partial_function = functools.partial(print, 'foo') >>> partial_function('bar') foo bar >>> partial_function('baz') foo baz >>> partial_function('quux') foo quux 

También hay cierres más complicados que no se ajustan al ejemplo de función parcial, y los demostraré más a medida que el tiempo lo permita.

Aquí hay un ejemplo de cierres de Python3.

 def closure(x): def counter(): nonlocal x x += 1 return x return counter; counter1 = closure(100); counter2 = closure(200); print("i from closure 1 " + str(counter1())) print("i from closure 1 " + str(counter1())) print("i from closure 2 " + str(counter2())) print("i from closure 1 " + str(counter1())) print("i from closure 1 " + str(counter1())) print("i from closure 1 " + str(counter1())) print("i from closure 2 " + str(counter2())) # result i from closure 1 101 i from closure 1 102 i from closure 2 201 i from closure 1 103 i from closure 1 104 i from closure 1 105 i from closure 2 202 

Para mí, los “cierres” son funciones capaces de recordar el entorno en el que se crearon. Esta funcionalidad le permite usar variables o métodos dentro del cierre que, de otra manera, no podría usar ya sea porque ya no existen o están fuera del scope debido al scope. Veamos este código en ruby:

 def makefunction (x) def multiply (a,b) puts a*b end return lambda {|n| multiply(n,x)} # => returning a closure end func = makefunction(2) # => we capture the closure func.call(6) # => Result equal "12" 

funciona incluso cuando los dos métodos, “multiplicación” y variable “x”, ya no existen. Todo porque la capacidad de cierre para recordar.

 # A Closure is a function object that remembers values in enclosing scopes even if they are not present in memory. # Defining a closure # This is an outer function. def outer_function(message): # This is an inner nested function. def inner_function(): print(message) return inner_function # Now lets call the outer function and return value bound to name 'temp' temp = outer_function("Hello") # On calling temp, 'message' will be still be remembered although we had finished executing outer_function() temp() # Technique by which some data('message') that remembers values in enclosing scopes # even if they are not present in memory is called closures # Output: Hello 

Los criterios a cumplir por los cierres son:

  1. Debemos tener función anidada.
  2. La función anidada debe referirse al valor definido en la función que lo contiene.
  3. La función de cierre debe devolver la función anidada.

 # Example 2 def make_multiplier_of(n): # Outer function def multiplier(x): # Inner nested function return x * n return multiplier # Multiplier of 3 times3 = make_multiplier_of(3) # Multiplier of 5 times5 = make_multiplier_of(5) print(times5(3)) # 15 print(times3(2)) # 6 

Todos hemos usado decoradores en python. Son buenos ejemplos para mostrar cuáles son las funciones de cierre en python.

 class Test(): def decorator(func): def wrapper(*args): b = args[1] + 5 return func(b) return wrapper @decorator def foo(val): print val + 2 obj = Test() obj.foo(5) 

aquí el valor final es 12

Aquí, la función de envoltura puede acceder al objeto func porque la envoltura es “cierre léxico”, puede acceder a sus atributos principales. Por eso, es capaz de acceder al objeto func.

Me gustaría compartir mi ejemplo y una explicación sobre los cierres. Hice un ejemplo de python, y dos figuras para demostrar estados de stack.

 def maker(a, b, n): margin_top = 2 padding = 4 def message(msg): print('\n' * margin_top, a * n, ' ' * padding, msg, ' ' * padding, b * n) return message f = maker('*', '#', 5) g = maker('', '♥', 3) … f('hello') g('good bye!') 

La salida de este código sería la siguiente:

 ***** hello #####  good bye! ♥♥♥ 

Aquí hay dos figuras para mostrar stacks y el cierre adjunto al objeto de función.

cuando la función es devuelta por el fabricante

cuando la función se llama más tarde

Cuando se llama a la función a través de un parámetro o una variable no local, el código necesita enlaces de variables locales como margin_top, padding y a, b, n. Para garantizar que el código de función funcione, debe estar accesible el marco de stack de la función de fabricante que desapareció hace mucho tiempo, que está respaldado en el cierre que podemos encontrar junto con el objeto de función ‘message’.

La mejor explicación que he visto de un cierre fue explicar el mecanismo. Fue algo como ésto:

Imagine la stack de su progtwig como un árbol degenerado donde cada nodo tiene un solo hijo y el nodo de una sola hoja es el contexto de su procedimiento en ejecución actualmente.

Ahora relaje la restricción de que cada nodo puede tener un solo hijo.

Si lo hace, puede tener una construcción (‘rendimiento’) que puede regresar de un procedimiento sin descartar el contexto local (es decir, no lo saca de la stack cuando regresa). La próxima vez que se invoca el procedimiento, la invocación recoge el marco de stack (árbol) antiguo y continúa ejecutándose donde lo dejó.