Las funciones anidadas pueden acceder a variables de su función contenedora (el ámbito encerrador). Ahora, estamos listos para explorar un concepto aún más profundo y poderoso que emerge de esta capacidad: los closures o clausuras.
Un closure (clausura) en Python es una función anidada (interna) que "recuerda" y puede acceder a las variables de su ámbito encerrador (externo), incluso después de que la función externa ha terminado de ejecutarse.
Puedes imaginarlo como si la función anidada encapsulara el entorno en el que fue definida. Cuando la función externa finaliza y sus variables locales normalmente desaparecerían, la función interna que es un closure mantiene una referencia a esas variables, permitiéndole utilizarlas en cualquier momento en el futuro.
Para que se forme un closure, se deben cumplir tres condiciones:
- Debe haber una función anidada: Una función definida dentro de otra función.
- La función anidada debe hacer referencia a una variable del ámbito encerrador: Es decir, debe usar al menos una variable definida en la función externa.
- La función externa debe retornar la función anidada: En lugar de retornar un valor directamente, la función externa devuelve la definición de su función interna.
def creador_saludo(idioma): if idioma == "es": prefijo = "¡Hola" elif idioma == "en": prefijo = "Hello" else: prefijo = "Saludos" def generar_saludo(nombre): # Funcion anidada # 'prefijo' y 'nombre' son del ámbito encerrador y local, respectivamente return f"{prefijo}, {nombre}!" # Accede a 'prefijo' del ámbito E return generar_saludo # ¡Devolvemos la función anidada! # Creamos una función anidada configurada para español saludar_espanol = creador_saludo("es") # La función 'creador_saludo' ya terminó de ejecutarse, # pero 'saludar_espanol' (la función interna) "recuerda" que 'prefijo' era "¡Hola" print(saludar_espanol("Maria")) # Salida: ¡Hola, Maria! # Creamos otra función anidada configurada para inglés saludar_ingles = creador_saludo("en") print(saludar_ingles("John")) # Salida: Hello, John!
En este ejemplo, generar_saludo accede a prefijo (que está en el ámbito externo). Lo notable es que saludar_espanol y saludar_ingles son diferentes instancias de la función interna, cada una "recordando" el valor de prefijo de la ejecución de creador_saludo que la creó.
# Otro ejemplo básico de Closure def hacer_multiplicador(factor): # 'factor' es una variable del ámbito encerrador para 'multiplicar_por_factor' def multiplicar_por_factor(numero): # Función anidada # Accede a 'factor' del ámbito encerrador return numero * factor return multiplicar_por_factor # La función externa devuelve la función anidada (closure) # Creamos dos closures diferentes duplicador = hacer_multiplicador(2) # 'duplicador' es un closure que "recuerda" factor = 2 triplicador = hacer_multiplicador(3) # 'triplicador' es un closure que "recuerda" factor = 3 print(f"Duplicar 5: {duplicador(5)}") # Salida: Duplicar 5: 10 print(f"Triplicar 5: {triplicador(5)}") # Salida: Triplicar 5: 15 # Observa que 'hacer_multiplicador' ya terminó de ejecutarse, # pero 'duplicador' y 'triplicador' siguen usando su 'factor' recordado.
En este ejemplo, duplicador y triplicador son closures. Aunque hacer_multiplicador ya ha concluido, duplicador aún "recuerda" que su factor es 2, y triplicador recuerda que su factor es 3.
def crear_contador(inicio=0): contador = inicio # variable libre que la función interna "recuerda" def incrementar(): nonlocal contador # para modificar la variable del closure contador += 1 return contador return incrementar # Crear dos contadores independientes contador_a = crear_contador(10) contador_b = crear_contador(100) print(contador_a()) # 11 print(contador_a()) # 12 print(contador_b()) # 101 print(contador_a()) # 13 print(contador_b()) # 102
La palabra clave 'nonlocal' es obligatoria en Python, con esto le decimos que queremos acceder a la variable 'contador' definido en la función crear_contador, es decir en la función externa.
La función incrementar recuerda el estado de la variable contador incluso después de que crear_contador haya terminado.
Cada llamada a crear_contador genera un entorno independiente, por eso contador_a y contador_b no se mezclan.
Esto permite simular objetos con estado sin usar clases.
Veamos otro ejemplo práctico usando closures para manejar un sistema de puntuación en un juego.
def crear_sistema_puntuacion(): puntuacion = 0 historial = [] def sistema(accion, valor=0): nonlocal puntuacion, historial if accion == "sumar": puntuacion += valor historial.append(("sumar", valor)) elif accion == "restar": puntuacion -= valor historial.append(("restar", valor)) elif accion == "mostrar": return puntuacion elif accion == "historial": return historial else: return "Acción no válida" return puntuacion return sistema # Crear el sistema puntos = crear_sistema_puntuacion() print(puntos("sumar", 10)) # 10 print(puntos("sumar", 20)) # 30 print(puntos("restar", 5)) # 25 print("Puntuación actual:", puntos("mostrar")) # 25 print("Historial:", puntos("historial")) # [('sumar', 10), ('sumar', 20), ('restar', 5)]
El closure mantiene el estado de la puntuación y el historial sin usar clases.
Puede crear múltiples sistemas de puntuación independientes para distintos jugadores.
Es escalable: podrías añadir multiplicadores, combos, bonus sin cambiar la idea base.