def sumar(x, y):
return x + y
3, 11) sumar(
14
La programación funcional es un paradigma de programación que se centra en el uso de funciones puras y en concebir la computación como la evaluación de funciones. En lugar de dar instrucciones paso a paso que cambian variables o estados (como ocurre en la programación imperativa), la idea es construir programas a partir de funciones que transforman datos.
Existen lenguajes diseñados específicamente para la programación funcional (como Haskell), pero Python no es uno de ellos. Python es un lenguaje multiparadigma, lo que significa que nos permite combinar diferentes estilos de programación. Por este motivo, la programación funcional en Python no suele ser el enfoque principal, pero puede ser muy útil para escribir código más claro, conciso y fácil de probar.
Una función es pura cuando su salida depende únicamente de los valores de entrada y no produce ningún efecto secundario o colateral (side effect, en inglés).
La función sumar
, que calcula y devuelve la suma de dos números, es un ejemplo de función pura: su resultado depende solo de sus argumentos y no genera efectos colaterales.
En cambio, la función agregar
no es una función pura. Esto se debe a que modifica un objeto global, lo que se conoce como un efecto secundario. Además, el valor de su salida no depende únicamente de la entrada, sino también de un estado global: la cantidad de elementos en lista
.
Un efecto secundario (side effect) es cualquier cambio de estado observable que ocurre fuera del ámbito local de una función. En otras palabras, se trata de una modificación del entorno externo de la función que va más allá de simplemente devolver un valor.
Algunos ejemplos de side effects son:
Las funciones con efectos secundarios pueden ser problemáticas porque, al modificar elementos externos, hacen que el código sea impredecible y difícil de probar.
En el ejemplo de la función agregar
que creamos anteriormente, no es posible predecir el valor de salida para un valor de entrada determinado.
Por eso, la programación funcional promueve el uso de funciones puras, que no producen efectos secundarios. De esta manera, con las mismas entradas siempre se obtiene la misma salida, logrando un código más confiable, predecible y sencillo de mantener.
Definamos otra función muy sencilla, restar
, que calcula y devuelve la diferencia entre dos objetos.
Podemos observar que esta función es un objeto de tipo function
.
Al imprimir la función, Python nos muestra su nombre y la dirección de memoria donde está almacenada (en formato hexadecimal). En cambio, al mostrar su representación, obtenemos información adicional: el módulo en el que fue definida (en este caso __main__
) y la lista de parámetros que recibe (x
e y
).
Dado que la función restar
es un objeto de Python, podemos asignarla a una nueva variable y realizar una llamada utilizando esa nueva etiqueta en vez de la original.
Notemos que resta_especial
no es una nueva función; es solamente una nueva referencia a la función antes definida.
En Python, las funciones son ciudadanos de primera clase. Esto significa que son objetos, al igual que las cadenas o los números. Por lo tanto, todo lo que se puede hacer con una cadena o un número también puede hacerse con una función.
Por ejemplo, se pueden almacenar dentro de una lista junto con otros objetos de distintos tipos:
Incluso una función puede ser almacenada como valor en un diccionario:
Luego, se las puede usar de la siguiente manera:
Como cualquier objeto de Python, una función puede ser pasada como argumento de otra función. Debajo definimos dos funciones muy simples. Una imprime un mensaje de bienvenida y la otra uno de despedida.
¡Hola!
¡Chau!
Se puede otra función, que llamamos externa
(del inglés outer function), que tiene un único parámetro interna
. En su cuerpo, la función externa
llama a la función interna
y devuelve lo que sea que interna
haya devuelto.
De este modo, si llamamos a externa
pasándole como argumento a bienvenida
, se imprimirá ¡Hola!
; y si lo hacemos con despedida
, se imprimirá ¡Chau!
.
A esta función podemos pasarle cualquier función que pueda ser llamada sin ningún argumento. Por ejemplo:
También es posible que una función devuelva como resultado otra función. Debajo, la función fabrica
devuelva una función que computa la suma de dos objetos.
def fabricar():
def interna(x, y):
return x + y
return interna
# La llamada a 'fabricar' genera y devuelve una función
f = fabricar()
# La función obtenida puede ser tratada como cualquier otra función
f(10, 15)
25
Como veremos más adelante en este apunte, una función que fabrica otras funciones puede recibir parámetros que luego son utilizados dentro de la función interna.
En el bloque siguiente, la función crear_multiplicador
recibe un parámetro x
, que define el valor por el cual se multiplicará el argumento de la función interna que se devuelve.
Así, es posible crear funciones para duplicar, triplicar, etc.
duplicar = crear_multiplicador(2)
triplicar = crear_multiplicador(3)
print(duplicar(5))
print(triplicar(5))
10
15
A las funciones que crean y devuelven funciones se las conoce como fábrica de funciones, del inglés function factory.
En Python, las funciones también cuentan con atributos, del mismo modo que otros objetos. En el siguiente ejemplo definimos la función resolvente
, que recibe las constantes a
, b
y c
de un polinomio de segundo grado, calcula sus raíces usando la fórmula resolvente y las devuelve en una tupla.
def resolvente(a, b, c):
discriminante = b ** 2 - 4 * a * c
x0 = (-b + (discriminante) ** 0.5) / (2 * a)
x1 = (-b - (discriminante) ** 0.5) / (2 * a)
return x0, x1
resolvente(2, 5, -3)
(0.5, -3.0)
A través del atributo especial __code__
es posible consultar ciertos atributos o detalles internos de una función:
print(resolvente.__code__.co_argcount) # Cantidad de argumentos
print(resolvente.__code__.co_name) # Nombre de la función
print(resolvente.__code__.co_varnames) # Variables en el ámbito local
3
resolvente
('a', 'b', 'c', 'discriminante', 'x0', 'x1')
Acceder a la información de una función a través de __code__
puede resultar poco práctico, ya que los atributos disponibles son técnicos y no siempre coinciden directamente con lo que solemos necesitar (por ejemplo, obtener solo los nombres de los argumentos).
Para facilitar esta tarea, la librería estándar de Python incluye el módulo inspect
que ofrece herramientas más claras e intuitivas para explorar los atributos y detalles de una función.
Como ejemplo, la función signature
que devuelve un objeto que representa la firma de la función resolvente
.
A partir de esta firma podemos consultar distintos aspectos de los parámetros, como sus valores por defecto:
Finalmente, inspect también permite acceder al código fuente de la función en forma de cadena de texto:
La programación funcional se basa en llamar funciones y pasarlas, por lo que, naturalmente, implica definir muchas funciones. En Python, además de usar def
, podemos crear funciones anónimas de forma rápida con una expresión lambda.
La sintaxis es la siguiente:
y devuelve como resultado una función anónima. Un ejemplo es el siguiente:
Como la función anónima que acabamos de crear no fue asignada a ninguna variable, ya no podemos usarla.
Una posibilidad es invocarla inmediatamente al momento de su creación:
Otra opción es asignarla a una variable para poder llamarla más adelante:
return
A diferencia de las funciones definidas con def
, las expresiones lambda no requieren la sentencia return
. De forma implícita, siempre devuelven el resultado de la única expresión que contienen.
En ninguno de los ejemplos anteriores parece que obtengamos alguna ventaja frente a usar def
para definir una función. De hecho, da la impresión de que estamos complicando innecesariamente las cosas.
Lo cierto es que las funciones anónimas no están pensadas para emplearse de la manera expuesta en nuestros ejemplos. Su uso principal es en operaciones simples y puntuales, cuando no resulta práctico definir una función completa con def
.
Un caso típico es al utilizarlas como argumentos de otras funciones.
Supongamos que queremos ordenar la siguiente lista de refranes según diferentes criterios.
Por defecto, la función sorted
ordena una lista de cadenas de manera alfabética.
['A caballo regalado no se le miran los dientes',
'Al mal tiempo, buena cara',
'Cada loco con su tema',
'El que mucho abarca, poco aprieta',
'Más vale pájaro en mano que cien volando',
'Perro que ladra no muerde']
Si quisiéramos ordenar los refranes por su longitud, podemos usar el argumento opcional key
de sorted
. Este argumento recibe una función que, aplicada a cada elemento, devuelve el valor a utilizar en la comparación. En nuestro caso, basta con usar len
, ya que solo nos interesa la cantidad de caracteres de cada cadena.
['Cada loco con su tema',
'Al mal tiempo, buena cara',
'Perro que ladra no muerde',
'El que mucho abarca, poco aprieta',
'Más vale pájaro en mano que cien volando',
'A caballo regalado no se le miran los dientes']
¿Y si quisiéramos ordenarlos según la cantidad de palabras? Para eso necesitamos una función que reciba una cadena, la divida en palabras y cuente cuántas tiene.
Sin funciones anónimas podríamos hacer lo siguiente:
['Al mal tiempo, buena cara',
'Perro que ladra no muerde',
'Cada loco con su tema',
'El que mucho abarca, poco aprieta',
'Más vale pájaro en mano que cien volando',
'A caballo regalado no se le miran los dientes']
En cambio, con una función anónima podemos escribir todo el programa en una sola línea:
['Al mal tiempo, buena cara',
'Perro que ladra no muerde',
'Cada loco con su tema',
'El que mucho abarca, poco aprieta',
'Más vale pájaro en mano que cien volando',
'A caballo regalado no se le miran los dientes']
De esta forma el código es más conciso y evitamos definir funciones “descartables” que no volverán a usarse.
El término lambda proviene del cálculo lambda, un sistema formal de lógica matemática para expresar cálculos basados en la abstracción y aplicación de funciones.
Se le dio ese nombre porque Alonzo Church, creador del cálculo lambda en la década de 1930, usó la letra griega λ para denotar la operación de abstracción de funciones.
Una función lambda normalmente recibe uno o múltiples parámetros, pero no es obligatorio, por lo que es posibile escribir una función anónima sin parámetros:
La función anónima crear_numero_magico
es equivalente a la siguiente función
Las funciones variádicas son funciones que pueden recibir una cantidad variable de argumentos.
A lo largo de estos apuntes hemos utilizado funciones variádicas en tantísimas oportunidades. Un ejemplo de función variádica es print
, que acepta tantos argumentos posicionales como necesitemos.
Las funciones variádicas no son exclusivas de Python; también podemos implementarlas nosotros mismos.
*args
Supongamos que queremos una función que recibe una cantidad arbitraria de gustos de helado y e imprime un mensaje como si lo agregase a un pedido. Por ejemplo:
Una posible implementación para tal función es:
def armar_pedido(gusto_1=None, gusto_2=None, gusto_3=None):
if gusto_1 is not None:
print(f"Agregando '{gusto_1}'")
if gusto_2 is not None:
print(f"Agregando '{gusto_2}'")
if gusto_3 is not None:
print(f"Agregando '{gusto_3}'")
Aunque funciona, esta solución está lejos de ser ideal. Requiere definir un argumento separado para cada gusto, asignarle un valor por defecto y luego verificar si es distinto de None
antes de agregarlo al pedido.
Además, el código resulta repetitivo y restrictivo: solo permite un máximo de tres gustos.
Afortunadamente, en Python es posible crear funciones que acepten una cantidad arbitraria de argumentos posicionales de una manera sencilla.
Para ello se utiliza un argumento especial precedido por un asterisco (*
), lo que le permite recibir una cantidad arbitraria de valores no nombrados.
Agregando 'Dulce de leche'
Agregando 'Sambayón'
Agregando 'Frutos del bosque'
Agregando 'Menta granizada'
Agregando 'gusto 1'
Agregando 'gusto 2'
Agregando 'gusto 3'
Agregando 'gusto 4'
Agregando 'gusto 5'
Agregando 'gusto 6'
Agregando 'gusto 7'
Por convención, este argumento suele escribirse como *args
, aunque en realidad el nombre del argumento puede ser cualquiera que resulte apropiado. En nuestro caso, resulta más intuitivo usar *gustos
, y la función quedaría así:
Agregando 'gusto 1'
Agregando 'gusto 2'
Agregando 'gusto 3'
Agregando 'gusto 4'
Agregando 'gusto 5'
Agregando 'gusto 6'
Agregando 'gusto 7'
*args
🔍
Cuando se utilizan valores con el argumento especial *args
, Python los reúne automáticamente en una tupla. Esto permite acceder a todos los argumentos como miembros de una colección inmutable.
5
('que', 'es', 'esto', True, None)
<class 'tuple'>
**kwargs
Así como recibimos una cantidad arbitraria de argumentos posicionales, también podemos recibir una cantidad arbitraria de argumentos nombrados.
En este caso, se utilizan dos asteríscos (**
) en vez de uno (*
).
La convención es usar el nombre **kwargs
, pero también es válido usar cualquier otro nombre que sea adecuado en nuestro contexto.
Comencemos por un ejemplo elemental, que solo imprime el objeto kwrargs
y su tipo:
def ejemplo(**kwargs):
print(kwargs)
print(type(kwargs))
ejemplo(nombre="Mariano", apellido="Pérez")
{'nombre': 'Mariano', 'apellido': 'Pérez'}
<class 'dict'>
Cuando usamos una cantidad variable de argumentos nombrados, Python los agrupa en un diccionario, ya que esta estructura permite asociar cada nombre con su valor de forma natural.
Dentro de la función, se puede manipular al diccionario kwargs
como a cualquier otro diccionario de Python.
Imaginemos, por ejemplo, una función que registra información de distintos departamentos. En este caso, no sabemos de antemano qué atributos se van a proporcionar, pero sí sabemos que ciertos atributos deben contar con un valor por defecto si no se especifican.
def registrar_propiedad(**kwargs):
print("Diccionario original:")
print(kwargs)
# Si no se especifica la cantidad de cocheras, se pone 0 por defecto
if "cochera" not in kwargs:
kwargs["cochera"] = 0
# Si no se especifica la ciudad, se pone 'Desconocido' por defecto
if "ciudad" not in kwargs:
kwargs["ciudad"] = "Desconocido"
return kwargs
Cuando no se especifician la cantidad de cocheras, la función nos devuelve un diccionario donde la cantidad de cocheras es 0.
datos = registrar_propiedad(ambientes=2, ciudad="Rosario")
print("\nDiccionario sanitizado")
print(datos)
Diccionario original:
{'ambientes': 2, 'ciudad': 'Rosario'}
Diccionario sanitizado
{'ambientes': 2, 'ciudad': 'Rosario', 'cochera': 0}
Si los atributos requeridos son especificados, se devuelve el diccionario sin cambios.
datos = registrar_propiedad(ambientes=2, ciudad="Santa Fe", cochera=2)
print("\nDiccionario sanitizado")
print(datos)
Diccionario original:
{'ambientes': 2, 'ciudad': 'Santa Fe', 'cochera': 2}
Diccionario sanitizado
{'ambientes': 2, 'ciudad': 'Santa Fe', 'cochera': 2}
Y si se pasan atributos inesperados, también se incluyen en la salida.
*args
y **kwargs
Las funciones en Python pueden recibir simultáneamente una cantidad variable de argumentos posicionales y nombrados. Para lograrlo, se combinan *args
y **kwargs
. Es importante recordar que, al definir la función, *args
debe colocarse antes que **kwargs
, ya que los argumentos posicionales siempre se pasan antes que los nombrados.
def superfuncion(*args, **kwargs):
for arg in args:
print(f"Me pasaron el argumento posicional '{arg}'")
for key, value in kwargs.items():
print(f"Me pasaron el argumento con nombre '{key}' y valor '{value}'")
superfuncion(True, 64, nombre="Elsa", apellido="Pato")
Me pasaron el argumento posicional 'True'
Me pasaron el argumento posicional '64'
Me pasaron el argumento con nombre 'nombre' y valor 'Elsa'
Me pasaron el argumento con nombre 'apellido' y valor 'Pato'
Si se intenta pasar un argumento posicional (sin nombre) después de un argumento nombrado, obtendríamos un error:
superfuncion(True, nombre="Elsa", apellido="Pato", 64)
^
SyntaxError: positional argument follows keyword argument
A primera vista, los ejemplos de *args
y **kwargs
pueden dar la impresión de que estas herramientas solo complican la escritura del código. Sin embargo, su verdadero valor aparece al trabajar en programas más complejos, donde se vuelven fundamentales para simplificar la lógica y aportar flexibilidad en la resolución de una gran variedad de problemas.
Ya llegaremos…
Para reforzar que los nombres *args
y **kwargs
son solamente una convención, podríamos escribir la función superfuncion
como:
def superfuncion(*posicionales, **nombrados):
for arg in posicionales:
print(f"Me pasaron el argumento posicional '{arg}'")
for key, value in nombrados.items():
print(f"Me pasaron el argumento con nombre '{key}' y valor '{value}'")
y funcionaría de igual modo.
En la Sección 3.1 vimos el siguiente ejemplo:
def crear_multiplicador(x):
def interna(y):
return y * x
return interna
duplicar = crear_multiplicador(2)
triplicar = crear_multiplicador(3)
print(duplicar(5))
print(triplicar(5))
La función crear_multiplicador
es una fábrica de funciones que devuelve otra función que se encarga de realizar la multiplicación. Lo interesante de esta implementación es que la función interna solo recibe uno de los dos valores necesarios para la multiplicación; el otro queda que fijado cuando se ejecuta la función externa crear_multiplicador
.
Para que duplicar
y triplicar
funcionen correctamente, ambas funciones internas deben conservar acceso al entorno en el que está definido el valor de x
. Ese mecanismo, que permite a una tener acceso a las variables de su contexto incluso después de que la ejecución de la función externa haya concluído, es precisamente lo que se conoce como un closure.
def crear_multiplicador(x):
def interna(y):
return y * x
return interna
duplicar = crear_multiplicador(2)
triplicar = crear_multiplicador(3)
print(duplicar(5))
print(triplicar(5))
10
15
El siguiente ejemplo hace aún más evidente el funcionamiento de este mecanismo.
Dentro del cuerpo de la function factory externa
, se define valor
con el número 256
. Luego, la función interna hace uso de esta variable valor
dentro de print
.
def externa():
valor = 256
def closure():
print(f"¡El valor es: {valor}!")
return closure
revelar_numero = externa()
revelar_numero()
¡El valor es: 256!
Aunque desde fuera no podemos acceder directamente a valor
:
la función interna sí puede hacerlo tantas veces como sea necesario:
¡El valor es: 256!
¡El valor es: 256!
¡El valor es: 256!
Para finalizar, veamos un ejemplo similar al anterior, pero donde el valor de la variable numero
es desconocido para nosotros. Dicho valor se genera de manera aleatoria cuando se crea ejecuta la fábrica de funciones crear_funcion
.
Luego, sin importar cuántas veces llamemos a reveladora
, el mensaje será siempre el mismo, ya que el valor de numero
se definió una sola vez en el momento de crear la función.
A menudo se dice que un closure es una función. Así, en el siguiente ejemplo, duplicar
sería considerado un closure:
def crear_multiplicador(x):
def interna(y):
return y * x
return interna
duplicar = crear_multiplicador(2)
Sin embargo, esa definición es un tanto imprecisa. Un closure no es simplemente una función, sino el mecanismo que permite a las funciones acceder a las variables del entorno en el que fueron definidas, incluso cuando ese entorno ya dejó de existir (por ejemplo, después de que termina la ejecución de la función externa que las creó).
Uf… ¡qué complicado!
object of type 'closure' is not subsettable
😵
Si en R intentamos seleccionar filas o columnas de data
sin haberle asignado un objeto previamente, obtendremos el siguiente error:
Esto ocurre porque data
es en realidad una función en R. En este lenguaje, el tipo de los objetos función se denomina closure
, haciendo referencia la capacidad que tienen las funciones de acceder a valores del ambiente donde fueron definidas.