Functions: The Building Blocks of Code
Bloques reutilizables de código para crear programas modulares y elegantes
Las funciones son uno de los conceptos más importantes en cualquier lenguaje de programación. Una función es un bloque de código reutilizable diseñado para realizar una tarea específica. Esta unidad puede ser importada y usada donde sea necesario, permitiendo escribir código más limpio, mantenible y eficiente.
En este capítulo exploraremos: qué son las funciones y por qué usarlas, scopes y resolución de nombres, parámetros de entrada y valores de retorno, funciones recursivas y anónimas, y cómo importar objetos para reutilizar código.
Anatomía de una Función
Una función en Python se define usando la palabra clave def, seguida del nombre de la función, paréntesis (que pueden contener parámetros de entrada), y dos puntos. El cuerpo de la función está indentado y contiene las instrucciones que se ejecutarán al llamar la función.
def greet(name): """Retorna un saludo personalizado.""" return f"Hello, {name}!" # Llamar a la función message = greet("Python Developer") print(message)
return explícitamente, la función retorna None automáticamente.
¿Por qué Usar Funciones?
Las funciones son fundamentales para escribir código profesional. Aquí están las principales razones por las que deberías usarlas constantemente:
Reducir Duplicación de Código
Imagina que calculas el IVA en múltiples lugares de tu aplicación. Sin funciones, tendrías código repetido que sería difícil de mantener:
# SIN función - código repetido y difícil de mantener price1 = 100 final_price1 = price1 * 1.2 # 20% VAT price2 = 250 final_price2 = price2 * 1.2 # 20% VAT - duplicado! price3 = 75 final_price3 = price3 * 1.2 # 20% VAT - duplicado otra vez!
# CON función - limpio y mantenible def calculate_price_with_vat(price, vat=20): """Calcula el precio con IVA incluido.""" return price * (100 + vat) / 100 # Ahora es fácil usar y modificar final_price1 = calculate_price_with_vat(100) # 120.0 final_price2 = calculate_price_with_vat(250) # 300.0 final_price3 = calculate_price_with_vat(75, 21) # 90.75 (21% VAT)
Dividir Tareas Complejas
Las funciones permiten descomponer procesos complejos en pasos más simples y manejables:
def do_report(data_source): """Genera un reporte completo desde una fuente de datos.""" # Obtener y preparar datos data = fetch_data(data_source) parsed_data = parse_data(data) filtered_data = filter_data(parsed_data) polished_data = polish_data(filtered_data) # Ejecutar análisis final_data = analyse(polished_data) # Crear y retornar reporte report = Report(final_data) return report
Mejorar la Legibilidad
Incluso para operaciones simples, una función con un nombre descriptivo mejora dramáticamente la legibilidad:
# Difícil de entender a primera vista c = [[sum(i * j for i, j in zip(r, c)) for c in zip(*b)] for r in a] # Mucho más claro con una función def matrix_mul(a, b): """Multiplica dos matrices a y b.""" return [ [sum(i * j for i, j in zip(r, c)) for c in zip(*b)] for r in a ] a = [[1, 2], [3, 4]] b = [[5, 1], [2, 1]] c = matrix_mul(a, b) # Claramente una multiplicación de matrices
Scopes y Resolución de Nombres
El scope (ámbito) determina dónde una variable es accesible en tu código. Python sigue la regla LEGB: Local, Enclosing, Global, Built-in.
def outer(): test = 1 # outer scope def inner(): test = 2 # inner scope (shadows outer) print("inner:", test) inner() print("outer:", test) test = 0 # global scope outer() print("global:", test)
outer: 1
global: 0
Las Sentencias Global y Nonlocal
Puedes modificar variables de scopes externos usando global y nonlocal:
def outer(): test = 1 # outer scope def inner(): nonlocal test # referencia al scope enclosing test = 2 print("inner:", test) inner() print("outer:", test) # ¡Ahora también es 2! outer()
outer: 2
def modify_global(): global counter # referencia al scope global counter = counter + 1 print("Inside function:", counter) counter = 0 # global scope modify_global() print("Global:", counter)
Global: 1
global se considera mala práctica. Puede hacer tu código difícil de seguir y propenso a bugs. Prefiere pasar valores como argumentos y retornar resultados.
Parámetros de Entrada
Python ofrece múltiples formas de pasar argumentos a funciones, proporcionando gran flexibilidad.
Argumentos Posicionales y Por Keyword
def func(a, b, c): print(a, b, c) # Argumentos posicionales func(1, 2, 3) # 1 2 3 # Argumentos por keyword (el orden no importa) func(c=3, a=1, b=2) # 1 2 3 # Combinación (posicionales primero) func(1, c=3, b=2) # 1 2 3
Parámetros con Valores por Defecto
def connect(host="localhost", port=5432, user="", pwd=""): """Conecta a una base de datos con parámetros opcionales.""" print(f"Connecting to {host}:{port} as {user or 'anonymous'}") # Usando valores por defecto connect() # localhost:5432 as anonymous # Sobrescribiendo algunos valores connect(host="192.168.1.100", user="admin") # Sobrescribiendo todo connect("db.server.com", 3306, "root", "secret")
Parámetros Variables: *args y **kwargs
def minimum(*numbers): """Encuentra el mínimo de cualquier cantidad de números.""" if not numbers: return None result = numbers[0] for num in numbers[1:]: if num < result: result = num return result print(minimum(5, 3, 8, 1, 9)) # 1 print(minimum(42)) # 42 print(minimum()) # None
def build_profile(name, **attributes): """Construye un perfil con atributos flexibles.""" profile = {"name": name} profile.update(attributes) return profile user = build_profile( "Alice", age=28, city="Tokyo", role="Developer", languages=["Python", "Rust"] ) print(user)
Parámetros Positional-Only y Keyword-Only
Python 3.8+ introdujo sintaxis especial para restringir cómo se pueden pasar argumentos:
# / marca el fin de parámetros positional-only # * marca el inicio de parámetros keyword-only def example(pos_only, /, pos_or_kw, *, kw_only): print(f"pos_only={pos_only}, pos_or_kw={pos_or_kw}, kw_only={kw_only}") # Correcto example(1, 2, kw_only=3) # ✓ example(1, pos_or_kw=2, kw_only=3) # ✓ # Incorrecto # example(pos_only=1, 2, kw_only=3) # ✗ Error: pos_only es positional-only # example(1, 2, 3) # ✗ Error: kw_only es keyword-only
Valores de Retorno
Las funciones pueden retornar cualquier tipo de objeto usando la sentencia return. También pueden retornar múltiples valores usando tuplas.
Retornar un Solo Valor
def factorial(n): """Calcula el factorial de n.""" if n in (0, 1): return 1 result = n for k in range(2, n): result *= k return result print(factorial(5)) # 120 print(factorial(0)) # 1 print(factorial(10)) # 3628800
Retornar Múltiples Valores
def divmod_custom(a, b): """Retorna el cociente y el resto de la división.""" return a // b, a % b # Retorna una tupla quotient, remainder = divmod_custom(20, 7) print(f"20 ÷ 7 = {quotient} remainder {remainder}") # También puedes recibir como tupla result = divmod_custom(20, 7) print(result) # (2, 6)
(2, 6)
Versión Funcional con reduce
from functools import reduce from operator import mul def factorial(n): """Factorial usando programación funcional.""" return reduce(mul, range(1, n + 1), 1) print(factorial(5)) # 120
¡Cuidado! Defaults Mutables
Uno de los errores más comunes en Python es usar objetos mutables como valores por defecto. Los defaults se crean una sola vez cuando la función se define, no cada vez que se llama.
# ⚠️ PELIGRO: Default mutable def add_item_wrong(item, items=[]): items.append(item) return items print(add_item_wrong("a")) # ['a'] print(add_item_wrong("b")) # ['a', 'b'] ← ¡Inesperado! print(add_item_wrong("c")) # ['a', 'b', 'c'] ← ¡La lista persiste!
['a', 'b']
['a', 'b', 'c']
# ✓ CORRECTO: Usar None como default def add_item_correct(item, items=None): if items is None: items = [] # Nueva lista en cada llamada items.append(item) return items print(add_item_correct("a")) # ['a'] print(add_item_correct("b")) # ['b'] ← ¡Correcto! print(add_item_correct("c")) # ['c'] ← ¡Lista fresca!
['b']
['c']
Funciones Recursivas
Una función recursiva es aquella que se llama a sí misma. Toda función recursiva debe tener un caso base que detenga la recursión y un caso recursivo que reduce el problema.
def factorial(n): """Calcula factorial recursivamente: n! = n × (n-1)!""" # Caso base if n in (0, 1): return 1 # Caso recursivo return n * factorial(n - 1) # Visualización: factorial(5) # 5 * factorial(4) # 5 * 4 * factorial(3) # 5 * 4 * 3 * factorial(2) # 5 * 4 * 3 * 2 * factorial(1) # 5 * 4 * 3 * 2 * 1 = 120 print(factorial(5)) # 120
Fibonacci Recursivo
def fibonacci(n): """Calcula el n-ésimo número de Fibonacci.""" if n <= 1: return n return fibonacci(n - 1) + fibonacci(n - 2) # Primeros 10 números de Fibonacci for i in range(10): print(fibonacci(i), end=" ")
sys.getrecursionlimit() y modificarlo con sys.setrecursionlimit(). Para recursiones muy profundas, considera usar iteración.
Funciones Anónimas: Lambda
Las funciones lambda son funciones pequeñas y anónimas definidas en una sola línea. Son útiles cuando necesitas una función simple como argumento de otra función.
lambda argumentos: expresiónEquivale a:
def func(argumentos): return expresión
# Función tradicional def add(a, b): return a + b # Equivalente con lambda add_lambda = lambda a, b: a + b print(add(3, 5)) # 8 print(add_lambda(3, 5)) # 8 # Más ejemplos square = lambda x: x ** 2 to_upper = lambda s: s.upper() print(square(7)) # 49 print(to_upper("hello")) # HELLO
Lambda con filter() y map()
# Filtrar múltiplos de 5 def get_multiples_of_five(n): return list(filter(lambda x: x % 5 == 0, range(n))) print(get_multiples_of_five(30)) # [0, 5, 10, 15, 20, 25] # Elevar al cuadrado cada elemento numbers = [1, 2, 3, 4, 5] squared = list(map(lambda x: x ** 2, numbers)) print(squared) # [1, 4, 9, 16, 25] # Ordenar por segundo elemento pairs = [(1, 'b'), (3, 'a'), (2, 'c')] sorted_pairs = sorted(pairs, key=lambda x: x[1]) print(sorted_pairs) # [(3, 'a'), (1, 'b'), (2, 'c')]
Documentando tu Código
Los docstrings son cadenas de documentación que describen qué hace una función. Son accesibles mediante help() y el atributo __doc__.
def connect(host, port, user, password): """Connect to a database. Connect to a PostgreSQL database directly, using the given parameters. :param host: The host IP. :param port: The desired port. :param user: The connection username. :param password: The connection password. :return: The connection object. """ # Implementación aquí... pass # Acceder a la documentación print(connect.__doc__) # O usar help(connect) en el intérprete
Atributos de Función
def multiply(a, b=1): """Return a multiplied by b.""" return a * b # Inspeccionar atributos print("Name:", multiply.__name__) # multiply print("Doc:", multiply.__doc__) # Return a multiplied by b. print("Defaults:", multiply.__defaults__) # (1,) print("Module:", multiply.__module__) # __main__
Importando Objetos
Python permite reutilizar código importando módulos y funciones. Existen varias formas de hacerlo:
# Importar módulo completo import math print(math.sqrt(16)) # 4.0 # Importar funciones específicas from math import sqrt, ceil print(sqrt(25)) # 5.0 print(ceil(4.2)) # 5 # Importar con alias import numpy as np from datetime import datetime as dt # Importar desde tu propio módulo from mypackage.utils import helper_function # Import relativo (dentro de un paquete) from .mymodule import myfunc from ..parent import something
from module import *. Puede causar conflictos de nombres y hace difícil rastrear de dónde vienen las funciones.
Estructura de un Paquete
my_project/
├── main.py
├── utils/
│ ├── __init__.py
│ ├── math_helpers.py
│ └── string_helpers.py
└── tests/
├── __init__.py
└── test_utils.py
__init__.py marca un directorio como un paquete de Python. Desde Python 3.3, no es estrictamente necesario pero sigue siendo una buena práctica.
Buenas Prácticas
Sigue estas guidelines para escribir funciones de alta calidad:
- Consistencia en retornos: Retorna siempre el mismo tipo.
FalseyNoneno son lo mismo. - Sin efectos secundarios: No modifiques variables globales o argumentos mutables inesperadamente.
- Nombres descriptivos:
calculate_tax()es mejor quecalc()odo_stuff(). - Documenta: Usa docstrings para explicar qué hace la función, sus parámetros y retorno.
# ✓ Función pura: mismo input = mismo output, sin efectos secundarios def calculate_area(radius): """Calcula el área de un círculo dado su radio.""" import math return math.pi * radius ** 2 # ✗ Función con efectos secundarios (evitar) results = [] def add_result_bad(value): results.append(value) # Modifica estado global! # ✓ Mejor alternativa def add_result_good(results, value): return results + [value] # Retorna nueva lista
Resumen del Capítulo
01 // Definición
Las funciones se definen con def, aceptan parámetros y retornan valores con return.
02 // Scopes
Python sigue la regla LEGB: Local, Enclosing, Global, Built-in para resolver nombres.
03 // Parámetros
Posicionales, keyword, *args, **kwargs, defaults, positional-only y keyword-only.
04 // Lambda
Funciones anónimas de una línea: lambda args: expresión.
05 // Recursión
Funciones que se llaman a sí mismas con caso base y caso recursivo.
06 // Imports
Reutiliza código con import module o from module import func.