mayo 29, 2020

~ 7 MIN

Funciones en Python

< Blog RSS

Open In Colab

Funciones

Las funciones es la principal herramienta que nos ofrece Python a la hora de organizar nuestro código permitiendo también su reutilización. Como norma general, si te encuentras escribiendo el mismo código o una funcionalidad similar varias veces es muy probable que necesites un función. Esta función incluiría el código y cuando necesitamos ejectuarla la invocamos, enviando de manera opcional diferentes argumentos y recuperando el resultado devuelto por la función.

En Python definimos una función con la palabra clave def.

def func():
    pass

⚠️ Utilizamos la palabra reservada pass para indicarle a Python que no debe hacer nada.

Una vez tenemos nuestra función definida, podemos invocarla usando su nombre.

func()

De momento, nuestra función no lleva a cabo ninguna tarea interesante. Vamos a ver cómo podemos hacer que nuestra función imprima un string por la consola.

def func():
    print("hola")
func()
hola

Como puedes ver, incluímos todo el código que queremos ejecutar dentro de la función con la indentación correcta. Dentro de una función podemos definir nuevas variables que solo serán visibles dentro de la misma función.

def func():
    s = "hola"
    print(s)
func()
hola
# `s` sólo existe dentro de la función
s
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-10-ded5ba42480f> in <module>
----> 1 s


NameError: name 's' is not defined

Sin embargo, si que podemos utilizar variables definidas fuera de la función.

s = "hola"

def func():
    print(s)
func()
hola
# `s` ahora existe tanto dentro como fuera de la función

s
'hola'

Argumentos

En la mayoría de ocasiones queremos que nuestra función lleve a cabo una acción determinada de manera abstracta, pudiendo nosotros especificar los datos sobre los que debería actuar. Para ello podemos definir argumentos que la función usará durante su ejecución.

def func(s):
    print(s)
func("hola")
hola
func("hello")
hello

Ahora nuestra función es mucho más versátil y reusable. Podemos definir diferentes arguments separados por una coma.

def func(a, b, c):
    print(a, b, c)
func("hola", "que", "tal")
hola que tal

También podemos definir argumentos opcionales, de manera que si durante la llamada a la función no se especifica ningún valor se utilice el valor por defecto.

def func(a, b, c="hola"):
    print(a, b, c)
func("hola", "que")
hola que hola

⚠️ Recuerda siempre definir los argumentos obligatorios primero, y luego los opcionales.

Es posible utilizar el nombre del argumento cuando llamamos a una función.

func(a=1, b=2)
1 2 hola

Devolviendo valores

A parte de recibir argumentos una función puede devolver resultados. Para ello utilizamos la palabra reservada return.

def func(a, b):
    return a + b
x = func(1, 2)
x
3

Python nos permite devolver múltiples valores a la vez

def func(a, b):
    return a + b, a -b
x1, x2 = func(1, 2)
x1, x2
(3, -1)

Funciones anónimas

Una función anónima, también conocidas como funciones lambda, es una forma de definir funciones que sólo contienen una declaración, el resultado de la cual es automáticamente devuelto.

def func(x):
    return 2*x
# misma función, pero definida como lambda function

func = lambda x: 2*x
func(2)
4

En el análisis de datos es bastante común tener que llevar a cabo transformaciones que consisten en una sola declaración, y ser capaces de expresar esta funcionalidad de manera concisa aumenta nuestra productividad a la vez que reduce las posibles fuentes de error. Podemos definir funciones anónimas con varios parámetros.

func = lambda x, y: x + y
func(1, 2)
3

Generadores

Python implementa una manera consistente de iterar sobre secuencias, lo cual podemos utilizar en nuestro favor para definir nuestra propia lógica de iteración mediante un tipo de funciones llamadas generadores. Podemos definir un generador de la misma manera que definimos una función normal, cambiando la palabra return por yield. Esto le indicará a Python que pese a que la función ha devuelto un valor, la ejecución de la función no ha terminado y esperará que siga devolviendo valores en el futuro.

# itera una lista de números devolviendo sólo números impares

def func(n=10):
    for i in range(n):
        if i % 2:
            yield i
gen = func()

# ahora gen es un objeto `iterable`

for i in gen:
    print(i)
1
3
5
7
9

Una manera alternativa de definir un generador de manera más compacta es la siguiente

# similar a `list comprehension`

gen = (i for i in range(10) if i % 2)

for i in gen:
    print(i)
1
3
5
7
9

En análisis de datos es muy común iterar sobre grandes conjuntos de datos, aplicando transformaciones o procesados, y dependiendo del algoritmo que utilicemos querremos iterar nuestros datos de una manera u otra (por ejemplo de uno en uno, en grupos o batches, aplicando alguna transformación, etc). Un generador nos será muy útil en estas ocasiones.

Manejando errores

Si has trabajado un poco con Python, o simplemente has seguido los ejemplos que hemos ido viendo, te habrás dado cuenta que en ocasiones Python no devuelve un mensaje de error cuando algo va mal. Con el objetivo de hacer nuestro código robusto, tenemos que ser capaces de anteponernos a estas situaciones evitando que un programa se "cuelgue" o proveiendo mensajes de error útiles. Por ejemplo, la siguiente función espera un argumento y devuelve el mismo valor transformado en un número entero. Esto es posible para algunos tipos de datos, pero otros (como los string) no se pueden convertir a números y nuestra función devolverá un error.

def func(x):
    return int(x) 
func(1)
1
func("hola")
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-63-3db432dc1599> in <module>
----> 1 func("hola")


<ipython-input-60-185a22a41eef> in func(x)
      1 def func(x):
----> 2     return int(x)


ValueError: invalid literal for int() with base 10: 'hola'

Podemos anteponernos a este problema y proveer de un mejor mensaje de error con un bloque try-except. Nuestra función intentará ejecutar el código dentro del bloque try y, de recibir un error, saltará al bloque except.

def func(x):
    try:
        return int(x) 
    except:
        print("el argumento no se puede convertir a int")
func(1)
1
func("hola")
el argumento no se puede convertir a int

En ocasiones podemos querer ejecutar código tanto si hay errores como si no, para ello podemos usar el bloque finally.

def func(x):
    try:
        return int(x) 
    except:
        print("el argumento no se puede convertir a int")
    finally:
        print("siempre")
func(1)
siempre
1
func("hola")
el argumento no se puede convertir a int
siempre

Resumen

Con el objetivo de hacer nuestro código más robusto, legible y reutilizable Python nos ofrece la opción de utilizar funciones. En este post hemos visto cómo definir tales funciones, trozos de código que podemos invocar en cualquier momento enviándo argumentos y devolviendo resultados. Hemos hablado también sobre funciones anónimas, una manera de definir funciones cortas con una sola declaración muy útiles cuando necesitamos una función de manera inmediata. El uso de generadores nos permite defininr nuestros propios objetos iterables, útiles en el análisis de datos para iterar sobre grandes cantidades de datos llevando a cabo alguna lógica en concreto. Por último hemos visto como manejar los diferentes errores que pueden surgir en la invocación de función con el objetivo de evitar que nuestro programa se quede "colgado" cuando algún error surge en su ejecución.

< Blog RSS