Funciones

Introducción

Una de las mejores formas de ampliar tu alcance como científico de datos es escribir funciones. Las funciones te permiten automatizar tareas comunes de una manera más potente y general que copiar y pegar. Escribir una función tiene tres grandes ventajas frente a copiar y pegar:

  1. Puedes darle a una función un nombre evocador que haga tu código más fácil de entender.

  2. Cuando cambian los requisitos, solo necesitas actualizar el código en un lugar, en vez de en muchos.

  3. Eliminas la posibilidad de cometer errores accidentales al copiar y pegar (por ejemplo, actualizar el nombre de una variable en un lugar, pero no en otro).

Escribir buenas funciones es un viaje de toda la vida. Incluso después de usar Python durante muchos años, todavía puedes aprender nuevas técnicas y mejores formas de abordar viejos problemas. El objetivo de este capítulo no es enseñarte cada detalle esotérico de las funciones, sino ayudarte a empezar con algunos consejos pragmáticos que puedes aplicar de inmediato.

Además de consejos prácticos para escribir funciones, este capítulo también te da algunas sugerencias sobre cómo dar estilo a tu código. Un buen estilo de código es como una puntuación correcta. Puedesarreglártelassinella, ¡pero sin duda hace que todo sea más fácil de leer! Al igual que con los estilos de puntuación, hay muchas variaciones posibles. Aquí presentamos el estilo que usamos en nuestro código, pero lo más importante es ser coherente.

Requisitos previos

Necesitarás los paquetes pandas y numpy para este capítulo.

Funciones

Una función tiene entradas, realiza su función y devuelve las salidas que tenga. Las funciones comienzan con la palabra clave def, de ‘definir una función’. Luego tienen un nombre, seguido de paréntesis, (), que pueden contener argumentos de la función y argumentos de palabra clave. A esto le siguen dos puntos. El cuerpo de la función se indenta con respecto al texto situado más a la izquierda. Los argumentos de la función se definen entre paréntesis después del nombre, con las distintas entradas separadas por comas. Las salidas se indican con la palabra clave return, de nuevo con las distintas variables separadas por comas.

Veamos un ejemplo muy sencillo de una función con un único argumento (o arg):

def welcome_message(name):
    return f"Hello {name}, and welcome!"


# Without indentation, this code is not part of function
name = "Ada"
output_string = welcome_message(name)
print(output_string)

Una característica potente de las funciones es que podemos definir valores por defecto para los argumentos de entrada. Estos se llaman argumentos de palabra clave (o kwargs). Veámoslo en acción definiendo un valor por defecto para name, junto con varias salidas: un mensaje de saludo y una puntuación.

def score_message(score, name="student"):
    """This is a doc-string, a string describing a function.
    Args:
        score (float): Raw score
        name (str): Name of student
    Returns:
        str: A hello message.
        float: A normalised score.
    """
    norm_score = (score - 50) / 10
    return f"Hello {name}", norm_score


# Without indentation, this code is not part of function
name = "Ada"
score = 98
# No name entered
print(score_message(score))
# Name entered
print(score_message(score, name=name))
NotaArgumentos y argumentos con nombre (keyword arguments)

Los argumentos son las variables que las funciones necesitan siempre, como a y b en def add(a, b): return a + b. ¡La función no funcionará sin ellos! A los argumentos de una función a veces se les llama args.

Los argumentos de palabra clave son las variables opcionales de las funciones, como c en def add(a, b, c=5): return a + b - c. Si no proporcionas un valor para c al llamar a la función, se usará automáticamente c=5. A los argumentos de palabra clave a veces se les llama kwargs.

TipEjercicio

¿Cuál es el tipo de retorno de una función con varios valores de retorno separados por comas después de la instrucción return?

En ese último ejemplo, habrás notado que añadimos algo de texto a la función. Es un docstring, o cadena de documentación. Sirve para ayudar a los usuarios (y, muy probablemente, a tu yo del futuro) a entender qué hace la función. Veamos cómo funciona llamando a help() sobre la función score_message:

help(score_message)
TipEjercicio

Escribe una función que devuelva un carácter unicode de “chócala” (high five) si la entrada es igual a “coding for economists” y, en caso contrario, una cara triste, “:-/”.

Añade un segundo argumento que tome por defecto un string vacío pero que, si se usa, se añada (concatene) al mensaje devuelto. Úsalo para crear la salida “:-/ here is my message.”

