Aprende más sobre las clausuras (closures)

PythonPythonBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este laboratorio, aprenderás más sobre las clausuras (closures) en Python. Las clausuras son un concepto de programación poderoso que permite a las funciones recordar y acceder a variables de su ámbito envolvente, incluso después de que la función externa haya terminado su ejecución.

También entenderás las clausuras como una estructura de datos, las explorarás como un generador de código y descubrirás cómo implementar la comprobación de tipos (type - checking) con clausuras. Este laboratorio te ayudará a descubrir algunos de los aspectos más inusuales y poderosos de las clausuras en Python.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/FunctionsGroup -.-> python/scope("Scope") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("Raising Exceptions") subgraph Lab Skills python/conditional_statements -.-> lab-132506{{"Aprende más sobre las clausuras (closures)"}} python/function_definition -.-> lab-132506{{"Aprende más sobre las clausuras (closures)"}} python/arguments_return -.-> lab-132506{{"Aprende más sobre las clausuras (closures)"}} python/scope -.-> lab-132506{{"Aprende más sobre las clausuras (closures)"}} python/classes_objects -.-> lab-132506{{"Aprende más sobre las clausuras (closures)"}} python/raising_exceptions -.-> lab-132506{{"Aprende más sobre las clausuras (closures)"}} end

Clausuras como una Estructura de Datos

En Python, las clausuras (closures) ofrecen una forma poderosa de encapsular datos. El encapsulamiento significa mantener los datos privados y controlar el acceso a ellos. Con las clausuras, puedes crear funciones que administren y modifiquen datos privados sin tener que usar clases o variables globales. Las variables globales se pueden acceder y modificar desde cualquier parte de tu código, lo que puede llevar a un comportamiento inesperado. Las clases, por otro lado, requieren una estructura más compleja. Las clausuras proporcionan una alternativa más simple para el encapsulamiento de datos.

Vamos a crear un archivo llamado counter.py para demostrar este concepto:

  1. Abre el WebIDE y crea un nuevo archivo llamado counter.py en el directorio /home/labex/project. Aquí es donde escribiremos el código que define nuestro contador basado en clausuras.

  2. Agrega el siguiente código al archivo:

def counter(value):
    """
    Create a counter with increment and decrement functions.

    Args:
        value: Initial value of the counter

    Returns:
        Two functions: one to increment the counter, one to decrement it
    """
    def incr():
        nonlocal value
        value += 1
        return value

    def decr():
        nonlocal value
        value -= 1
        return value

    return incr, decr

En este código, definimos una función llamada counter(). Esta función toma un valor inicial como argumento. Dentro de la función counter(), definimos dos funciones internas: incr() y decr(). Estas funciones internas comparten el acceso a la misma variable valor. La palabra clave nonlocal se utiliza para decirle a Python que queremos modificar la variable valor del ámbito envolvente (la función counter()). Sin la palabra clave nonlocal, Python crearía una nueva variable local dentro de las funciones internas en lugar de modificar el valor del ámbito externo.

  1. Ahora, vamos a crear un archivo de prueba para ver esto en acción. Crea un nuevo archivo llamado test_counter.py con el siguiente contenido:
from counter import counter

## Create a counter starting at 0
up, down = counter(0)

## Increment the counter several times
print("Incrementing the counter:")
print(up())  ## Should print 1
print(up())  ## Should print 2
print(up())  ## Should print 3

## Decrement the counter
print("\nDecrementing the counter:")
print(down())  ## Should print 2
print(down())  ## Should print 1

En este archivo de prueba, primero importamos la función counter() del archivo counter.py. Luego, creamos un contador que comienza en 0 llamando a counter(0) y desempaquetando las funciones devueltas en up y down. Luego llamamos a la función up() varias veces para incrementar el contador e imprimimos los resultados. Después de eso, llamamos a la función down() para decrementar el contador e imprimimos los resultados.

  1. Ejecuta el archivo de prueba ejecutando el siguiente comando en la terminal:
python3 test_counter.py

Deberías ver la siguiente salida:

Incrementing the counter:
1
2
3

Decrementing the counter:
2
1

Observa cómo aquí no está involucrada ninguna definición de clase. Las funciones up() y down() están manipulando un valor compartido que no es una variable global ni un atributo de instancia. Este valor se almacena en la clausura, lo que lo hace accesible solo para las funciones devueltas por counter().

