Sesión 6 - Funciones en Python

Introducción

Hasta ahora hemos aprendido sobre tipos de datos básicos (int, float, bool, str), operadores, estructuras de control (if, elif, else), ciclos (while, for), y listas. Estos elementos nos permiten escribir programas, pero a medida que estos crecen en complejidad, es necesario organizar nuestro código para hacerlo más legible, reutilizable y fácil de mantener. Aquí es donde entran las funciones.

Las funciones son bloques de código que realizan una tarea específica y pueden ser reutilizados en diferentes partes de un programa. Son fundamentales para la programación modular, donde dividimos un problema grande en partes más pequeñas y manejables.

Definición de Funciones

En Python, definimos una función usando la palabra clave def, seguida del nombre de la función y paréntesis que pueden contener parámetros. La definición termina con dos puntos (:) y el cuerpo de la función debe estar indentado.

Sintaxis básica:

def nombre_funcion(parametro1, parametro2, ...):
    # Cuerpo de la función
    # Código que realiza una tarea específica
    return valor  # Opcional

Ejemplo simple:

def saludar():
    print("¡Hola, mundo!")

# Llamada a la función
saludar()  # Imprime: ¡Hola, mundo!

Parámetros y Argumentos

Los parámetros son variables que se definen en la declaración de la función, mientras que los argumentos son los valores que se pasan a la función cuando se llama.

def saludar_persona(nombre):  # 'nombre' es un parámetro
    print(f"¡Hola, {nombre}!")

saludar_persona("Ana")  # "Ana" es un argumento
saludar_persona("Carlos")  # "Carlos" es un argumento

Múltiples parámetros:

def describir_persona(nombre, edad, ciudad):
    print(f"{nombre} tiene {edad} años y vive en {ciudad}.")

describir_persona("Juan", 25, "Bogotá")

Parámetros con Valores por Defecto

Python permite asignar valores predeterminados (por defecto) a los parámetros, lo que hace que sean opcionales al momento de llamar a la función.

def saludar(nombre, mensaje="¡Hola!"):
    print(f"{mensaje} {nombre}")

saludar("María")  # Usa el mensaje por defecto: ¡Hola! María
saludar("Pedro", "¡Buenos días!")  # Usa el mensaje personalizado: ¡Buenos días! Pedro

Esto es especialmente útil cuando una función tiene muchos parámetros, pero la mayoría de las veces solo se necesitan algunos de ellos:

def crear_descripcion(nombre, edad, ciudad="Desconocida", profesion="No especificada"):
    return f"{nombre} tiene {edad} años, vive en {ciudad} y trabaja como {profesion}."

# Solo proporcionamos nombre y edad, usando valores por defecto para los demás
perfil1 = crear_descripcion("Ana", 28)
print(perfil1)  # Ana tiene 28 años, vive en Desconocida y trabaja como No especificada.

# Especificamos todos los valores
perfil2 = crear_descripcion("Carlos", 35, "Madrid", "Ingeniero")
print(perfil2)  # Carlos tiene 35 años, vive en Madrid y trabaja como Ingeniero.

# Podemos omitir algunos parámetros intermedios usando argumentos nombrados
perfil3 = crear_descripcion("Laura", 42, profesion="Doctora")
print(perfil3)  # Laura tiene 42 años, vive en Desconocida y trabaja como Doctora.

Consideraciones importantes:

  1. Los parámetros con valores por defecto deben definirse después de los parámetros sin valores por defecto.

  2. Los valores por defecto se evalúan solo una vez, cuando se define la función.

  3. Para objetos mutables (como listas o diccionarios), es recomendable usar None como valor por defecto y luego inicializar el objeto dentro de la función:

# Forma correcta
def añadir_item(item, lista=None):
    if lista is None:
        lista = []
    lista.append(item)
    return lista

# Forma incorrecta (puede causar comportamientos inesperados)
def añadir_item_incorrecto(item, lista=[]):
    lista.append(item)
    return lista

# Demostración
print(añadir_item(1))  # [1]
print(añadir_item(2))  # [2]

print(añadir_item_incorrecto(1))  # [1]
print(añadir_item_incorrecto(2))  # [1, 2] - ¡La lista se comparte entre llamadas!

