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


84

He estado leyendo mucho sobre los cierres y creo que los entiendo, pero sin empañar la imagen para mí y para los demás, 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 comprender dónde y por qué querría usarlos.

Respuestas:


96

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

6
Tenga en cuenta que nonlocalse agregó en python 3, python 2.x no tenía cierres completos de lectura y escritura (es decir, podría leer variables cerradas, pero no cambiar sus valores)
James Porter

6
@JamesPorter: nota: puede emular la nonlocalpalabra clave en Python 2 usando un objeto mutable, por ejemplo, L = [0] \n def counter(): L[0] += 1; return L[0]es decir, no puede cambiar el nombre (vincularlo a otro objeto) en este caso, pero puede cambiar el objeto mutable en sí mismo al que se refiere el nombre a. La lista es obligatoria porque los enteros son inmutables en Python.
jfs

1
@JFSebastian: cierto. Eso siempre se siente como un truco sucio y sucio :)
James Porter

46

Es simple: una función que hace referencia a variables de un alcance contenedor, potencialmente después de que el flujo de control haya abandonado ese alcance. 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 lo que hace que f y g sean los cierres adecuados.


No hay necesidad de hacerlo constant = x; podría hacerlo return y + xen la función anidada (o recibir el argumento con el nombre constant), y funcionaría bien; Los argumentos son capturados por el cierre no de manera diferente a los locales sin argumentos.
ShadowRanger

15

Me gusta esta definición aproximada y sucinta :

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

Yo agregaría

Un cierre le permite vincular 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 se modifica por los datos en tiempo de ejecución.

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


15

Para ser honesto, entiendo perfectamente los cierres, excepto que nunca he tenido claro qué es exactamente lo que es el "cierre" y qué tiene de "cierre". 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 local 'x' aunque 'x' ha salido del alcance y debería estar inactivo. Este gancho es para la var en sí, no solo el valor que tenía var en ese momento, por lo que cuando se llama a bar, imprime 5, no 3.

También tenga claro que Python 2.x tiene un cierre limitado: no hay forma de que pueda modificar 'x' dentro de 'bar' porque escribir 'x = bla' declararía una 'x' local en la barra, no asignaría a 'x' de foo . Este es un efecto secundario de la declaración asignación = de Python. Para evitar 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

7

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

Se llama cierre porque "cierra" 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 cierra sobre x:

x = 0

def f():
    def g(): 
        return x * 2
    return g


closure = f()
print(closure()) # 0
x = 2
print(closure()) # 4

Además, tal como está, g()calcula x * 2pero no devuelve nada. Eso debería ser return x * 2. +1 sin embargo para una explicación de la palabra "cierre".
Bruno Le Floch

3

Aquí hay un caso de uso típico para cierres: devoluciones de llamada para elementos de GUI (esta 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 la presión de un 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.


2

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 __closure__atributo de funciones :

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 la variable closure_instance. Aparentemente, si lo llamamos con un objeto, bardebería imprimir la cadena, 'foo'y cualquiera que sea la representación de la cadena bar.

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

>>> 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 de "celda" se utilizan para implementar variables a las que hacen referencia varios ámbitos

Y podemos demostrar el uso de nuestro cierre, notando que 'foo'está atascado 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 "<stdin>", line 1, in <module>
TypeError: readonly attribute

Funciones parciales

El ejemplo dado usa el cierre como una función parcial, pero si este es nuestro único objetivo, el mismo objetivo 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 encajarían en el ejemplo de función parcial, y los demostraré más a medida que el tiempo lo permita.


2
# 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 que deben cumplir los cierres son:

  1. Debemos tener una función anidada.
  2. La función anidada debe hacer referencia al valor definido en la función adjunta.
  3. La función envolvente 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

1

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

0

Para mí, los "cierres" son funciones que son capaces de recordar el entorno en el que fueron creados. Esta funcionalidad, te permite usar variables o métodos dentro del cierre que, de otra manera, no podrías usar porque ya no existen o están fuera de alcance por alcance. 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 ambos, el método "multiplicar" y la variable "x", ya no existen. Todo por la capacidad de cierre para recordar.


0

todos hemos usado Decoradores en Python. Son buenos ejemplos para mostrar qué 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 envoltorio puede acceder al objeto func porque el envoltorio es "cierre léxico", puede acceder a sus atributos principales. Por eso, puede acceder al objeto func.


0

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

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 las pilas 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 vinculaciones de variables locales como margin_top, padding y a, b, n. Para garantizar que el código de la función funcione, el marco de pila de la función maker que desapareció hace mucho tiempo debería ser accesible, que está respaldado en el cierre que podemos encontrar junto con el objeto de función del mensaje.


-2

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

Imagine su pila de programas como un árbol degenerado donde cada nodo tiene solo un hijo y el nodo de hoja única es el contexto de su procedimiento en ejecución.

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

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


Esa NO es una explicación de los cierres.
Jules

Estás describiendo continuaciones, no cierres.
Matthew Olenik
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.