Escribe un docstring para tu función y llama a help sobre ella.

Para aprender más sobre args y kwargs, consulta estos breves videotutoriales.

¿Cuándo deberías escribir una función?

Deberías considerar escribir una función siempre que hayas copiado y pegado un bloque de código más de dos veces (es decir, ya tienes tres copias del mismo código). Por ejemplo, echa un vistazo a este código. ¿Qué hace?

import numpy as np
import pandas as pd

df = pd.DataFrame(np.random.normal(size=(10, 4)), columns=["a", "b", "c", "d"])

df["a"] = (df["a"] - df["a"].min()) / (df["a"].max() - df["a"].min())
df["b"] = (df["b"] - df["b"].min()) / (df["b"].max() - df["a"].min())
df["c"] = (df["c"] - df["c"].min()) / (df["c"].max() - df["c"].min())
df["d"] = (df["d"] - df["d"].min()) / (df["d"].max() - df["d"].min())

Quizá puedas deducir que reescala cada columna para que tenga un rango de 0 a 1. Pero ¿viste el error? Hubo una equivocación al copiar y pegar el código para df["b"]: alguien olvidó cambiar una a por una b. Extraer el código repetido a una función es una buena idea porque evita que cometas este tipo de errores.

Para escribir una función, primero necesitas analizar el código. ¿Cuántas entradas tiene?

df["a"] - df["a"].min() / (df["a"].max() - df["a"].min())

Este código solo tiene una entrada: df["a"]. Para que las entradas sean más claras, es buena idea reescribir el código usando variables temporales con nombres generales. Aquí el código solo requiere un único vector numérico, así que lo llamaremos x y lo pondremos en una función.

Las funciones comienzan con la palabra clave def, de ‘definir una función’. Luego tienen un nombre, seguido de paréntesis, (), que pueden contener argumentos de la función y argumentos de palabra clave. A esto le siguen dos puntos. El cuerpo de la función se indenta con respecto al texto situado más a la izquierda. Los argumentos de la función se definen entre paréntesis después del nombre, con las distintas entradas separadas por comas. Las salidas se indican con la palabra clave return, de nuevo con las distintas variables separadas por comas.

Así, en Python, las funciones tienen la forma:

def name_of_function(<inputs>):
    <code to carry out on inputs>
    <return statement, if appropriate>

Así que aquí sería:

def rescale(x):
    return (x - x.min()) / (x.max() - x.min())

Todavía hay algo de duplicación en este código. Estamos calculando el mínimo de los datos dos veces, así que tiene sentido hacerlo una sola vez:

def rescale(x):
    minimum = x.min()
    return (x - minimum) / (x.max() - minimum)

Extraer los cálculos intermedios a variables con nombre es una buena práctica, porque deja más claro lo que hace el código.

Hay tres pasos clave para crear una nueva función:

  1. Debes elegir un nombre para la función. Aquí hemos usado rescale porque esta función reescala un vector para que quede entre 0 y 1.

  2. Enumeras las entradas, o argumentos, de la función dentro de function. Aquí solo tenemos un argumento. Si tuviéramos más, la llamada se vería como function(x, y, z). (También podríamos tener un argumento de palabra clave con nombre, como data=, después de los argumentos.)

  3. Colocas el código que has desarrollado en el cuerpo de la función, un bloque que sigue inmediatamente a function(...):.

Fíjate en el proceso general: solo creamos la función después de haber averiguado cómo hacerla funcionar con una entrada sencilla. Es más fácil empezar con código que funciona y convertirlo en una función; es más difícil crear una función y luego intentar que funcione.

En este punto, es buena idea comprobar tu función con algunas entradas diferentes:

rescale(pd.Series([-10, 0, 10]))
rescale(pd.Series([1, 2, 3, np.nan, 5]))

A medida que escribas más y más funciones, con el tiempo querrás convertir estas pruebas informales e interactivas en pruebas formales y automatizadas. Ese proceso se llama pruebas unitarias (unit testing). Por desgracia, queda fuera del alcance de este libro.

Ahora que tenemos una función, podemos simplificar el ejemplo original:

df["a"] = rescale(df["a"])
df["b"] = rescale(df["b"])
df["c"] = rescale(df["c"])
df["d"] = rescale(df["d"])