Valores de Retorno

Las funciones pueden devolver valores usando la palabra clave return. Una vez que se ejecuta una declaración return, la función termina y devuelve el valor especificado.

def sumar(a, b):
    resultado = a + b
    return resultado

total = sumar(5, 3)  # total = 8
print(total)  # Imprime: 8

Una función puede devolver múltiples valores como una tupla:

def operaciones_basicas(a, b):
    suma = a + b
    resta = a - b
    return suma, resta  # Devuelve una tupla

s, r = operaciones_basicas(10, 4)
print(f"Suma: {s}, Resta: {r}")  # Imprime: Suma: 14, Resta: 6

Docstrings

Las docstrings son cadenas de texto que se colocan justo después de la definición de una función y sirven para documentar qué hace la función, qué parámetros recibe y qué valor retorna. Usar docstrings es una buena práctica porque hace que el código sea más comprensible y facilita su mantenimiento.

Sintaxis básica:

def nombre_funcion(parametros):
    """
    Descripción de lo que hace la función.

    Args:
        param1: Descripción del primer parámetro
        param2: Descripción del segundo parámetro

    Returns:
        Descripción de lo que retorna la función
    """
    # Cuerpo de la función
    return resultado

Ejemplo:

def calcular_area_circulo(radio):
    """
    Calcula el área de un círculo.

    Args:
        radio: El radio del círculo en unidades arbitrarias

    Returns:
        El área del círculo
    """
    import math
    return math.pi * radio ** 2

# Acceder a la docstring
print(calcular_area_circulo.__doc__)
# También puedes usar la función help()
help(calcular_area_circulo)

Los docstrings son accesibles a través de la propiedad __doc__ de la función o mediante la función help(). Esto permite que otros programadores (o tú mismo en el futuro) entiendan cómo usar tu función sin tener que leer todo su código.

Anotaciones de Tipo (Type Hints)

Python es un lenguaje de tipado dinámico, lo que significa que no necesitas declarar el tipo de una variable cuando la creas. Sin embargo, a partir de Python 3.5, se introdujeron las anotaciones de tipo (type hints), que permiten indicar de manera explícita qué tipos de datos se esperan como parámetros y qué tipo de dato se retorna en una función.

Las anotaciones de tipo no afectan la ejecución del programa, pero son útiles por varias razones:

  1. Documentación: Hacen el código más legible y autodocumentado

  2. Validación: Herramientas como mypy pueden verificar posibles errores de tipo

  3. Compatibilidad con IDEs: Mejoran las sugerencias de autocompletado y la detección de errores en editores como PyCharm, VSCode, etc.

Sintaxis básica:

def nombre_funcion(parametro1: tipo1, parametro2: tipo2) -> tipo_retorno:
    # Cuerpo de la función
    return valor

Ejemplos:

def saludar(nombre: str) -> str:
    return f"Hola, {nombre}!"

def sumar(a: int, b: int) -> int:
    return a + b

def es_mayor_de_edad(edad: int) -> bool:
    return edad >= 18

Para tipos más complejos como listas, diccionarios o tipos opcionales, se utiliza el módulo typing:

from typing import List, Optional, Tuple

def procesar_numeros(numeros: List[float]) -> float:
    """Calcula el promedio de una lista de números."""
    return sum(numeros) / len(numeros)

def obtener_usuario(id_usuario: int) -> Optional[str]:
     """
     Busca un usuario por su ID.
     Retorna None si no se encuentra, o un string con la información si existe.
     """
     # Simulación de búsqueda
     if id_usuario > 0:
         return f"Usuario con ID {id_usuario}, edad: 25 años"
     return None

def operaciones(a: int, b: int) -> Tuple[int, int, float]:
    """Realiza suma, resta y división entre dos números."""
    suma = a + b
    resta = a - b
    division = a / b if b != 0 else 0
    return suma, resta, division

Las anotaciones de tipo hacen que tu código sea más profesional, facilitan el trabajo en equipo y ayudan a prevenir errores comunes de tipo durante el desarrollo.

Manejo de Errores en Funciones

Cuando escribimos funciones, es importante considerar los posibles errores que pueden ocurrir durante su ejecución. Python proporciona el mecanismo try/except para manejar excepciones y hacer que nuestras funciones sean más robustas.

