Constantes y funciones puras en Python: como hacerlo bien
Bajo el paradigma de la programación funcional, las funciones deberían ser puras: el mismo input produce siempre el mismo output, sin efectos secundarios. Pero en ocasiones la teoría puede chocar contra la realidad. Cuando usas constantes empiezas a depender del scope. Hay variables externas a la función. Las constantes, por definición, no rompen la pureza ya que son... constantes. El resultado es predecible. Sin embargo dificultamos el testing, ya que debemos importarlas o hacer mocks, el código empieza a ser menos legible, se vuelve menos portable y además añadimos un elemento de fragilidad ya que no podemos controlar su valor o la localización de esos valores externos.
Un caso de lo mencionado sería:
IMPUESTO = 0.21
def calcular_total(precio: float) -> float:
return precio + (precio * IMPUESTO)
¿Qué pasa si el impuesto cambia entre países? ¿Y si donde llamo calcular_total ya tiene su propio IMPUESTO?
Te voy a dar algunas soluciones.
Inyección de dependencias con functools.partial
Puedes usar la aplicación parcial con functools.partial. Las funciones base reciben todo por parámetro y luego "fijamos" las constantes para crear las versiones definitivas.
from functools import partial
def calcular_total(impuesto: float, precio: float) -> float:
return precio + (precio * impuesto)
def aplicar_descuento(descuento: float, precio: float) -> float:
return precio - descuento
IMPUESTO = 0.21
DESCUENTO_BASE = 5.0
calcular_total_espania = partial(calcular_total, IMPUESTO)
aplicar_descuento_fijo = partial(aplicar_descuento, DESCUENTO_BASE)
print(calcular_total_espania(100)) # 121.0
Aquí ganamos máxima pureza. Las funciones base son cien por cien reutilizables, fáciles de testear (solo pasas configuraciones distintas por argumento) y están aisladas del entorno.
El coste es algo de complejidad visual si tu equipo no está habituado a partial.
Clausuras o closures
Otra forma es encapsular las funciones en una función constructora, una especie de factory.
def crear_sistema_precios(impuesto: float, descuento: float):
def calcular_total(precio: float) -> float:
return precio + (precio * impuesto)
def aplicar_descuento(precio: float) -> float:
return precio - descuento
def procesar_factura(precio: float) -> float:
return aplicar_descuento(calcular_total(precio))
return calcular_total, aplicar_descuento, procesar_factura
calcular, descontar, procesar = crear_sistema_precios(0.21, 5.0)
Las constantes quedan atrapadas en el closure, y agrupamos de forma lógica las funciones que comparten contexto sin recurrir a clases, evitando así el estado mutable.
La contrapartida es que puede dificultar la lectura si las funciones internas crecen demasiado.
Conclusiones
Python es multiparadigma, así que no estás obligado a casarte con un solo enfoque. Lo importante es ser consciente del compromiso: cuanto más hacia afuera mira una función, más cómoda es de escribir, pero más difícil de testear de forma aislada.
Este trabajo está bajo una licencia Attribution-NonCommercial-NoDerivatives 4.0 International.
Apóyame en Ko-fi
Comentarios
Todavía no hay ningún comentario.