U2 - Programación funcional
1 Trasfromando funciones
# 1
# * Usa variable global
# * Modifica variable global
# * El resultado no depende de los valores de entrada
def incrementar(contador):
return contador + 1
# 2
# * El resultado no depende de la entrada, sino del momento de la llamada.
# Se puede obtener una función similar, pero no exactamente igual.
import datetime
def hora(dt):
return dt.hour
hora(datetime.datetime.now())
# 3
# Acá no estoy tan seguro, supongo que lo que es no puro no es `add_time``, sino que es
# `increment_time`. Yo (tomi) propondría
def add_time(time, hours, minutes, seconds):
return time + datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds)
add_time(datetime.datetime(2025, 8, 12), 2, 12, 44)
# 4
# Modifica objeto global
# Salida sí depende de la entrada, eso está OK.
# Una alternativa posible
def registar_nombre(listado, nombre):
return listado + [nombre]
historial_de_nombres = []
historial_de_nombres = registar_nombre(historial_de_nombres, "Mateo")
historial_de_nombres = registar_nombre(historial_de_nombres, "Camila")
historial_de_nombres = registar_nombre(historial_de_nombres, "Victoria")
historial_de_nombres
# 5
# No es pura, no signific que sea mala per se.
# El problema es que su salida no depende de sus valores
# Si cambia una variable fuera de ella, cammbia el resultado.
# Una alternativa
def verificar_limite(valor, limite):
if valor > limite:
return "Excede el límite"
return "Dentro del límite"
LIMITE_MAXIMO = 100
verificar_limite(88, LIMITE_MAXIMO)2 Fábrica de promociones
# Parte 1:
def crear_promocion(medio):
if medio == "efectivo":
multiplicador = 1
elif medio == "débito":
multiplicador = (1 - 0.1)
elif medio == "crédito":
multiplicador = (1 + 0.05)
else:
print(f"El medio {medio} es desconocido.")
multiplicador = 1
def f(x):
return x * multiplicador
return f
promo_debito = crear_promocion("débito")
print(promo_debito(1000))
print(promo_debito(2700))
# Parte 2
def crear_promocion_personalizada(medio, descuento=None):
if descuento is None:
if medio == "efectivo":
multiplicador = 1
elif medio == "débito":
multiplicador = (1 - 0.1)
elif medio == "crédito":
multiplicador = (1 + 0.05)
else:
print(f"El medio {medio} es desconocido.")
multiplicador = 1
else:
multiplicador = 1 - descuento / 100
def f(x):
return x * multiplicador
return f
promo_debito = crear_promocion_personalizada("débito", 15)
promo_debito(1000)
promo_debito = crear_promocion_personalizada("débito")
promo_debito(1000)3 Bendita media
def mean(valores, *args):
# Asume que 'valores' es coleccion de numeros
if not args:
return sum(valores) / len(valores)
# Asume que 'valores' es un solo numero
x = [valores] + list(args)
return sum(x) / len(x)
mean([6.27, 8.11, 7.6, 5.2, 4.8])
mean(7.3, 8.2, 11.0, 12.5)4 Sucesión de Fibonacci
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
for i in range(15):
print(fibonacci(i))5 Palíndromos recursivos
# Parte 2
def palindromo(x):
if len(x) < 2:
return True
if x[0] == x[-1]:
return palindromo(x[1:-1])
return False
palindromo("hola amigo")
palindromo("somos")
palindromo("anilina")
palindromo("menem")
palindromo("neuquen")
# Parte 2
def _palindromo(x):
# Identica a la anterior, pero ahora es una funcion interna
if len(x) < 2:
return True
if x[0] == x[-1]:
return palindromo(x[1:-1])
return False
def palindromo(x):
return _palindromo(x.lower().replace(" ", ""))
palindromo("Anita lava la tina")
palindromo("Luz azul")
palindromo("Cualquier cosa")6 Área de aprendizaje
rectangulos = [
(5, 8),
(2, 2),
(9, 2),
(3, 3),
(3, 7),
(6, 3)
]
sorted(rectangulos, key=lambda r: r[0] * r[1])
# Cuidado que esto funciona, pero hace otra cosa
sorted(rectangulos)7 Socios ordenados
datos_socios = [
{"nombre": "Bautista Carrara", "edad": 22, "altura_cm": 178, "record_100m": 13.4},
{"nombre": "Valentina Lucci", "edad": 23, "altura_cm": 163, "record_100m": 14.2},
{"nombre": "Gerónimo Cuesta", "edad": 26, "altura_cm": 170, "record_100m": 14.0},
{"nombre": "Lucio Borga", "edad": 28, "altura_cm": 186, "record_100m": 13.8},
{"nombre": "Julia Spoglia", "edad": 21, "altura_cm": 163, "record_100m": 11.9},
{"nombre": "Soledad Colombo", "edad": 22, "altura_cm": 170, "record_100m": 13.5}
]
sorted(datos_socios, key=lambda datum: datum["record_100m"])
# Punto extra
def ordenar_diccionario(data, key):
if key == "nombre":
return sorted(data, key=lambda datum: datum["nombre"].split(" ")[1])
return sorted(data, key=lambda datum: datum[key])
ordenar_diccionario(datos_socios, "record_100m")
ordenar_diccionario(datos_socios, "edad")
ordenar_diccionario(datos_socios, "altura_cm")
ordenar_diccionario(datos_socios, "nombre")8 Listado de rimas
palabras_a_rimar = [
"actividad",
"bendición",
"cartelera",
"ciudad",
"escalera",
"estación",
"felicidad",
"función",
"reposera"
]
sorted(palabras_a_rimar, key=lambda c: c[::-1])9 Analistas de temperaturas
temperaturas_celsius = [
25.5, 28.0, 19.3, 31.5, 22.8, 17.0, 30.2, 35.6, 14.2,
32.4, 22.7, 10.1, 29.5, 33.9, 22.1, 38.9, 18.4, 16.3
]
# Enfoque funcional
# 1
temperaturas_f = list(map(lambda c: c * 9 / 5 + 32, temperaturas_celsius))
# 2
# Opcion A:
list(filter(lambda f: f > 80, temperaturas_f))
# Opcion B --> las devuelve en grados celsius
list(filter(lambda c: (c * 9 / 5 + 32) > 80, temperaturas_celsius))
# Enfoque idiomático
[
c * 9 / 5 + 32 for c in temperaturas_celsius if c > 22
]10 El tiempo vuela
import time
def crear_cronometro():
t_creacion = time.time()
def fun():
t_ejecucion = time.time()
return t_ejecucion - t_creacion
return fun
cronometro1 = crear_cronometro()
for i in range(10**4):
i ** 2 # Calcula el cuadrado de un número pero no lo devuelve
print(f"El bloque entero tardó {cronometro1()} segundos en ejecutarse.")
cronometro2 = crear_cronometro()
for j in range(10**6):
j // 2 # Calcula la división entera por 2 pero no la devuelve
print(f"El segundo bucle tardó {cronometro2()} segundos en ejecutarse.")
# Punto extra: ¿cache?
def crear_cronometro():
t_creacion = time.time()
tiempos = [t_creacion]
def fun():
t_ejecucion = time.time()
t_anterior = tiempos[-1]
tiempos.append(t_ejecucion)
return t_ejecucion - t_anterior
return fun
g = crear_cronometro()
g()
g()
g()11 No perdamos el centro
numeros = [
2.05, 1.09, None, 2.31, 2.28, 0.97, 2.59, 2.72, 0.76, None, 1.88, 2.04, 3.25, 1.88, None
]
numeros_no_nulos = [n for n in numeros if n is not None]
media = sum(numeros_no_nulos) / len(numeros_no_nulos)
[n - media for n in numeros if n is not None]12 En Python es mejor
numeros = [
4.74346239e-01, -2.90877176e-01, -1.44377789e+00, -4.48680759e+01,
-1.21249801e+00, -3.32729317e-01, 2.21676912e-01, 1.05599711e+00,
-3.62372053e+00, -2.96441579e-01, -4.28304222e+00, 1.55908820e+02,
9.00858234e-01, -1.09384173e+00, -1.51083571e+00, -5.38491167e-01,
-3.84153084e-02, 1.20393395e+00, 1.82651406e-01, 2.05179405e+00
]
def media(x):
return sum(x) / len(x)
def varianza(x):
numerador = 0
x_media = media(x)
for x_i in x:
numerador += (x_i - x_media) ** 2
return numerador / len(x)
x_media = media(numeros)
x_desvio = varianza(numeros) ** 0.5
map_obj = map(lambda x: (x - x_media) / x_desvio, numeros)
list(filter(lambda x: abs(x) > 3, map_obj))
# Respuesta 1: si queremos el numero transformado
[(x - x_media) / x_desvio for x in numeros if abs((x - x_media) / x_desvio) > 3]
# Respuesta 2: si queremos el numero original
[x for x in numeros if abs((x - x_media) / x_desvio) > 3]
13 Subiendo de rango
def frange(start, stop, step):
while start < stop:
yield start
start += step
for i in frange(3, 4, 0.2):
print(f"{i:.2f}")
for i in frange(3, 4, 0.15):
print(f"{i:.2f}")14 La cajita musical
import random
def cajita_musical(versos):
versos = versos[:] # Para hacer una copia
while versos: # Mientras la lista no esté vacía
# Generar un índice al azar
i = random.randint(0, len(versos) - 1)
# Sacar y devolver el valor del índice al azar
yield versos.pop(i)
versos = [
"Tengo que confesar que a veces no me gusta tu forma de ser",
"Luego te me desapareces y no entiendo muy bien por qué",
"No dices nada romántico cuando llega el atardecer",
"Te pones de un humor extraño con cada luna llena al mes",
"Pero a todo lo demás le gana lo bueno que me das",
"Sólo tenerte cerca, siento que vuelvo a empezar"
]
for verso in cajita_musical(versos):
print(verso)15 El mejor precio
def promo_dia_semana(compra):
"""Aplica un 15% de descuento si la compra se realiza un lunes o miércoles."""
if compra["dia"] in ("lunes", "miércoles"):
return 0.85
return 1
def promo_monto_grande(compra):
"""Aplica un 10% de descuento si la compra tiene un monto superior a $50.000."""
if compra["monto"] > 50_000:
return 0.9
return 1
def promo_edad(compra):
"""Aplica un 20% de descuento si el cliente tiene 65 años o más."""
if compra["edad_cliente"] >= 65:
return 0.8
return 1
promos = [promo_dia_semana, promo_monto_grande, promo_edad]
def mejor_promo(compra):
"""Ordena los descuentos de mayor a menor y aplica el mejor disponible."""
multiplicador = sorted([promo(compra) for promo in promos])[0]
return {
"monto_original": compra["monto"],
"monto_final": compra["monto"] * multiplicador,
"descuento": f"{round((1 - multiplicador) * 100)}%"
}
ejemplo_compra = {"dia": "miércoles", "edad_cliente": 42, "monto": 66420}
mejor_promo(ejemplo_compra)
# Solucion (no se si esto es lo que Joaco tenia en mente)
PROMOS = [] # mayusculas para indicar que es global, una convencion
def promo(fun):
def envoltura(*args, **kwargs):
return fun(*args, **kwargs)
PROMOS.append(envoltura)
return envoltura
@promo
def promo_dia_semana(compra):
if compra["dia"] in ("lunes", "miércoles"):
return 0.85
return 1
@promo
def promo_monto_grande(compra):
if compra["monto"] > 50_000:
return 0.9
return 1
@promo
def promo_edad(compra):
if compra["edad_cliente"] >= 65:
return 0.8
return 1
PROMOS # ahora contiene a las funciones
def mejor_promo(compra):
multiplicador = sorted([promo(compra) for promo in PROMOS])[0]
return {
"monto_original": compra["monto"],
"monto_final": compra["monto"] * multiplicador,
"descuento": f"{round((1 - multiplicador) * 100)}%"
}
ejemplo_compra = {"dia": "miércoles", "edad_cliente": 42, "monto": 66420}
mejor_promo(ejemplo_compra)
ejemplo_compra = {"dia": "jueves", "edad_cliente": 42, "monto": 28000}
mejor_promo(ejemplo_compra)
ejemplo_compra = {"dia": "jueves", "edad_cliente": 67, "monto": 28000}
mejor_promo(ejemplo_compra)16 Bromas pesadas 😱
def romper_cada(n):
def decorador(func):
if type(n) is not int or n < 1: # Chequea que "n" sea un argumento válido
return func
contador = 0 # Cuántas veces se llamó a la función
def envoltura(*args, **kwargs):
nonlocal contador
contador += 1
if contador % n == 0: # Cada n veces...
return None # ...se rompe
return func(*args, **kwargs)
return envoltura
return decorador
# Ejemplo de uso
@romper_cada(3)
def saludar(nombre):
print(f"¡Hola, {nombre}!")
saludar("Carlos") # "¡Hola, Carlos!"
saludar("María Luz") # "¡Hola, María Luz!"
saludar("Mirna") # Nada (la función devuelve None)
saludar("Diego") # "¡Hola, Diego!"17 Pipelines de procesamiento 😱
from math import log10
# Sin usar filter
def eliminar_nulos(datos):
datos_sin_nulos = []
for fila in datos:
if None not in fila.values():
datos_sin_nulos.append(fila.copy())
return datos_sin_nulos
def calcular_log(datos, variable):
datos_log = []
for fila in datos:
copia_fila = fila.copy()
valor = copia_fila[variable]
copia_fila[variable] = round(log10(valor), 3) if (valor is not None) else None
datos_log.append(copia_fila)
return datos_log
def filtrar(datos, variable, funcion):
datos_filtrados = []
for fila in datos:
if fila[variable] is not None and funcion(fila[variable]):
datos_filtrados.append(fila.copy())
return datos_filtrados
# Usando filter
def eliminar_nulos(datos):
return list(filter(lambda fila: None not in fila.values(), datos))
def filtrar(datos, variable, funcion):
return list(filter(lambda fila: fila[variable] is not None and funcion(fila[variable]), datos))
18 ¿Espacio o tiempo? ⏳
# 1 - PREPARACION DE DATOS
import random
CATEGORIAS = ("electrónica", "hogar", "accesorios", "deportes")
def generar_ventas(n=100_000, seed=None):
rng = random.Random(seed)
return [
{
"id": f"P{i+1:06d}",
"precio": round(rng.uniform(5.0, 500.0), 2),
"categoria": CATEGORIAS[i % len(CATEGORIAS)],
}
for i in range(1, n + 1)
]
ventas = generar_ventas(n=100_000, seed=1234)
# 2- Precios con IVA (21%)
# Versión A - List comprehension (ejecuta y guarda todo):
precios_con_iva_1 = [venta["precio"] * 1.21 for venta in ventas]
# Versión B - Generador (define la operación pero no la ejecuta):
precios_con_iva_2 = (venta["precio"] * 1.21 for venta in ventas)
# 3 - Filtrar ventas de electrónica
# Versión A - List comprehension:
electronica_1 = [venta for venta in ventas if venta["categoria"] == "electrónica"]
#Versión B - Generador:
electronica_2 = (venta for venta in ventas if venta["categoria"] == "electrónica")
# 4 - Análisis y comparación (tiempo y memoria)
import time
import sys
# A - Lista materializada
t1 = time.time()
total_1 = sum(venta["precio"] for venta in electronica_1)
t2 = time.time()
mem_1 = sys.getsizeof(electronica_1)
print(f"electronica_1 → total: {total_1:.2f}, tiempo: {t2 - t1:.4f}s, memoria: {mem_1} bytes")
# B - Generador (se consume al usar sum)
t3 = time.time()
total_2 = sum(venta["precio"] for venta in electronica_2)
t4 = time.time()
mem_2 = sys.getsizeof(electronica_2)
print(f"electronica_2 → total: {total_2:.2f}, tiempo: {t4 - t3:.4f}s, memoria: {mem_2} bytes")
# 5- Reutilización del objeto
# Lista: se puede usar de nuevo
print(sum(venta["precio"] for venta in electronica_1)) # OK
# Generador: ya fue consumido, no devuelve nada
print(sum(venta["precio"] for venta in electronica_2)) # 0.0
# 6 - Reflexión final (resumen corto)
# List comprehension (Versión A): usa más memoria, pero permite reutilizar los datos.
# Ideal si vas a recorrer los resultados varias veces.
# Generadores (Versión B): consumen poca memoria, pero solo pueden recorrerse una vez.
# Son más eficientes si solo necesitás un recorrido rápido.
# En problemas donde el dataset es enorme o se usa solo una vez → mejor un generador.
# Si necesitás acceder varias veces al resultado o compartirlo entre funciones → mejor una lista.