Sintaxis básica:

def funcion():
    try:
        # Código que puede generar una excepción
        resultado = operacion_riesgosa()
        return resultado
    except TipoDeExcepcion:
        # Código para manejar la excepción
        return valor_alternativo

Ejemplo práctico:

def dividir(a: float, b: float) -> float:
    """
    Divide dos números con manejo de error para división por cero.

    Args:
        a: Numerador
        b: Denominador

    Returns:
        El resultado de la división, o 0 si hay división por cero
    """
    try:
        return a / b
    except ZeroDivisionError:
        print("Error: No es posible dividir por cero")
        return 0  # Retornamos un valor por defecto

# Ejemplo de uso:
print(dividir(10, 2))   # 5.0
print(dividir(10, 0))   # Imprime mensaje de error y retorna 0

El manejo de errores es especialmente útil cuando:

  1. Trabajamos con entrada de usuario

  2. Realizamos operaciones matemáticas que pueden causar errores (división por cero, raíz cuadrada de números negativos, etc.)

  3. Accedemos a recursos externos como archivos o conexiones de red

  4. Procesamos datos de formato desconocido o variable

Funciones Lambda

Las funciones lambda (o funciones anónimas) permiten crear funciones pequeñas y de un solo uso de forma concisa, sin necesidad de utilizar la sintaxis completa de def. Son útiles especialmente cuando necesitamos pasar una función simple como argumento a otra función.

Sintaxis básica:

lambda parametros: expresion

Ejemplos:

# Función lambda que calcula el cuadrado de un número
cuadrado = lambda x: x**2

# Equivalente usando def
def cuadrado_normal(x):
    return x**2

# Uso de la función lambda
print(cuadrado(5))  # 25

# Lambda con múltiples parámetros
suma = lambda a, b: a + b
print(suma(3, 4))  # 7

# Uso común con funciones como map()
numeros = [1, 2, 3, 4, 5]
cuadrados = list(map(lambda x: x**2, numeros))
print(cuadrados)  # [1, 4, 9, 16, 25]

# Uso con filter()
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pares = list(filter(lambda x: x % 2 == 0, numeros))
print(pares)  # [2, 4, 6, 8, 10]

Limitaciones de las funciones lambda:

  1. Solo pueden contener una expresión (no múltiples líneas de código)

  2. No se pueden incluir declaraciones (como if, for, etc.) de forma directa

  3. No tienen nombre, lo que dificulta la depuración

  4. No permiten docstrings

Las funciones lambda son ideales para operaciones simples, pero para lógica más compleja es mejor usar funciones normales definidas con def.

Funciones de Alto Orden

Las funciones de alto orden son funciones que pueden recibir otras funciones como parámetros o que retornan funciones como resultado. Este concepto es fundamental en la programación funcional y Python lo soporta completamente.

Funciones que Reciben Funciones

Muchas funciones integradas en Python funcionan de esta manera:

def aplicar_operacion(func, valor):
    """
    Aplica una función a un valor dado.

    Args:
        func: La función a aplicar
        valor: El valor sobre el cual aplicar la función

    Returns:
        El resultado de aplicar func a valor
    """
    return func(valor)

# Usando la función con diferentes operaciones
def cuadrado(x):
    return x * x

def doble(x):
    return x * 2

print(aplicar_operacion(cuadrado, 5))  # 25
print(aplicar_operacion(doble, 5))     # 10
print(aplicar_operacion(lambda x: x + 10, 5))  # 15

Funciones Integradas de Alto Orden