Este es un ejemplo de cómo las clausuras se pueden usar como una estructura de datos. La variable encerrada valor se mantiene entre llamadas a funciones y es privada para las funciones que la acceden. Esto significa que ninguna otra parte de tu código puede acceder o modificar directamente esta variable valor, lo que proporciona un nivel de protección de datos.

Clausuras como un Generador de Código

En este paso, aprenderemos cómo se pueden utilizar las clausuras (closures) para generar código de forma dinámica. En concreto, construiremos un sistema de comprobación de tipos (type-checking) para atributos de clase utilizando clausuras.

Primero, entendamos qué son las clausuras. Una clausura es un objeto función que recuerda los valores en el ámbito envolvente incluso si no están presentes en la memoria. En Python, las clausuras se crean cuando una función anidada hace referencia a un valor de su función envolvente.

Ahora, comenzaremos a implementar nuestro sistema de comprobación de tipos.

  1. Crea un nuevo archivo llamado typedproperty.py en el directorio /home/labex/project con el siguiente código:
## typedproperty.py

def typedproperty(name, expected_type):
    """
    Create a property with type checking.

    Args:
        name: The name of the property
        expected_type: The expected type of the property value

    Returns:
        A property object that performs type checking
    """
    private_name = '_' + name

    @property
    def value(self):
        return getattr(self, private_name)

    @value.setter
    def value(self, val):
        if not isinstance(val, expected_type):
            raise TypeError(f'Expected {expected_type}')
        setattr(self, private_name, val)

    return value

En este código, la función typedproperty es una clausura. Toma dos argumentos: name y expected_type. El decorador @property se utiliza para crear un método getter para la propiedad, que recupera el valor del atributo privado. El decorador @value.setter crea un método setter que comprueba si el valor que se está estableciendo es del tipo esperado. Si no lo es, levanta una excepción TypeError.

  1. Ahora, creemos una clase que utilice estas propiedades con comprobación de tipos. Crea un archivo llamado stock.py con el siguiente código:
from typedproperty import typedproperty

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = typedproperty('name', str)
    shares = typedproperty('shares', int)
    price = typedproperty('price', float)

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

En la clase Stock, utilizamos la función typedproperty para crear atributos con comprobación de tipos para name, shares y price. Cuando creamos una instancia de la clase Stock, la comprobación de tipos se aplicará automáticamente.

  1. Creemos un archivo de prueba para ver esto en acción. Crea un archivo llamado test_stock.py con el siguiente código:
from stock import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.shares = "hundred"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

En este archivo de prueba, primero creamos un objeto Stock con los tipos correctos. Luego intentamos establecer el atributo shares a una cadena, lo que debería levantar una excepción TypeError porque el tipo esperado es un entero.

  1. Ejecuta el archivo de prueba:
python3 test_stock.py

Deberías ver una salida similar a:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'int'>

Esta salida muestra que la comprobación de tipos está funcionando correctamente.

  1. Ahora, mejoraremos typedproperty.py agregando funciones de conveniencia para tipos comunes. Agrega el siguiente código al final del archivo:
def String(name):
    """Create a string property with type checking."""
    return typedproperty(name, str)

def Integer(name):
    """Create an integer property with type checking."""
    return typedproperty(name, int)

def Float(name):
    """Create a float property with type checking."""
    return typedproperty(name, float)

Estas funciones son simplemente envoltorios (wrappers) alrededor de la función typedproperty, lo que facilita la creación de propiedades de tipos comunes.

  1. Crea un nuevo archivo llamado stock_enhanced.py que utilice estas funciones de conveniencia:
from typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = String('name')
    shares = Integer('shares')
    price = Float('price')

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Esta clase Stock utiliza las funciones de conveniencia para crear atributos con comprobación de tipos, lo que hace que el código sea más legible.

  1. Crea un archivo de prueba test_stock_enhanced.py para probar la versión mejorada:
from stock_enhanced import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.price = "490.1"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

Este archivo de prueba es similar al anterior, pero prueba la clase Stock mejorada.

  1. Ejecuta la prueba:
python3 test_stock_enhanced.py

Deberías ver una salida similar a:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'float'>

En este paso, hemos demostrado cómo se pueden utilizar las clausuras para generar código. La función typedproperty crea objetos de propiedad que realizan comprobación de tipos, y las funciones String, Integer y Float crean propiedades especializadas para tipos comunes.

✨ Revisar Solución y Practicar

Eliminando Nombres de Propiedades con Descriptores

