Cierres de Python: casos de uso comunes y ejemplos
En Python, un cierre suele ser una función definida dentro de otra función. Esta función interna toma los objetos definidos en su alcance circundante y los asocia con el objeto de función interna en sí. La combinación resultante se llama cierre.
Los cierres son una característica común en los lenguajes de programación funcionales. En Python, los cierres pueden ser bastante útiles porque te permiten crear decoradores basados en funciones, que son herramientas poderosas.
En este tutorial podrás:
- Aprenda qué son los cierres y cómo funcionan en Python
- Conozca casos de uso comunes de cierres
- Explora alternativas a los cierres
Para aprovechar al máximo este tutorial, debe estar familiarizado con varios temas de Python, incluidas funciones, funciones internas, decoradores, clases e instancias invocables.
Conociendo los cierres en Python
Un cierre es una función que retiene el acceso a su alcance léxico, incluso cuando la función se ejecuta fuera de ese alcance. Cuando la función adjunta devuelve la función interna, obtienes un objeto de función con un alcance extendido.
En otras palabras, los cierres son funciones que capturan los objetos definidos en su alcance circundante, permitiéndole usarlos en su cuerpo. Esta función le permite utilizar cierres cuando necesita conservar información de estado entre llamadas consecutivas.
Los cierres son comunes en los lenguajes de programación que se centran en la programación funcional y Python admite cierres como parte de su amplia variedad de características.
En Python, un cierre es una función que usted define en y regresa desde otra función. Esta función interna puede retener los objetos definidos en el ámbito no local justo antes de la definición de la función interna.
Para comprender mejor los cierres en Python, primero observará las funciones internas porque los cierres también son funciones internas.
Funciones internas
En Python, una función interna es una función que defines dentro de otra función. Este tipo de función puede acceder y actualizar nombres en su función adjunta, que es el alcance no local.
Aquí hay un ejemplo rápido:
>>> def outer_func():
... name = "Pythonista"
... def inner_func():
... print(f"Hello, {name}!")
... inner_func()
...
>>> outer_func()
Hello, Pythonista!
>>> greeter = outer_func()
>>> print(greeter)
None
En este ejemplo, define outer_func()
a nivel de módulo o alcance global. Dentro de esta función, define la variable local name
. Luego, define otra función llamada inner_func()
. Debido a que esta segunda función vive en el cuerpo de outer_func()
, es una función interna o anidada. Finalmente, llama a la función interna, que utiliza la variable name
definida en la función adjunta.
Cuando llamas a outer_func()
, inner_func()
interpola name
en la cadena de saludo e imprime el resultado en tu pantalla.
Nota: Para obtener más información sobre las funciones internas, consulte Funciones internas de Python: ¿para qué sirven? tutorial.
En el ejemplo anterior, definió una función interna que puede usar los nombres en el ámbito adjunto. Sin embargo, cuando llamas a la función externa, no obtienes una referencia a la función interna. La función interna y los nombres locales no estarán disponibles fuera de la función externa.
En la siguiente sección, aprenderá cómo convertir una función interna en un cierre, lo que hace que la función interna y las variables retenidas estén disponibles para usted.
Cierres de funciones
Todos los cierres son funciones internas, pero no todas las funciones internas son cierres. Para convertir una función interna en un cierre, debe devolver el objeto de función interna de la función externa. Esto puede sonar como un trabalenguas, pero así es como puedes hacer que outer_func()
devuelva un objeto de cierre:
>>> def outer_func():
... name = "Pythonista"
... def inner_func():
... print(f"Hello, {name}!")
... return inner_func
...
>>> outer_func()
<function outer_func.<locals>.inner_func at 0x1066d16c0>
>>> greeter = outer_func()
>>> greeter()
Hello, Pythonista!
En esta nueva versión de outer_func()
, devuelves el objeto de función inner_func
en lugar de llamarlo. Cuando llamas a outer_func()
, obtienes un objeto de función que es un cierre en lugar de un mensaje de saludo. Este objeto de cierre recuerda y puede acceder al valor de name
incluso después de que outer_func()
haya regresado. Es por eso que recibes el mensaje de saludo cuando llamas a greeter()
.
Para crear un cierre de Python, necesita los siguientes componentes:
Una función externa o envolvente: Esta es una función que contiene otra función, a menudo denominada función interna. La función externa puede tomar argumentos y definir variables a las que la función interna puede acceder y actualizar.
Variables que son locales de la función externa: Estas son variables de su ámbito circundante. Python conserva estas variables, lo que le permite usarlas en el cierre, incluso después de que la función externa haya regresado.
Una función interna o anidada: Esta es una función definida dentro de la función externa. Puede acceder y actualizar las variables desde la función externa incluso después de que la función externa haya regresado.
En el ejemplo de esta sección, tiene una función externa, una variable local (name
) y una función interna. El último paso para obtener un objeto de cierre de esta combinación es devolver el objeto de función interna de la función externa.
Es importante tener en cuenta que también puedes usar funciones lambda
para crear cierres:
>>> def outer_func():
... name = "Pythonista"
... return lambda: print(f"Hello, {name}!")
...
>>> greeter = outer_func()
>>> greeter()
Hello, Pythonista!
En esta versión modificada de outer_func()
, se utiliza una función lambda
para construir el cierre, que funciona como el original.
Variables capturadas
Como has aprendido, un cierre retiene las variables de su alcance circundante. Considere el siguiente ejemplo de juguete:
>>> def outer_func(outer_arg):
... local_var = "Outer local variable"
... def closure():
... print(outer_arg)
... print(local_var)
... print(another_local_var)
... another_local_var = "Another outer local variable"
... return closure
...
>>> closure = outer_func("Outer argument")
>>> closure()
Outer argument
Outer local variable
Another outer local variable
En este ejemplo, outer_arg
, local_var
y another_local_var
se adjuntan al cierre cuando llamas a outer_func()
. , incluso si su alcance contenedor ya no está disponible. Sin embargo, closure()
puede acceder a estas variables porque ahora son parte del cierre mismo. Por eso se puede decir que un cierre es una función con un alcance extendido.
Los cierres también pueden actualizar el valor de estas variables, y esto puede dar lugar a dos escenarios: las variables pueden apuntar a un objeto mutable o inmutable.
Para actualizar el valor de una variable que apunta a un objeto inmutable, debe utilizar la instrucción nonlocal
. Considere el siguiente ejemplo:
>>> def make_counter():
... count = 0
... def counter():
... nonlocal count
... count += 1
... return count
... return counter
...
>>> counter = make_counter()
>>> counter()
1
>>> counter()
2
>>> counter()
3
En este ejemplo, count
contiene una referencia a un valor entero, que es inmutable. Para actualizar el valor de count
, utiliza una declaración nonlocal
que le indica a Python que desea reutilizar la variable del ámbito no local.
Nota: Para profundizar en los objetos mutables e inmutables, consulte Tipos mutables e inmutables de Python: ¿Cuál es la diferencia?
Cuando su variable apunta a un objeto mutable, puede modificar el valor de la variable en su lugar:
>>> def make_appender():
... items = []
... def appender(new_item):
... items.append(new_item)
... return items
... return appender
...
>>> appender = make_appender()
>>> appender("First item")
['First item']
>>> appender("Second item")
['First item', 'Second item']
>>> appender("Third item")
['First item', 'Second item', 'Third item']
En este ejemplo, la variable items
apunta a un objeto list
, que es mutable. En este caso, no es necesario utilizar la palabra clave nonlocal
. Puede modificar la lista en su lugar.
Creando cierres para retener el estado
En la práctica, puedes utilizar un cierre de Python en varias situaciones diferentes. En esta sección, explorará cómo usar cierres para crear funciones de fábrica, mantener el estado en llamadas a funciones e implementar devoluciones de llamadas, lo que permite un código más dinámico, flexible y eficiente.
Crear funciones de fábrica
Puede escribir funciones para crear cierres con alguna configuración o parámetros iniciales. Esto es particularmente útil cuando necesita crear múltiples funciones similares con diferentes configuraciones.
Por ejemplo, supongamos que desea calcular raíces numéricas con diferentes grados y precisiones de resultados. En esta situación, puede codificar una función de fábrica que devuelva cierres con grados y precisiones predefinidos como el siguiente ejemplo:
>>> def make_root_calculator(root_degree, precision=2):
... def root_calculator(number):
... return round(pow(number, 1 / root_degree), precision)
... return root_calculator
...
>>> square_root = make_root_calculator(2, 4)
>>> square_root(42)
6.4807
>>> cubic_root = make_root_calculator(3)
>>> cubic_root(42)
3.48
make_root_calculator()
es una función de fábrica que puede utilizar para crear funciones que calculen diferentes raíces numéricas. En esta función se toman como parámetros de configuración el grado raíz y la precisión deseada.
Luego, define una función interna que toma un número como argumento y calcula la raíz especificada con la precisión deseada. Finalmente, devuelves la función interna, creando un cierre.
Puede utilizar esta función para crear cierres que le permitan calcular raíces numéricas de diferentes grados, como raíces cuadradas y cúbicas. Tenga en cuenta que también puede modificar la precisión del resultado.
Creación de funciones con estado
Puede utilizar cierres para conservar el estado entre llamadas a funciones. Estas funciones se conocen como funciones con estado y los cierres son una forma de crearlas.
Por ejemplo, digamos que desea escribir una función que tome valores numéricos consecutivos de un flujo de datos y calcule su promedio acumulado. Entre llamadas, la función debe realizar un seguimiento de los valores pasados previamente. En esta situación, puede utilizar la siguiente función:
>>> def cumulative_average():
... data = []
... def average(value):
... data.append(value)
... return sum(data) / len(data)
... return average
...
>>> stream_average = cumulative_average()
>>> stream_average(12)
12.0
>>> stream_average(13)
12.5
>>> stream_average(11)
12.0
>>> stream_average(10)
11.5
En cumulative_average()
, la variable local data
le permite conservar el estado entre llamadas consecutivas del objeto de cierre que devuelve esta función.
A continuación, crea un cierre llamado stream_average()
y lo llama con diferentes valores numéricos. Observe cómo este cierre recuerda los valores pasados anteriormente y calcula el promedio agregando el valor recién proporcionado.
Proporcionar funciones de devolución de llamada
Los cierres se utilizan comúnmente en la programación basada en eventos cuando es necesario crear funciones de devolución de llamada que contienen contexto adicional o información de estado. La programación de la interfaz gráfica de usuario (GUI) es un buen ejemplo de dónde se utilizan estas funciones de devolución de llamada.
Para ilustrar, suponga que desea crear una aplicación "Hello, World!"
con Tkinter, la biblioteca de programación GUI predeterminada de Python. La aplicación necesita una etiqueta para mostrar el saludo y un botón para activarlo. Aquí está el código para esa pequeña aplicación:
import tkinter as tk
app = tk.Tk()
app.title("GUI App")
app.geometry("320x240")
label = tk.Label(
app,
font=("Helvetica", 16, "bold"),
)
label.pack()
def callback(text):
def closure():
label.config(text=text)
return closure
button = tk.Button(
app,
text="Greet",
command=callback("Hello, World!"),
)
button.pack()
app.mainloop()
Este código define una aplicación Tkinter de juguete que consta de una ventana con una etiqueta y un botón. Cuando haces clic en el botón Saludar, la etiqueta muestra el mensaje "¡Hola, mundo!"
.
Nota: Para obtener más información sobre la creación de aplicaciones GUI con Tkinter, consulte el tutorial Programación GUI de Python con Tkinter.
La función callback()
devuelve un objeto de cierre que puede usar para proporcionar el argumento command
del botón. Este argumento acepta objetos invocables que no aceptan argumentos. Si necesita pasar argumentos como lo hizo en el ejemplo, puede usar un cierre.
Decoradores de escritura con cierres
Los decoradores son una característica poderosa en Python. Puede utilizar decoradores para modificar dinámicamente el comportamiento de una función. En Python, tienes dos tipos de decoradores:
- Decoradores basados en funciones
- Decoradores basados en clases
Un decorador basado en funciones es una función que toma un objeto de función como argumento y devuelve otro objeto de función con funcionalidad extendida. Este último objeto de función también es un cierre. Entonces, para crear decoradores basados en funciones, se utilizan cierres.
Nota: Para obtener más información sobre los decoradores, consulte el tutorial Introducción a los decoradores de Python.
Como ya aprendiste, los decoradores te permiten modificar el comportamiento de las funciones sin alterar su código interno. En la práctica, los decoradores basados en funciones son cierres. La característica distintiva es que su objetivo principal es modificar el comportamiento de la función que pasa como argumento a la función que contiene el cierre.
A continuación se muestra un ejemplo de un decorador mínimo que agrega mensajes además de la funcionalidad de la función de entrada:
>>> def decorator(function):
... def closure():
... print("Doing something before calling the function.")
... function()
... print("Doing something after calling the function.")
... return closure
...
En este ejemplo, la función exterior es el decorador. Esta función devuelve un objeto de cierre que modifica el comportamiento original del objeto de función de entrada agregando características adicionales. El cierre puede actuar sobre la función de entrada incluso después de que la función decorator()
haya regresado.
A continuación se explica cómo puede utilizar la sintaxis del decorador para modificar dinámicamente el comportamiento de una función normal de Python:
>>> @decorator
... def greet():
... print("Hi, Pythonista!")
...
>>> greet()
Doing something before calling the function.
Hi, Pythonista!
Doing something after calling the function.
En este ejemplo, utiliza @decorator
para modificar el comportamiento de su función greet()
. Tenga en cuenta que ahora, cuando llama a greet()
, obtiene su funcionalidad original más la funcionalidad agregada por el decorador.
Implementación de memorización con cierres
El almacenamiento en caché puede mejorar el rendimiento de un algoritmo al evitar un nuevo cálculo innecesario. La memorización es una técnica de almacenamiento en caché común que evita que una función se ejecute más de una vez para la misma entrada.
La memorización funciona almacenando el resultado de un conjunto determinado de argumentos de entrada en la memoria y luego haciendo referencia a él cuando sea necesario. Puede utilizar cierres para implementar la memorización.
En el siguiente ejemplo de juguete, se aprovecha un decorador, que también es un cierre, para almacenar en caché los valores que resultan de un costoso cálculo hipotético:
>>> def memoize(function):
... cache = {}
... def closure(number):
... if number not in cache:
... cache[number] = function(number)
... return cache[number]
... return closure
...
Aquí, memoize()
toma un objeto de función como argumento y devuelve otro objeto de cierre. La función interna ejecuta la función de entrada solo para números no procesados. Los números procesados se almacenan en caché en el diccionario cache
junto con el resultado de la función de entrada.
Nota: Python viene con una memorización integrada en la biblioteca estándar. Si necesita almacenamiento en caché en sus proyectos, puede usar @cache
o @lru_cache
del módulo functools
.
Ahora supongamos que tiene la siguiente función de juguete que imita un cálculo costoso:
>>> from time import sleep
>>> def slow_operation(number):
... sleep(0.5)
...
Esta función mantiene la ejecución del código durante sólo medio segundo para imitar una operación costosa. Para hacer esto, utiliza la función sleep()
del módulo time
.
Puede medir el tiempo de ejecución de la función utilizando el siguiente código:
>>> from timeit import timeit
>>> timeit(
... "[slow_operation(number) for number in [2, 3, 4, 2, 3, 4]]",
... globals=globals(),
... number=1,
... )
3.02610950000053
En este fragmento de código, utiliza la función timeit()
del módulo timeit
para averiguar el tiempo de ejecución de slow_operation()
cuando ejecuta esta función con una lista de valores. Para procesar los seis valores de entrada, el código tarda un poco más de tres segundos. Puede utilizar la memorización para hacer que este cálculo sea más eficiente omitiendo los valores de entrada repetidos.
Continúe y decore slow_operation()
usando @memoize
como se muestra a continuación. Luego, ejecute el código de sincronización:
>>> @memoize
... def slow_operation(number):
... sleep(0.5)
...
>>> timeit(
... "[slow_operation(number) for number in [2, 3, 4, 2, 3, 4]]",
... globals=globals(),
... number=1,
... )
1.5151869590008573
Ahora el mismo código lleva la mitad de tiempo gracias a la técnica de memorización. Esto se debe a que la función slow_operation()
no se ejecuta para valores de entrada repetidos.
Lograr la encapsulación con cierres
En la programación orientada a objetos (POO), las clases proporcionan una forma de combinar datos y comportamiento en una sola entidad. Un requisito común en POO es la encapsulación de datos, un principio que recomienda proteger los datos de un objeto del mundo exterior y evitar el acceso directo.
Nota: Para obtener más información sobre la POO en Python, consulte el tutorial Programación orientada a objetos (POO) en Python.
En Python, lograr una encapsulación sólida de datos puede ser una tarea difícil porque no hay distinción entre atributos privados y públicos. En cambio, Python utiliza una convención de nomenclatura para comunicar si un miembro de una clase determinada es público o no.
Puede utilizar cierres de Python para lograr una encapsulación de datos más estricta. Los cierres le permiten crear un alcance privado para los datos, evitando que los usuarios accedan a esos datos. Esto ayuda a mantener la integridad de los datos y evitar modificaciones no deseadas.
Para ilustrar, digamos que tiene la siguiente clase Stack
:
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
return self._items.pop()
Esta clase Stack
almacena sus datos en un objeto list
llamado ._items
e implementa operaciones de pila comunes, como push. y pop.
Así es como puedes usar esta clase:
>>> from stack_v1 import Stack
>>> stack = Stack()
>>> stack.push(1)
>>> stack.push(2)
>>> stack.push(3)
>>> stack.pop()
3
>>> stack._items
[1, 2]
La funcionalidad básica de tu clase funciona. Sin embargo, aunque el atributo ._items
no es público, puedes acceder a sus valores usando notación de puntos como lo harías con un atributo normal. Este comportamiento dificulta encapsular datos para protegerlos del acceso directo.
Nota: Para obtener más información sobre la estructura de datos de la pila, consulte el tutorial Cómo implementar una pila de Python.
Nuevamente, un cierre proporciona un truco para lograr una encapsulación de datos más estricta. Considere el siguiente código:
def Stack():
_items = []
def push(item):
_items.append(item)
def pop():
return _items.pop()
def closure():
pass
closure.push = push
closure.pop = pop
return closure
En este ejemplo, escribe una función para crear un objeto de cierre en lugar de definir una clase. Dentro de la función, defines una variable local llamada _items
, que será parte de tu objeto de cierre. Utilizará esta variable para almacenar los datos de la pila. Luego, define dos funciones internas que implementan las operaciones de la pila.
La función interna closure()
es un marcador de posición para su cierre. Además de esta función, agrega las funciones push()
y pop()
. Finalmente, devuelves el cierre resultante.
Puede utilizar la función Stack()
prácticamente de la misma manera que utilizó la clase Stack
. Una diferencia significativa es que ahora no tienes acceso a ._items
:
>>> from stack_v2 import Stack
>>> stack = Stack()
>>> stack.push(1)
>>> stack.push(2)
>>> stack.push(3)
>>> stack.pop()
3
>>> stack._items
Traceback (most recent call last):
...
AttributeError: 'function' object has no attribute '_items'
La función Stack()
le permite crear cierres que funcionan como si fueran instancias de la clase Stack
. Sin embargo, no tiene acceso directo a ._items
, lo que mejora la encapsulación de sus datos.
Si eres muy exigente, puedes utilizar un truco avanzado para acceder al contenido de ._items
:
>>> stack.push.__closure__[0].cell_contents
[1, 2]
El atributo .__closure__
devuelve una tupla de celdas que contienen enlaces para las variables del cierre. Un objeto de celda tiene un atributo llamado cell_contents
, que puedes usar para obtener el valor de la celda.
Aunque este truco le permite acceder a variables capturadas, normalmente no se usa en el código Python. Al final, si estás intentando lograr la encapsulación, ¿por qué la romperías?
Explorando alternativas a los cierres
Hasta ahora, has aprendido que los cierres de Python pueden ayudar a resolver varios problemas. Sin embargo, puede ser un desafío comprender cómo funcionan bajo el capó, por lo que usar una herramienta alternativa puede hacer que sea más fácil razonar sobre su código.
Puede reemplazar un cierre con una clase que produzca instancias invocables implementando el método especial .__call__()
. Las instancias invocables son objetos que puedes llamar como llamarías a una función.
Nota: Para obtener más información sobre las instancias invocables, consulte el método .__call__()
de Python: creación de instancias invocables.
Para ilustrar, volvamos a la función de fábrica make_root_calculator()
:
>>> def make_root_calculator(root_degree, precision=2):
... def root_calculator(number):
... return round(pow(number, 1 / root_degree), precision)
... return root_calculator
...
>>> square_root = make_root_calculator(2, 4)
>>> square_root(42)
6.4807
>>> cubic_root = make_root_calculator(3)
>>> cubic_root(42)
3.48
La función devuelve cierres que conservan los argumentos root_grade
y precision
en su alcance extendido. Puede reemplazar esta función de fábrica con la siguiente clase:
class RootCalculator:
def __init__(self, root_degree, precision=2):
self.root_degree = root_degree
self.precision = precision
def __call__(self, number):
return round(pow(number, 1 / self.root_degree), self.precision)
Esta clase toma los mismos dos argumentos que make_root_calculator()
y los convierte en atributos de instancia.
Al proporcionar el método .__call__()
, transformas tus instancias de clase en objetos invocables que puedes llamar como funciones regulares. A continuación se explica cómo puede utilizar esta clase para crear objetos similares a funciones de calculadora raíz:
>>> from roots import RootCalculator
>>> square_root = RootCalculator(2, 4)
>>> square_root(42)
6.4807
>>> cubic_root = RootCalculator(3)
>>> cubic_root(42)
3.48
>>> cubic_root.root_degree
3
Como puede concluir, la función RootCalculator
funciona prácticamente igual que la función make_root_calculator()
. Como ventaja, ahora tienes acceso a argumentos de configuración como
Conclusión
Ahora sabes que un cierre es un objeto de función típicamente definido dentro de otra función en Python. Los cierres toman los objetos definidos en su alcance adjunto y los combinan con el objeto de función interna para crear un objeto invocable con un alcance extendido.
Puede usar cierres en múltiples escenarios, especialmente cuando necesita retener el estado entre llamadas a funciones consecutivas o escribir un decorador. Entonces, saber cómo usar cierres puede ser una habilidad excelente para un desarrollador de Python.
En este tutorial, has aprendido:
- Qué son los cierres y cómo funcionan en Python
- Cuándo pueden utilizarse cierres en la práctica
- Cómo las instancias invocables pueden reemplazar los cierres
Con este conocimiento, puedes empezar a crear y utilizar cierres de Python en tu código, especialmente si estás interesado en herramientas de programación funcionales.