Python incluye varias funciones de alto orden muy útiles:

  1. map(): Aplica una función a cada elemento de un iterable

    numeros = [1, 2, 3, 4, 5]
    cuadrados = list(map(lambda x: x**2, numeros))
    print(cuadrados)  # [1, 4, 9, 16, 25]
    
    # Equivalente con comprensión de listas
    cuadrados_comp = [x**2 for x in numeros]
    
  2. filter(): Filtra elementos de un iterable según una función que retorna booleanos

    numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    pares = list(filter(lambda x: x % 2 == 0, numeros))
    print(pares)  # [2, 4, 6, 8, 10]
    
    # Equivalente con comprensión de listas
    pares_comp = [x for x in numeros if x % 2 == 0]
    
  3. sorted(): Ordena un iterable utilizando una función clave opcional

    # Lista de tuplas (nombre, promedio)
    estudiantes = [
        ("Ana", 9.5),
        ("Carlos", 8.2),
        ("María", 9.8)
    ]
    
    # Ordenar por promedio (de mayor a menor)
    ordenados = sorted(estudiantes, key=lambda estudiante: estudiante[1], reverse=True)
    for nombre, promedio in ordenados:
        print(f"{nombre}: {promedio}")
    
    # Otro ejemplo: ordenar palabras por su longitud
    palabras = ["python", "es", "un", "lenguaje", "poderoso"]
    palabras_ordenadas = sorted(palabras, key=len)
    print(palabras_ordenadas)  # ['es', 'un', 'python', 'lenguaje', 'poderoso']
    
  4. reduce(): Aplica una función a pares de elementos para reducirlos a un solo valor

    from functools import reduce
    
    numeros = [1, 2, 3, 4, 5]
    producto = reduce(lambda x, y: x * y, numeros)
    print(producto)  # 120 (1*2*3*4*5)
    

Funciones que Retornan Funciones

También podemos crear funciones que devuelven otras funciones, lo que permite crear clausuras (closures) y fábricas de funciones:

def crear_multiplicador(factor):
    """
    Crea y retorna una función que multiplica por el factor dado.

    Args:
        factor: El número por el cual multiplicar

    Returns:
        Una función que toma un número y lo multiplica por factor
    """
    def multiplicador(x):
        return x * factor
    return multiplicador

duplicar = crear_multiplicador(2)
triplicar = crear_multiplicador(3)

print(duplicar(5))   # 10
print(triplicar(5))  # 15

Este patrón es muy útil para crear funciones especializadas a partir de funciones más generales, lo que aumenta la reutilización del código.

Alcance de Variables (Scope)

El alcance de una variable se refiere a la región del programa donde la variable es accesible. En Python, existen principalmente dos tipos de alcance:

  1. Local: Variables definidas dentro de una función

  2. Global: Variables definidas en el nivel principal del programa

x = 10  # Variable global

def funcion():
    y = 5  # Variable local
    print(x)  # Puede acceder a la variable global
    print(y)  # Puede acceder a la variable local

funcion()
print(x)  # Puede acceder a la variable global
# print(y)  # Error: y no está definida fuera de la función

Si intentamos modificar una variable global dentro de una función, Python creará una nueva variable local con el mismo nombre. Para modificar una variable global, debemos usar la palabra clave global:

contador = 0

def incrementar():
    global contador  # Indica que usaremos la variable global
    contador += 1

incrementar()
print(contador)  # Imprime: 1

Mejores Prácticas para Funciones

Al escribir funciones en Python, seguir estas prácticas recomendadas te ayudará a crear código más limpio, mantenible y eficiente:

  1. Nombres descriptivos: Usa nombres claros que describan lo que hace la función. - ✅ calcular_area_triangulo() - ❌ cal_a() o funcion1()

  2. Responsabilidad única: Cada función debe hacer una sola cosa y hacerla bien. - ✅ Funciones pequeñas y enfocadas - ❌ Funciones que hacen múltiples tareas no relacionadas

  3. Longitud adecuada: Intenta mantener tus funciones cortas (15-20 líneas como máximo). - Si una función es muy larga, probablemente pueda dividirse en funciones más pequeñas.

  4. Número de parámetros: Limita el número de parámetros (idealmente 4 o menos). - Demasiados parámetros dificultan la legibilidad y el uso de la función. - Considera usar un diccionario para múltiples parámetros opcionales.

  5. Valores por defecto razonables: Usa valores por defecto que sean útiles para el caso más común.

  6. Evita efectos secundarios: Las funciones no deberían modificar variables globales o cambiar valores de entrada. - ✅ Retornar un nuevo objeto con cambios - ❌ Modificar directamente un objeto pasado como parámetro (a menos que sea el propósito explícito)

  7. Manejo de errores adecuado: Anticipa posibles errores y manéjalos apropiadamente. - Usa try/except cuando sea necesario - Valida entradas al inicio de la función

  8. Documentación clara: Escribe docstrings que expliquen el propósito, parámetros y valores de retorno.

  9. Consistencia en el estilo: Mantén un estilo coherente en todas tus funciones. - Sigue PEP 8 para la convención de nombres (snake_case para funciones)

  10. DRY (Don’t Repeat Yourself): Si encuentras código duplicado, es una señal de que deberías crear una función.

  11. Early return: Devuelve resultados temprano para casos especiales o errores, evitando anidación excesiva.

    # Mejor (con early return)
    def procesar_dato(valor):
        if valor < 0:
            return None  # Early return para caso especial
    
        # Continuar con el procesamiento normal
        resultado = valor * 2
        return resultado
    
    # Peor (sin early return)
    def procesar_dato(valor):
        if valor >= 0:
            resultado = valor * 2
            return resultado
        else:
            return None
    