En comparación con el original, este código es más fácil de entender y hemos eliminado un tipo de errores de copiar y pegar. Todavía hay bastante duplicación, ya que hacemos lo mismo con varias columnas; en realidad también podemos eliminarla, pero veremos cómo hacerlo más adelante en el libro.

Otra ventaja de las funciones es que, si nuestros requisitos cambian, solo necesitamos hacer el cambio en un lugar. Por ejemplo, podríamos descubrir que algunas de nuestras variables incluyen valores infinitos y que rescale() (en la práctica) falla:

rescale(pd.Series([1, 2, 3, np.inf, 5]))

Como hemos extraído el código a una función, solo necesitamos hacer la corrección en un lugar:

def rescale(x):
    x = x.replace(np.inf, np.nan)
    minimum = x.min()
    return (x - minimum) / (x.max() - minimum)


rescale(pd.Series([1, 2, 3, np.inf, 5]))

Esta es una parte importante del principio “no te repitas” (o DRY, por sus siglas en inglés). Cuanta más repetición tengas en tu código, más lugares tendrás que recordar actualizar cuando las cosas cambien (¡y siempre cambian!), y más probable será que introduzcas errores con el tiempo.

Las funciones son para humanos y para computadoras

Es importante recordar que las funciones no son solo para la computadora, sino también para los humanos. En general, a Python no le importa cómo se llame tu función ni qué comentarios contenga, pero esto es importante para los lectores humanos. Esta sección trata algunas cosas que deberías tener en cuenta al escribir funciones que los humanos puedan entender.

El nombre de una función es importante. Lo ideal es que el nombre de tu función sea corto, pero que evoque claramente lo que hace la función. ¡Eso es difícil! Pero es mejor ser claro que breve, ya que el autocompletado de Visual Studio Code facilita escribir nombres largos.

En general, los nombres de las funciones deberían ser verbos, y los argumentos, sustantivos. Hay algunas excepciones: los sustantivos están bien si la función calcula un sustantivo muy conocido (por ejemplo, mean es mejor que compute_mean) o si accede a alguna propiedad de un objeto (por ejemplo, coef es mejor que get_coefficients). Una buena señal de que un sustantivo podría ser mejor opción es que estés usando un verbo muy amplio como “get”, “compute”, “calculate” o “determine”. Usa tu mejor criterio y no tengas miedo de renombrar una función si más adelante se te ocurre un nombre mejor.

# Not a verb, or descriptive
my_awesome_function()
# Long, but clear
impute_missing()
collapse_years()

Si el nombre de tu función se compone de varias palabras, usa “snake_case”, donde cada palabra en minúsculas se separa con un guion bajo. Si tienes una familia de funciones que hacen cosas similares, asegúrate de que tengan nombres y argumentos coherentes. Usa un prefijo común para indicar que están relacionadas. Eso es mejor que un sufijo común, porque el autocompletado te permite escribir el prefijo y ver todos los miembros de la familia.

# Good
input_select()
input_checkbox()
input_text()
# Not so good
select_input()
checkbox_input()
text_input()

Un buen ejemplo de este diseño es el paquete pandas: si no recuerdas exactamente qué función necesitas para leer datos, puedes escribir pd.read_ y refrescar la memoria mientras el autocompletado te muestra las opciones.

Ámbito de las funciones

El ámbito (scope) se refiere a qué partes de tu código pueden ver qué otras partes. Hay tres ámbitos diferentes que debes tener en cuenta: local, global y no local.

Local

Si defines una variable dentro de una función, el resto de tu código no podrá ‘verla’ ni usarla. Por ejemplo, aquí tienes una función que crea una variable y, a continuación, un ejemplo de cómo llamar a esa variable:

def var_func():
    str_variable = 'Hello World!'

var_func()
print(str_variable)

Esto generaría un error porque, para el resto de tu código, str_variable no existe fuera de la función. Este es un ejemplo de una variable local, es decir, una que solo existe dentro de una función.

Si quieres crear variables dentro de una función y que persistan, tienes que devolverlas explícitamente usando, por ejemplo, return str_variable, así:

def var_func():
    str_variable = "Hello World!"
    return str_variable


returned_var = var_func()
print(returned_var)

Global

Una variable declarada fuera de una función se conoce como variable global porque es accesible desde cualquier parte:

y = "I'm a global variable"


def print_y():
    print("y is inside a function:", y)


print_y()
print("y is outside a function:", y)