lunes, 19 de enero de 2026

Aprendiendo Python de 0 a experto - Funciones

Functions: The Building Blocks of Code
Python // Chapter 04

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.

FUNC_01

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.

Estructura de una Función
INPUT
FUNCTION
OUTPUT
basic_function.py
def greet(name):
    """Retorna un saludo personalizado."""
    return f"Hello, {name}!"

# Llamar a la función
message = greet("Python Developer")
print(message)
python basic_function.py
Hello, Python Developer!
Una función siempre retorna algo en Python. Si no usas return explícitamente, la función retorna None automáticamente.
FUNC_02

¿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:

REUSABILIDAD
Escribe una vez, usa múltiples veces sin duplicar código
MODULARIDAD
Divide tareas complejas en bloques manejables
LEGIBILIDAD
Código más fácil de leer y entender

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:

vat_without_function.py
# 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!
vat_with_function.py
# 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:

report_generator.py
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
Este patrón hace que el código sea fácil de entender sin necesidad de leer cada detalle de implementación. Si algo falla, es sencillo identificar qué parte del proceso necesita debugging.

Mejorar la Legibilidad

Incluso para operaciones simples, una función con un nombre descriptivo mejora dramáticamente la legibilidad:

matrix_multiplication.py
# 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
FUNC_03

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.

Regla LEGB
Built-in
Global
Enclosing
Local
scoping_levels.py
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)
python scoping_levels.py
inner: 2
outer: 1
global: 0

Las Sentencias Global y Nonlocal

Puedes modificar variables de scopes externos usando global y nonlocal:

nonlocal_example.py
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()
python nonlocal_example.py
inner: 2
outer: 2
global_example.py
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)
python global_example.py
Inside function: 1
Global: 1
El uso excesivo de 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.
FUNC_04

Parámetros de Entrada

Python ofrece múltiples formas de pasar argumentos a funciones, proporcionando gran flexibilidad.

Argumentos Posicionales y Por Keyword

arguments_types.py
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

default_parameters.py
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

Tipos de Parámetros Variables
*args
→ tuple
**kwargs
→ dict
variable_parameters.py
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
kwargs_example.py
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)
python kwargs_example.py
{'name': 'Alice', 'age': 28, 'city': 'Tokyo', 'role': 'Developer', 'languages': ['Python', 'Rust']}

Parámetros Positional-Only y Keyword-Only

Python 3.8+ introdujo sintaxis especial para restringir cómo se pueden pasar argumentos:

parameter_restrictions.py
# / 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
Los parámetros positional-only son útiles para emular funciones built-in de C y para evitar que los nombres de parámetros se conviertan en parte de la API pública.
FUNC_05

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

factorial.py
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

multiple_returns.py
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)
python multiple_returns.py
20 ÷ 7 = 2 remainder 6
(2, 6)

Versión Funcional con reduce

factorial_functional.py
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
FUNC_06

¡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.

mutable_default_trap.py
# ⚠️ 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!
python mutable_default_trap.py
['a']
['a', 'b']
['a', 'b', 'c']
La misma lista se reutiliza en cada llamada porque se creó una sola vez cuando Python definió la función.
mutable_default_correct.py
# ✓ 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!
python mutable_default_correct.py
['a']
['b']
['c']
FUNC_07

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.

Estructura Recursiva
Caso Base
Recursión
Caso Recursivo
recursive_factorial.py
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

fibonacci.py
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=" ")
python fibonacci.py
0 1 1 2 3 5 8 13 21 34
Python tiene un límite de recursión (por defecto ~1000 llamadas). Puedes verificarlo con sys.getrecursionlimit() y modificarlo con sys.setrecursionlimit(). Para recursiones muy profundas, considera usar iteración.
FUNC_08

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.

Sintaxis Lambda
lambda argumentos: expresión

Equivale a:
def func(argumentos): return expresión
lambda_examples.py
# 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()

lambda_filter_map.py
# 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')]
FUNC_09

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__.

docstrings.py
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

function_attributes.py
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__
FUNC_10

Importando Objetos

Python permite reutilizar código importando módulos y funciones. Existen varias formas de hacerlo:

import_styles.py
# 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
Evita usar from module import *. Puede causar conflictos de nombres y hace difícil rastrear de dónde vienen las funciones.

Estructura de un Paquete

project_structure
my_project/
├── main.py
├── utils/
│   ├── __init__.py
│   ├── math_helpers.py
│   └── string_helpers.py
└── tests/
    ├── __init__.py
    └── test_utils.py
El archivo __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.
FUNC_11

Buenas Prácticas

Sigue estas guidelines para escribir funciones de alta calidad:

UNA COSA
Cada función debe hacer una sola cosa y hacerla bien
PEQUEÑAS
Funciones cortas son más fáciles de probar y mantener
POCOS PARAMS
Idealmente 0-3 parámetros; más dificulta el uso
  • Consistencia en retornos: Retorna siempre el mismo tipo. False y None no son lo mismo.
  • Sin efectos secundarios: No modifiques variables globales o argumentos mutables inesperadamente.
  • Nombres descriptivos: calculate_tax() es mejor que calc() o do_stuff().
  • Documenta: Usa docstrings para explicar qué hace la función, sus parámetros y retorno.
pure_function_example.py
# ✓ 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.

Basado en "Learn Python Programming, 4th Edition" // Fabrizio Romano & Heinrich Kruger

Chapter 4: Functions, the Building Blocks of Code