Siguiendo estas prácticas, tu código será más legible, mantenible y tendrás menos errores, lo que se traduce en menos tiempo de depuración y más productividad.

Ejercicios Prácticos

Ejercicio 1: Distancia Euclidiana

Escribe una función que calcule la distancia euclidiana entre dos puntos en un plano 2D. La función debe recibir las coordenadas (x1, y1) y (x2, y2) como parámetros.

def distancia_euclidiana(x1: float, y1: float, x2: float, y2: float) -> float:
    """
    Calcula la distancia euclidiana entre dos puntos (x1, y1) y (x2, y2).

    Args:
        x1: Coordenada x del primer punto
        y1: Coordenada y del primer punto
        x2: Coordenada x del segundo punto
        y2: Coordenada y del segundo punto

    Returns:
        La distancia euclidiana entre los dos puntos
    """
    import math
    dx = x2 - x1
    dy = y2 - y1
    return math.sqrt(dx**2 + dy**2)

# Ejemplo de uso:
# distancia = distancia_euclidiana(1, 2, 4, 6)
# print(f"La distancia es: {distancia}")  # Imprime: La distancia es: 5.0

Ejercicio 2: Invertir una Cadena

Implementa una función que invierta una cadena de texto.

def invertir_cadena(texto: str) -> str:
    """
    Invierte el orden de los caracteres en una cadena.

    Args:
        texto: La cadena a invertir

    Returns:
        La cadena invertida
    """
    return texto[::-1]

# Ejemplo de uso:
# original = "Python"
# invertida = invertir_cadena(original)
# print(f"Original: {original}, Invertida: {invertida}")
# # Imprime: Original: Python, Invertida: nohtyP

Ejercicio 3: Suma de una Lista

Escribe una función que calcule la suma de todos los elementos de una lista.

from typing import List

def sumar_lista(numeros: List[float]) -> float:
    """
    Suma todos los elementos de una lista.

    Args:
        numeros: Lista de números a sumar

    Returns:
        La suma de todos los elementos
    """
    total = 0
    for num in numeros:
        total += num
    return total

# Ejemplo de uso:
# lista = [1, 2, 3, 4, 5]
# suma = sumar_lista(lista)
# print(f"La suma de {lista} es: {suma}")  # Imprime: La suma de [1, 2, 3, 4, 5] es: 15

Ejercicio 4: Verificación de Número Primo

Crea una función que verifique si un número es primo.

def es_primo(numero: int) -> bool:
    """
    Verifica si un número es primo.

    Args:
        numero: El número a verificar

    Returns:
        True si el número es primo, False en caso contrario
    """
    if numero <= 1:
        return False

    if numero == 2:
        return True

    if numero % 2 == 0:
        return False

    # Verificamos solo los divisores impares hasta la raíz cuadrada
    i = 3
    while i * i <= numero:
        if numero % i == 0:
            return False
        i += 2

    return True

# Ejemplo de uso:
# for n in range(1, 21):
#     print(f"{n} es primo: {es_primo(n)}")

Ejercicio 5: Convertidor de Temperatura

Escribe dos funciones: una que convierta de grados Celsius a Fahrenheit y otra que haga la conversión inversa.

