
🐍 Programación funcional en Python
Cualquier desarrollador de Python, tarde o temprano, escucha: «intente escribir en un estilo funcional, su código será más limpio». Pero cuando abre la documentación, ve map, filter y reduce con lambdas y no entiende por dónde empezar.
El problema no es que la programación funcional sea difícil. El problema es que la mayoría de las guías o bien se adentran en la espesura académica de Haskell o se limitan a la sintaxis sin explicar el «por qué». Python, al ser un lenguaje multiparadigma, proporciona exactamente tantas herramientas funcionales como necesita para las tareas cotidianas, sin extremos.
En este material, un desglose práctico de las capacidades funcionales de Python: desde las comprensiones de listas hasta las funciones lambda, con ejemplos reales y escenarios donde cada herramienta simplifica genuinamente el código.
💡 Resumen rápido:
- Desglose de las comprensiones de listas, por qué son mejores que los bucles y cuándo usarlas
- Dominio de los generadores:
yield, ahorro de memoria y expresiones generadoras - Recorrido por
map+filter+reduce: ejemplos prácticos de sustitución de bucles - Aprender a escribir funciones lambda y entender dónde son apropiadas y dónde perjudican la legibilidad
Programación funcional, de qué estamos hablando
La programación funcional es un enfoque donde un programa se construye a partir de funciones matemáticas: toman datos de entrada inmutables y devuelven un resultado sin cambiar nada externo. Sin efectos secundarios, sin modificar el estado global.
En lenguajes funcionales puros como Haskell, las partes «puras» e «impuras» (que interactúan con el mundo exterior) del programa están estrictamente separadas. Este enfoque permite la prueba formal de la corrección del código: el compilador garantiza la ausencia de efectos secundarios inesperados.
Python no fue diseñado como un lenguaje funcional. Es multiparadigma: orientado a objetos, procedimental y, sí, también funcional. Escribir en Python en un estilo puramente funcional es como clavar clavos con un microscopio: técnicamente posible, pero incómodo y poco natural. Sin embargo, tomar herramientas funcionales individuales e incrustarlas en código familiar es un patrón útil y que funciona.
Comprensiones de listas, la principal herramienta funcional de Python
El pan de cada día de la programación funcional es trabajar con listas. Seleccionar elementos por condición, transformar cada elemento, construir una nueva lista a partir de una existente, todo esto en Python se cubre con las comprensiones de listas.
Antes, bucle procedimental:
1 def filter_odd(li): 2 result = [] 3 for i in li: 4 if i % 2 == 1: 5 result.append(i) 6 return result 7 8 print(filter_odd([2, 4, 6, 7, 8, 1, 19, 200, 42, 31]))
Después, comprensión de lista:
1 li = [2, 4, 6, 7, 8, 1, 19, 200, 42, 31] 2 odd_numbers = [x for x in li if x % 2 == 1] 3 print(odd_numbers) # [7, 1, 19, 31]
Cuatro líneas de código procedimental comprimidas en una. La legibilidad no se resintió: la sintaxis [expression for element in iterator if condition] es intuitivamente clara, «tome x de li si x es impar».
Y esto no es solo azúcar sintáctico. La comprensión de lista se ejecuta más rápido que un bucle equivalente con .append() porque se ejecuta a nivel de C, no a nivel del intérprete de Python.
Qué es un iterador
La comprensión de lista se basa en el concepto de iterador, un objeto que devuelve el siguiente elemento de una secuencia cuando se solicita next(). Cualquier objeto que implemente el método __iter__ se denomina iterable. Lista, cadena, tupla, diccionario, conjunto, todos son iterables.
1 spam_iter = iter("foobar") 2 result = "".join([c.upper() for c in spam_iter]) 3 print(result) # FOOBAR
Aquí iter() devuelve un iterador de cadena, y la comprensión de lista itera a través de él, aplicando .upper() a cada carácter.
Generadores, secuencias perezosas
Un iterador puede crearse no solo a partir de una colección existente, sino también usando una función generadora. En lugar de return, utiliza yield: la función «se duerme», recordando su estado, y en la siguiente llamada a next() continúa desde el mismo lugar.
1 def gen(max_val): 2 i = 1 3 while i < max_val: 4 yield i 5 i += 1 6 7 g = gen(1000) 8 for _ in range(12): 9 print(next(g))
Este código imprimirá números del 1 al 12. La función gen no crea una lista de mil elementos en memoria, genera valores uno a uno, bajo demanda. Para secuencias de millones de registros (líneas de un archivo de registro, flujo de base de datos), la diferencia en el consumo de memoria es de órdenes de magnitud.
Expresiones generadoras
Sintaxis compacta: lo mismo que las comprensiones de listas, pero con paréntesis en lugar de corchetes.
1 g = (c.upper() for c in "foobar")
Esto no es una tupla, es un generador. La regla es simple: corchetes → lista (ansiosa, todo en memoria), paréntesis → generador (perezoso, un elemento a la vez).
Los generadores son un puente entre el estilo procedimental y el funcional en Python: proporcionan evaluación perezosa sin sumergirse en la teoría de mónadas.
Map, filter y reduce: tres pilares del procesamiento funcional
Tres funciones integradas que en los lenguajes funcionales son la base de todo. Python las implementa a su manera, y es importante conocer los matices de las versiones específicas de Python.
Map, aplicar una función a cada elemento
1 def square(x): 2 return x * x 3 4 result = map(square, [1, 2, 3]) 5 print(list(result)) # [1, 4, 9]
map toma una función y una secuencia, y devuelve un iterador con los resultados de aplicar la función a cada elemento. En Python 3, map devuelve un iterador, no una lista, por lo que necesita list() para ver el resultado.
Para transformaciones simples, la comprensión de lista a menudo se lee mejor:
1 [x * x for x in [1, 2, 3]] # same thing, but more familiar
Filter, seleccionar elementos por condición
1 def is_upper(c): 2 return c == c.upper() 3 4 result = filter(is_upper, "FreedominObscureandoutlandishcOde") 5 print(list(result)) # ['F', 'O', 'O']
filter conserva solo aquellos elementos para los que la función predicado devuelve True. Al igual que map, devuelve un iterador en Python 3.
Reduce, plegar una secuencia en un solo valor
A diferencia de map y filter, reduce no reside en el ámbito integrado, sino en el módulo functools. Aplica secuencialmente una función a los elementos, acumulando el resultado.
1 from functools import reduce 2 3 def add(a, b): 4 return a + b 5 6 print(reduce(add, range(1, 6))) # 15
Bajo el capó: ((((1 + 2) + 3) + 4) + 5) = 15. Para sumar números en Python existe la función integrada sum(), por lo que reduce se usa más a menudo para pliegues no estándar: construir un árbol a partir de una lista plana, fusionar diccionarios anidados, calcular el máximo común divisor de una secuencia.
Funciones lambda, ayudantes anónimos
Cuando se necesita una función exactamente una vez y su cuerpo cabe en una expresión, use lambda:
1 lambda arguments: expression
El mismo ejemplo de filter puede reescribirse sin un def separado:
1 result = list(filter(lambda c: c == c.upper(), "FreedominObscureandoutlandishcOde"))
Lambda es un compromiso. Ventaja: no es necesario declarar una función separada para una comprobación trivial. Desventaja: la legibilidad disminuye si la expresión se vuelve más compleja que un par de operaciones. Regla general: si la lambda no cabe en una línea o empieza a anidar lambdas unas dentro de otras, extraiga un def normal.
Dónde son realmente útiles las lambdas
El escenario más común es la ordenación por una clave no estándar:
1 users = [{"name": "Alice", "age": 31}, {"name": "Bob", "age": 25}] 2 users.sort(key=lambda u: u["age"])
O la transformación rápida en map/filter cuando el cuerpo es realmente trivial:
1 squares = list(map(lambda x: x * x, range(10)))
Pero si está a punto de usar una lambda para una lógica con cuatro o cinco operaciones, deténgase. El nombre de una función funciona como documentación, y def proporciona espacio para una cadena de documentación.
⁉️🤔 Preguntas frecuentes
¿Cuándo es mejor una comprensión de lista que map?
Una comprensión de lista como
[x*2 for x in data]se lee de forma más natural quelist(map(lambda x: x*2, data)). Una operación integrada con una lambda tiene dos niveles de indirección, mientras que una comprensión de lista tiene uno. Pero si la función ya existe como undefseparado,mapcon ella es conciso. La comprensión de lista gana en legibilidad para transformaciones simples, especialmente con filtrado mediante[... if ...]. Para lógica compleja con variables intermedias, un buclefornormal es imbatible.
¿Generador o lista, cuál elegir?
Si el resultado se necesita varias veces (iteración, indexación, longitud), elija una lista. Si itera una vez y el volumen de datos es grande, un generador ahorrará memoria. Una lista de un millón de enteros ocupa unos 8 MB, un generador para el mismo rango ocupa menos de un kilobyte. Pero un generador no puede «rebobinarse» ni accederse por índice. Regla práctica: datos de un archivo o base de datos, generador; resultado para uso repetido,
list().
¿Perjudican las lambdas la legibilidad del código?
En pequeñas dosis y para lógica trivial, no.
key=lambda x: x["price"]se lee al instante. Pero tan pronto como una lambda deja de ser «un solo pensamiento», extráigala a una función normal. Los equipos con revisión de código limitan las lambdas a una expresión sin anidamiento. Si un colega necesita más de cinco segundos para entenderla, reescríbala como undef. El nombre sirve como documentación:def by_price(item): return item["price"]es más claro que unlambdaanónimo.
¿Cuánto más lento es el Python funcional que el procedimental?
Para la mayoría de las tareas cotidianas, la diferencia es insignificante, fracciones de microsegundo. La comprensión de lista suele ser más rápida que un bucle equivalente porque está implementada en C. El cuello de botella en los programas Python no es la elección entre
mapy un bucle, sino la entrada/salida, las solicitudes de red y la arquitectura en general. Escriba con claridad, perfile los puntos calientes concProfiley optimice solo lo que realmente ralentiza las cosas.
Qué usar en su código hoy
Las herramientas funcionales de Python no son un dogma, sino palancas. No reemplazan el código procedimental y orientado a objetos, sino que lo complementan en puntos específicos.
Comience con las comprensiones de listas, esta es la aplicación más común y segura. Domine los generadores: tan pronto como vea una lista temporal que se usa una sola vez, pregúntese: ¿no debería reemplazarse esto con yield? Use map y filter donde realmente simplifiquen una cadena de transformación. Reserve las lambdas para key= en ordenaciones y callbacks triviales.
El estilo funcional en Python no trata sobre la «pureza», sino sobre la expresividad. Si después de reemplazar un bucle con una comprensión de lista el código quedó más claro, lo hizo todo bien. Intente reescribir un método problemático en clave funcional y compare el diff: a menudo el resultado habla por sí solo.