En el paso anterior, al crear propiedades tipadas, tuvimos que declarar explícitamente los nombres de las propiedades. Esto es redundante porque los nombres de las propiedades ya se especifican en la definición de la clase. En este paso, usaremos descriptores para eliminar esta redundancia.

Un descriptor en Python es un objeto especial que controla cómo funciona el acceso a los atributos. Cuando se implementa el método __set_name__ en un descriptor, este puede capturar automáticamente el nombre del atributo de la definición de la clase.

Comencemos creando un nuevo archivo.

  1. Crea un nuevo archivo llamado improved_typedproperty.py con el siguiente código:
## improved_typedproperty.py

class TypedProperty:
    """
    A descriptor that performs type checking.

    This descriptor automatically captures the attribute name from the class definition.
    """
    def __init__(self, expected_type):
        self.expected_type = expected_type
        self.name = None

    def __set_name__(self, owner, name):
        ## This method is called when the descriptor is assigned to a class attribute
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f'Expected {self.expected_type}')
        instance.__dict__[self.name] = value

## Convenience functions
def String():
    """Create a string property with type checking."""
    return TypedProperty(str)

def Integer():
    """Create an integer property with type checking."""
    return TypedProperty(int)

def Float():
    """Create a float property with type checking."""
    return TypedProperty(float)

Este código define una clase descriptor llamada TypedProperty que comprueba el tipo de los valores asignados a los atributos. El método __set_name__ se llama automáticamente cuando el descriptor se asigna a un atributo de clase. Esto permite que el descriptor capture el nombre del atributo sin que tengamos que especificarlo manualmente.

A continuación, crearemos una clase que utilice estas propiedades tipadas mejoradas.

  1. Crea un nuevo archivo llamado stock_improved.py que utilice las propiedades tipadas mejoradas:
from improved_typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    ## No need to specify property names anymore
    name = String()
    shares = Integer()
    price = Float()

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Observa que no es necesario especificar los nombres de las propiedades al crear las propiedades tipadas. El descriptor obtendrá automáticamente el nombre del atributo de la definición de la clase.

Ahora, probemos nuestra clase mejorada.

  1. Crea un archivo de prueba test_stock_improved.py para probar la versión mejorada:
from stock_improved import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try setting attributes with wrong types
try:
    s.name = 123  ## Should raise TypeError
    print("Name type check failed")
except TypeError as e:
    print(f"Name type check succeeded: {e}")

try:
    s.shares = "hundred"  ## Should raise TypeError
    print("Shares type check failed")
except TypeError as e:
    print(f"Shares type check succeeded: {e}")

try:
    s.price = "490.1"  ## Should raise TypeError
    print("Price type check failed")
except TypeError as e:
    print(f"Price type check succeeded: {e}")

Finalmente, ejecutaremos la prueba para ver si todo funciona como se espera.

  1. Ejecuta la prueba:
python3 test_stock_improved.py

Deberías ver una salida similar a:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Name type check succeeded: Expected <class 'str'>
Shares type check succeeded: Expected <class 'int'>
Price type check succeeded: Expected <class 'float'>

En este paso, hemos mejorado nuestro sistema de comprobación de tipos utilizando descriptores y el método __set_name__. Esto elimina la especificación redundante del nombre de la propiedad, lo que hace que el código sea más corto y menos propenso a errores.

El método __set_name__ es una característica muy útil de los descriptores. Permite que recopilen automáticamente información sobre cómo se utilizan en una definición de clase. Esto se puede utilizar para crear APIs más fáciles de entender y usar.

Resumen

En este laboratorio (lab), has aprendido aspectos avanzados de las clausuras (closures) en Python. En primer lugar, has explorado el uso de las clausuras como una estructura de datos, que puede encapsular datos y permitir que las funciones mantengan un estado entre llamadas sin depender de clases o variables globales. En segundo lugar, has visto cómo las clausuras pueden actuar como un generador de código, generando objetos de propiedad con comprobación de tipos para un enfoque más funcional de la validación de atributos.

También has descubierto cómo utilizar el protocolo de descriptores y el método __set_name__ para crear atributos de comprobación de tipos elegantes que capturan automáticamente sus nombres de las definiciones de clase. Estas técnicas demuestran el poder y la flexibilidad de las clausuras, lo que te permite implementar comportamientos complejos de manera concisa. Comprender las clausuras y los descriptores te brinda más herramientas para crear código Python mantenible y robusto.