def celsius_a_fahrenheit(celsius: float) -> float:
    """
    Convierte una temperatura de grados Celsius a Fahrenheit.

    Args:
        celsius: Temperatura en grados Celsius

    Returns:
        Temperatura en grados Fahrenheit
    """
    return (celsius * 9/5) + 32

def fahrenheit_a_celsius(fahrenheit: float) -> float:
    """
    Convierte una temperatura de grados Fahrenheit a Celsius.

    Args:
        fahrenheit: Temperatura en grados Fahrenheit

    Returns:
        Temperatura en grados Celsius
    """
    return (fahrenheit - 32) * 5/9

# Ejemplo de uso:
# temp_c = 25
# temp_f = celsius_a_fahrenheit(temp_c)
# print(f"{temp_c}°C = {temp_f}°F")
#
# temp_f = 98.6
# temp_c = fahrenheit_a_celsius(temp_f)
# print(f"{temp_f}°F = {temp_c}°C")

Ejercicio 6: Factorial

Implementa una función que calcule el factorial de un número entero no negativo.

def factorial(n: int) -> int:
    """
    Calcula el factorial de un número entero no negativo.

    Args:
        n: Número entero no negativo

    Returns:
        El factorial de n

    Raises:
        ValueError: Si n es negativo
    """
    if n < 0:
        raise ValueError("El factorial no está definido para números negativos")

    if n == 0 or n == 1:
        return 1

    resultado = 1
    for i in range(2, n + 1):
        resultado *= i

    return resultado

# Ejemplo de uso:
# for i in range(6):
#     print(f"{i}! = {factorial(i)}")

Ejercicio 7: Contador de Vocales

Crea una función que cuente el número de vocales en una cadena de texto.

def contar_vocales(texto: str) -> int:
    """
    Cuenta el número de vocales en una cadena de texto.

    Args:
        texto: La cadena de texto a analizar

    Returns:
        El número de vocales encontradas
    """
    vocales = "aeiouAEIOU"
    contador = 0
    for caracter in texto:
        if caracter in vocales:
            contador += 1
    return contador

# Ejemplo de uso:
# texto = "Hola, mundo"
# num_vocales = contar_vocales(texto)
# print(f"'{texto}' tiene {num_vocales} vocales")

Ejercicio 8: Máximo Común Divisor

Implementa una función que calcule el MCD de dos números utilizando el algoritmo de Euclides.

def mcd(a: int, b: int) -> int:
    """
    Calcula el máximo común divisor de dos números enteros usando el algoritmo de Euclides.

    Args:
        a: Primer número entero
        b: Segundo número entero

    Returns:
        El máximo común divisor de a y b
    """
    while b:
        a, b = b, a % b
    return a

# Ejemplo de uso:
# print(f"MCD de 48 y 18: {mcd(48, 18)}")  # Debería imprimir 6

Ejercicio 9: Validador de Contraseñas

Desarrolla una función que verifique si una contraseña cumple con ciertos criterios: - Longitud mínima de 8 caracteres - Al menos una letra mayúscula - Al menos una letra minúscula - Al menos un número

Ejercicio 10: Función con Parámetros por Defecto

Crea una función para generar un mensaje personalizado que incluya los parámetros: nombre, edad, y ocupación, donde edad y ocupación tengan valores por defecto.

Conclusión

Las funciones son una herramienta fundamental en Python y en la programación en general. Nos permiten:

  • Modularizar nuestro código

  • Reutilizar bloques de código

  • Hacer nuestro código más legible y mantenible

  • Abstraer la complejidad

Los parámetros por defecto aumentan la flexibilidad de nuestras funciones, permitiendo que tengan comportamientos predeterminados cuando no se proporcionan todos los argumentos.

Las docstrings, anotaciones de tipo y el manejo adecuado de errores nos ayudan a escribir código más robusto y profesional.

Las funciones lambda nos proporcionan una forma concisa de definir funciones simples cuando es necesario.

En la robótica y programación en Webots, las funciones nos ayudarán a organizar nuestro código para controlar sensores, actuadores y comportamientos del robot de manera más estructurada y eficiente.

Recuerda siempre documentar tus funciones y seguir las convenciones de Python para nombrarlas (nombres en minúsculas separados por guiones bajos).

Referencias