Metadata: Estos notebooks están (más que) inspirados en el excelente trabajo de Jake van der Plass y su Whirlwind Tour Of Python. Ver A Whirlwind Tour of Python by Jake VanderPlas (O’Reilly). Copyright 2016 O’Reilly Media, Inc., 978-1-491-96465-1.". Estos notebooks están protegidos con la misma licencia de los originales, Creative Commons 0. Todas las notas están disponibles en PrograUDD1.

Funciones

Hasta ahora (al menos en las clases teóricas :P) hemos estado usando sólo simples "pedazos" de código para hacer nuestros programas. Una forma más inteligente de ordenar nuestro código de Python es hacerlo aún más leible y reusable, agrupando, si se quiere porciones de código en unidades a las que podemos llamar desde distintos puntos del código. Estas unidades se llaman funciones. Aqui vamos a ver dos formas de definir funciones: la instrucción def, que es útil para cualquier tipo de función, y la notación lambda, que es útil para definir pequeñas funciones anónimas dentro del código.

Usando funciones

Las funciones son bloques de código que tienen un nombre, y a las que se puede llamar usando paréntesis. Ya hemos visto funciones antes. Por ejemplo, print es una función (en Python 3, al menos):


In [1]:
print('abc')


abc

Aqui, print() es el nombre de la función, y 'abc' es lo que se llama un argumento (de la función).

Además de los argumentos, hay lo que se llama keyword arguments (argumentos clave) que se especifican por nombre. Uno de estos posibles argumentos con nombre para la función print() es sep, que le dice a print() qué caracter usar para separar los argumentos que va a imprimir cuando hay multiples elementos, por ejemplo:


In [2]:
print(1, 2, 3)


1 2 3

In [3]:
print(1, 2, 3, sep='--')


1--2--3

Cuando se usan argumentos y argumentos con nombre, los argumentos con nombre siempre deben aparecer al final, pero en cualquier orden.

Definiendo funciones

Las funciones se vuelven útiles cuando empezamos a definir las nuestras, organizando funcionalidad y reusandolas en múltiples lugares del código. En Python, las funciones se definen con la instrucción def. Por ejemplo, podemos encapsular una version de Fibonacci de la siguiente manera:


In [4]:
def fibonacci(N):
    L = []
    a, b = 0, 1
    while len(L) < N:
        a, b = b, a + b
        L.append(a)
    return L

Ahora ya tenemos una función que se llama fibonacci y que toma un solo argumento llamado N, hace cosas incontables con este argumento, y retorna un valor. En este caso, la lista de los N primeros números de la serie de Fibonacci.


In [5]:
fibonacci(10)


Out[5]:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Para aquellos que sepan algo de lenguajes que son "tipeados" (a los que hay que asignar un tipo de datos a cada variable), verán que no hay información de los tipos asociados a los argumentos o valores de retorno de la función. Las funciones de Python pueden retornar cualquier tipo de objeto de Python, simple o complejo.

Por ejemplo, como discutimos hace un par de clases atrás, podemos return (devolver) valores múltiples de la función, simplemente poniendolos en una tupla, la cual es definida por comas.


In [6]:
def real_imag_conj(val):
    return val.real, val.imag, val.conjugate()

r, i, c = real_imag_conj(3 + 4j)
print(r, i, c)


3.0 4.0 (3-4j)

Valores por "omisión" de los argumentos

A veces cuando definimos una función, hay un valore que usamos la mayor parte del tiempo, pero también queremos darle al usuario de la función (que puede que no seamos solo nosotros), algo de flexibilidad. En este caso, definimos valores por "omisión" de los argumentos. Consideremos la función fibonacci de más arriba. Cómo podríamos hacer que el usuaro eligiera el valor de comienzo? Por ejemplo, así:


In [7]:
def fibonacci(N, a=0, b=1):
    L = []
    while len(L) < N:
        a, b = b, a + b
        L.append(a)
    return L

Con un solo argumento, el resultado de llamar a la función es idéntico al anterior:


In [8]:
fibonacci(10)


Out[8]:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Pero ahora podemos usar la función para explorar otros valores:


In [9]:
fibonacci(10, 0, 2)


Out[9]:
[2, 2, 4, 6, 10, 16, 26, 42, 68, 110]

Si especificamos el nombre de los argumentos con clave, no es necesario el orden:


In [10]:
fibonacci(10, b=3, a=1)


Out[10]:
[3, 4, 7, 11, 18, 29, 47, 76, 123, 199]

*args and **kwargs: Argumentos flexibles

Hay veces que queremos definir funciones con un número de argumentos que pueden variar (un ejemplo es?). En este caso, podemos usar *args and **kwargs para tomar todos los argumentos que se han pasado a la función:


In [11]:
def catch_all(*args, **kwargs):
    print("args =", args)
    print("kwargs = ", kwargs)

In [12]:
catch_all(1, 2, 3, a=4, b=5)


args = (1, 2, 3)
kwargs =  {'a': 4, 'b': 5}

In [13]:
catch_all('a', keyword=2)


args = ('a',)
kwargs =  {'keyword': 2}

Noten que aqui los nombres args y kwargs no son importantes, sino los asteriscos: un asterisco significa "expandir como secuencia" (tupla), mientras que dos asteriscos significa "expandir como diccionario).

Funciones anónimas: lambda

Además del def, hay una manera de definir funciones cortas y específicas:


In [14]:
add = lambda x, y: x + y
add(1, 2)


Out[14]:
3

que es mas o menos equivalente a


In [15]:
def add(x, y):
    return x + y

Como todo es un objeto en Python, también podemos pasar funciones como argumento de otras funciones! Como ejemplo, tomemos algunos datos almacenados en una lista de diccionarios (!):


In [16]:
data = [{'first':'Guido', 'last':'Van Rossum', 'YOB':1956},
        {'first':'Grace', 'last':'Hopper',     'YOB':1906},
        {'first':'Alan',  'last':'Turing',     'YOB':1912}]

Supongamos que queremos ordenar la lista por año de nacimiento. Las listas efectivamente tienen un método sort:


In [17]:
sorted([2,4,3,5,1,6])


Out[17]:
[1, 2, 3, 4, 5, 6]

Pero los diccionarios no: tenemos que decirle a la función cómo ordenar nuestros datos. Podemos hacer esto especificando una "llave" (key) del diccionario al método sort:


In [18]:
# sort alphabetically by first name
sorted(data, key=lambda item: item['first'])


Out[18]:
[{'YOB': 1912, 'first': 'Alan', 'last': 'Turing'},
 {'YOB': 1906, 'first': 'Grace', 'last': 'Hopper'},
 {'YOB': 1956, 'first': 'Guido', 'last': 'Van Rossum'}]

In [19]:
# sort by year of birth
sorted(data, key=lambda item: item['YOB'])


Out[19]:
[{'YOB': 1906, 'first': 'Grace', 'last': 'Hopper'},
 {'YOB': 1912, 'first': 'Alan', 'last': 'Turing'},
 {'YOB': 1956, 'first': 'Guido', 'last': 'Van Rossum'}]

Aunque podamos haber definido la función lambda con un def, hay veces que es más corto y conciso hacerlo de esta manera.