Extraer función consiste en tomar un bloque de código, moverlo a una función nueva y reemplazar el bloque original por una llamada con un nombre claro. Es una técnica central del refactoring porque permite separar responsabilidades, reducir duplicación y hacer visible la intención.
En este tema practicaremos cómo elegir qué bloque extraer, qué parámetros pasar, qué valor devolver y cómo verificar que el comportamiento se mantiene. Trabajaremos con ejemplos en Python y ejecutaremos pruebas después de cada paso importante.
Extraer una función es útil cuando un bloque tiene una intención reconocible. No se trata de cortar código por cantidad de líneas, sino de darle nombre a una idea del dominio o a una operación que se repite.
Algunas señales frecuentes son:
Crea el archivo src/reportes.py con este código:
def generar_resumen_ventas(ventas):
total = 0
cantidad = 0
ventas_validas = []
for venta in ventas:
if venta["estado"] == "confirmada":
importe = venta["precio"] * venta["cantidad"]
if venta["cliente"] == "vip":
importe = importe * 0.9
total = total + importe
cantidad = cantidad + 1
ventas_validas.append(venta)
if cantidad == 0:
promedio = 0
else:
promedio = total / cantidad
if total >= 100000:
categoria = "alta"
elif total >= 50000:
categoria = "media"
else:
categoria = "baja"
texto = "Ventas confirmadas: " + str(cantidad)
texto = texto + " | Total: " + str(round(total, 2))
texto = texto + " | Promedio: " + str(round(promedio, 2))
texto = texto + " | Categoría: " + categoria
return {
"cantidad": cantidad,
"total": round(total, 2),
"promedio": round(promedio, 2),
"categoria": categoria,
"texto": texto,
}
La función calcula importes, filtra ventas, aplica descuento, calcula promedio, clasifica el total y arma texto. Hay varias responsabilidades en un solo lugar.
Antes de extraer funciones, escribimos pruebas. Crea tests/test_reportes.py:
from reportes import generar_resumen_ventas
def test_genera_resumen_con_ventas_confirmadas_y_descuento_vip():
ventas = [
{"estado": "confirmada", "precio": 30000, "cantidad": 2, "cliente": "vip"},
{"estado": "pendiente", "precio": 10000, "cantidad": 1, "cliente": "regular"},
{"estado": "confirmada", "precio": 20000, "cantidad": 1, "cliente": "regular"},
]
assert generar_resumen_ventas(ventas) == {
"cantidad": 2,
"total": 74000.0,
"promedio": 37000.0,
"categoria": "media",
"texto": "Ventas confirmadas: 2 | Total: 74000.0 | Promedio: 37000.0 | Categoría: media",
}
def test_genera_resumen_sin_ventas_confirmadas():
ventas = [
{"estado": "pendiente", "precio": 10000, "cantidad": 1, "cliente": "regular"},
]
assert generar_resumen_ventas(ventas) == {
"cantidad": 0,
"total": 0,
"promedio": 0,
"categoria": "baja",
"texto": "Ventas confirmadas: 0 | Total: 0 | Promedio: 0 | Categoría: baja",
}
Ejecuta:
python -m pytest tests/test_reportes.py
El cálculo del importe de una venta tiene intención propia. Hoy está mezclado dentro del bucle:
importe = venta["precio"] * venta["cantidad"]
if venta["cliente"] == "vip":
importe = importe * 0.9
Podemos extraerlo a una función llamada calcular_importe_venta.
Agregamos la función nueva y reemplazamos el bloque original por una llamada:
def calcular_importe_venta(venta):
importe = venta["precio"] * venta["cantidad"]
if venta["cliente"] == "vip":
importe = importe * 0.9
return importe
def generar_resumen_ventas(ventas):
total = 0
cantidad = 0
ventas_validas = []
for venta in ventas:
if venta["estado"] == "confirmada":
importe = calcular_importe_venta(venta)
total = total + importe
cantidad = cantidad + 1
ventas_validas.append(venta)
if cantidad == 0:
promedio = 0
else:
promedio = total / cantidad
if total >= 100000:
categoria = "alta"
elif total >= 50000:
categoria = "media"
else:
categoria = "baja"
texto = "Ventas confirmadas: " + str(cantidad)
texto = texto + " | Total: " + str(round(total, 2))
texto = texto + " | Promedio: " + str(round(promedio, 2))
texto = texto + " | Categoría: " + categoria
return {
"cantidad": cantidad,
"total": round(total, 2),
"promedio": round(promedio, 2),
"categoria": categoria,
"texto": texto,
}
Después de este paso ejecuta python -m pytest tests/test_reportes.py.
La función extraída recibe venta completa. En este caso es aceptable porque el cálculo depende de varios campos de esa venta. Otra opción sería pasar precio, cantidad y cliente, pero la llamada quedaría más larga.
La decisión depende de la claridad. Si una función recibe demasiados datos, tal vez el bloque extraído todavía no tiene límites claros. Si recibe un objeto completo pero solo usa un campo, quizá conviene pasar ese campo directamente.
El cálculo del promedio tiene una regla especial cuando la cantidad es cero. Podemos darle nombre:
def calcular_promedio(total, cantidad):
if cantidad == 0:
return 0
return total / cantidad
Y reemplazar el bloque original:
promedio = calcular_promedio(total, cantidad)
Ejecuta las pruebas. Este cambio es pequeño y debería conservar los mismos resultados.
La clasificación del total también es una regla separada:
def obtener_categoria_ventas(total):
if total >= 100000:
return "alta"
if total >= 50000:
return "media"
return "baja"
La función principal queda con una línea:
categoria = obtener_categoria_ventas(total)
Además de reducir líneas, el nombre explica qué representa la decisión.
El armado de texto no debería mezclarse con el cálculo del resumen. Podemos extraerlo:
def crear_texto_resumen(cantidad, total, promedio, categoria):
texto = "Ventas confirmadas: " + str(cantidad)
texto = texto + " | Total: " + str(round(total, 2))
texto = texto + " | Promedio: " + str(round(promedio, 2))
texto = texto + " | Categoría: " + categoria
return texto
Y reemplazarlo por:
texto = crear_texto_resumen(cantidad, total, promedio, categoria)
Luego ejecuta python -m pytest.
Después de aplicar los pasos anteriores, el archivo puede quedar así:
def calcular_importe_venta(venta):
importe = venta["precio"] * venta["cantidad"]
if venta["cliente"] == "vip":
importe = importe * 0.9
return importe
def calcular_promedio(total, cantidad):
if cantidad == 0:
return 0
return total / cantidad
def obtener_categoria_ventas(total):
if total >= 100000:
return "alta"
if total >= 50000:
return "media"
return "baja"
def crear_texto_resumen(cantidad, total, promedio, categoria):
texto = "Ventas confirmadas: " + str(cantidad)
texto = texto + " | Total: " + str(round(total, 2))
texto = texto + " | Promedio: " + str(round(promedio, 2))
texto = texto + " | Categoría: " + categoria
return texto
def generar_resumen_ventas(ventas):
total = 0
cantidad = 0
for venta in ventas:
if venta["estado"] == "confirmada":
total = total + calcular_importe_venta(venta)
cantidad = cantidad + 1
promedio = calcular_promedio(total, cantidad)
categoria = obtener_categoria_ventas(total)
texto = crear_texto_resumen(cantidad, total, promedio, categoria)
return {
"cantidad": cantidad,
"total": round(total, 2),
"promedio": round(promedio, 2),
"categoria": categoria,
"texto": texto,
}
La función principal ahora muestra el flujo general y las funciones auxiliares explican las reglas específicas.
En el código inicial existía ventas_validas, pero no se usaba para construir el resultado. Al extraer funciones y ejecutar pruebas podemos detectar este tipo de variables sobrantes.
Eliminar una variable no usada también es refactoring si no cambia el comportamiento. Herramientas como ruff suelen ayudar a encontrar estos casos.
ruff check .
La extracción también sirve cuando hay duplicación. Observa este ejemplo:
def calcular_total_web(items):
total = 0
for item in items:
total = total + item["precio"] * item["cantidad"]
return total * 1.21
def calcular_total_sucursal(items):
total = 0
for item in items:
total = total + item["precio"] * item["cantidad"]
return total
El cálculo del subtotal está repetido. Podemos extraerlo:
def calcular_subtotal(items):
total = 0
for item in items:
total = total + item["precio"] * item["cantidad"]
return total
def calcular_total_web(items):
return calcular_subtotal(items) * 1.21
def calcular_total_sucursal(items):
return calcular_subtotal(items)
No todo bloque merece una función nueva. Extraer por extraer puede producir código fragmentado, donde hay que saltar entre demasiadas funciones para entender una idea simple.
Evita extraer cuando:
Crea el archivo src/inscripciones.py:
def generar_confirmacion(inscripcion):
total = inscripcion["curso"]["precio"]
if inscripcion["alumno"]["tipo"] == "becado":
total = total * 0.5
if inscripcion["alumno"]["tipo"] == "egresado":
total = total * 0.8
if inscripcion["curso"]["modalidad"] == "presencial":
total = total + 3000
if total == 0:
estado = "sin cargo"
elif inscripcion["pagado"] >= total:
estado = "pagado"
else:
estado = "pendiente"
mensaje = "Alumno: " + inscripcion["alumno"]["nombre"]
mensaje = mensaje + " | Curso: " + inscripcion["curso"]["nombre"]
mensaje = mensaje + " | Total: " + str(round(total, 2))
mensaje = mensaje + " | Estado: " + estado
return {"total": round(total, 2), "estado": estado, "mensaje": mensaje}
Realiza estas tareas:
python -m pytest después de cada extracción.Una versión refactorizada podría ser:
def calcular_total_inscripcion(inscripcion):
total = inscripcion["curso"]["precio"]
tipo_alumno = inscripcion["alumno"]["tipo"]
if tipo_alumno == "becado":
total = total * 0.5
if tipo_alumno == "egresado":
total = total * 0.8
if inscripcion["curso"]["modalidad"] == "presencial":
total = total + 3000
return total
def determinar_estado(total, pagado):
if total == 0:
return "sin cargo"
if pagado >= total:
return "pagado"
return "pendiente"
def crear_mensaje_confirmacion(inscripcion, total, estado):
mensaje = "Alumno: " + inscripcion["alumno"]["nombre"]
mensaje = mensaje + " | Curso: " + inscripcion["curso"]["nombre"]
mensaje = mensaje + " | Total: " + str(round(total, 2))
mensaje = mensaje + " | Estado: " + estado
return mensaje
def generar_confirmacion(inscripcion):
total = calcular_total_inscripcion(inscripcion)
estado = determinar_estado(total, inscripcion["pagado"])
mensaje = crear_mensaje_confirmacion(inscripcion, total, estado)
return {"total": round(total, 2), "estado": estado, "mensaje": mensaje}
La función principal queda más breve, pero sigue mostrando el flujo completo de la operación.
Antes de continuar, verifica que puedes explicar estos puntos:
En este tema practicamos la extracción de funciones a partir de bloques largos o repetidos. Vimos que el objetivo no es crear muchas funciones, sino nombrar ideas importantes y separar responsabilidades sin modificar el comportamiento.
En el próximo tema veremos el caso inverso: aplicar inline de variables y funciones cuando una abstracción sobra.