Python & AI Tutorials Logo
Programación Python

23. Funciones de primera clase y técnicas funcionales

En capítulos anteriores, aprendimos a definir y llamar funciones, trabajar con parámetros y argumentos, y comprender el alcance de las variables. Ahora exploraremos una característica poderosa que distingue a Python: las funciones son objetos de primera clase. Esto significa que las funciones pueden tratarse como cualquier otro valor: almacenarse en variables, pasarse como argumentos a otras funciones y devolverse desde funciones.

Esta capacidad abre técnicas de programación elegantes que hacen que el código sea más flexible, reutilizable y expresivo. Exploraremos cómo aprovechar las funciones de primera clase mediante ejemplos prácticos, comprender closures (funciones que "recuerdan" su entorno), usar expresiones lambda para definiciones de funciones concisas y aplicar funciones integradas como map(), filter(), any() y all() para trabajar con colecciones de forma eficiente.

23.1) Funciones como objetos de primera clase

23.1.1) Qué significa "primera clase"

En Python, las funciones son objetos de primera clase, lo que significa que pueden ser:

  • Asignadas a variables
  • Almacenadas en estructuras de datos (listas, diccionarios, etc.)
  • Pasadas como argumentos a otras funciones
  • Devueltas como valores desde otras funciones

Esto es diferente de algunos lenguajes de programación donde las funciones tienen un estatus especial y no se pueden manipular como valores normales. En Python, una función es simplemente otro tipo de objeto, similar a los enteros, las cadenas o las listas.

Veamos esto en acción:

python
# Define una función simple
def greet(name):
    return f"Hello, {name}!"
 
# Asigna la función a una variable
say_hello = greet
 
# Llama a la función a través de la nueva variable
message = say_hello("Alice")
print(message)  # Output: Hello, Alice!
 
# Comprueba que ambos nombres se refieren a la misma función
print(greet)      # Output: <function greet at 0x...>
print(say_hello)  # Output: <function greet at 0x...>
print(greet is say_hello)  # Output: True

Fíjate en que cuando escribimos say_hello = greet, no estamos llamando a la función (sin paréntesis). Estamos creando un nuevo nombre que se refiere al mismo objeto función. Tanto greet como say_hello ahora apuntan a la misma función, lo cual podemos verificar usando el operador is.

23.1.2) Almacenar funciones en estructuras de datos

Como las funciones son objetos, podemos almacenarlas en listas, diccionarios o cualquier otra colección:

python
# Calculadora con operaciones almacenadas en un diccionario
def add(x, y):
    return x + y
 
def subtract(x, y):
    return x - y
 
def multiply(x, y):
    return x * y
 
def divide(x, y):
    return x / y
 
# Almacena funciones en un diccionario
operations = {
    '+': add,
    '-': subtract,
    '*': multiply,
    '/': divide
}
 
# Usa el diccionario para realizar cálculos
num1 = 10
num2 = 5
operator = '*'
 
result = operations[operator](num1, num2)
print(f"{num1} {operator} {num2} = {result}")  # Output: 10 * 5 = 50

Este patrón es extremadamente útil para construir sistemas flexibles. En lugar de escribir largas cadenas de sentencias if-elif para elegir qué función llamar, podemos buscar la función adecuada en un diccionario y llamarla directamente.

23.2) Pasar funciones como argumentos

23.2.1) El concepto básico

Uno de los usos más potentes de las funciones de primera clase es pasarlas como argumentos a otras funciones. Esto nos permite escribir código flexible y reutilizable que puede funcionar con distintos comportamientos.

Aquí tienes un ejemplo simple:

python
# Función que aplica otra función a un valor
def apply_operation(value, operation):
    """Aplica la función operation recibida como parámetro al valor."""
    return operation(value)
 
# Diferentes operaciones
def double(x):
    return x * 2
 
def square(x):
    return x * x
 
def negate(x):
    return -x
 
# Usa la misma función apply_operation con diferentes operaciones
number = 5
print(apply_operation(number, double))   # Output: 10
print(apply_operation(number, square))   # Output: 25
print(apply_operation(number, negate))   # Output: -5

La función apply_operation no sabe ni le importa qué operación específica está realizando. Simplemente llama a cualquier función que se le pase. Esta separación de responsabilidades hace que el código sea más modular y más fácil de extender.

23.2.2) Procesar colecciones con funciones personalizadas

Un patrón común es procesar cada elemento de una colección usando una función pasada como argumento:

python
# Procesa cada elemento de una lista usando una función dada
def process_list(items, processor):
    """Aplica la función processor a cada elemento de la lista."""
    results = []
    for item in items:
        results.append(processor(item))
    return results
 
# Diferentes funciones de procesamiento
def uppercase(text):
    return text.upper()
 
def add_exclamation(text):
    return text + "!"
 
def get_length(text):
    return len(text)
 
# Procesa la misma lista de diferentes formas
words = ["hello", "world", "python"]
 
print(process_list(words, uppercase))        # Output: ['HELLO', 'WORLD', 'PYTHON']
print(process_list(words, add_exclamation))  # Output: ['hello!', 'world!', 'python!']
print(process_list(words, get_length))       # Output: [5, 5, 6]

Este patrón es tan útil que Python proporciona funciones integradas como map() y filter() que funcionan de esta manera (las exploraremos en la Sección 23.6).

23.2.3) Ordenar proporcionando una función key (breve introducción)

La función sorted() de Python acepta un parámetro key: una función que determina cómo comparar elementos:

python
# Ordena estudiantes por distintos criterios
students = [
    {"name": "Alice", "grade": 85, "age": 20},
    {"name": "Bob", "grade": 92, "age": 19},
    {"name": "Charlie", "grade": 78, "age": 21},
    {"name": "Diana", "grade": 95, "age": 20}
]
 
# Función para extraer la nota
def get_grade(student):
    return student["grade"]
 
# Función para extraer el nombre
def get_name(student):
    return student["name"]
 
# Ordena por nota (ascendente)
by_grade = sorted(students, key=get_grade)
print("Sorted by grade:")
for student in by_grade:
    print(f"  {student['name']}: {student['grade']}")
# Output:
#   Charlie: 78
#   Alice: 85
#   Bob: 92
#   Diana: 95
 
# Ordena por nombre (alfabéticamente)
by_name = sorted(students, key=get_name)
print("\nSorted by name:")
for student in by_name:
    print(f"  {student['name']}: {student['grade']}")
# Output:
#   Alice: 85
#   Bob: 92
#   Charlie: 78
#   Diana: 95

La función key se llama una vez por cada elemento, y su valor de retorno se usa para la comparación. Esto es mucho más flexible que tener que escribir una lógica de ordenación personalizada.

Este patrón de pasar funciones para personalizar el comportamiento es extremadamente común en Python. Exploraremos técnicas de ordenación más avanzadas en el Capítulo 38.

23.3) Devolver funciones desde funciones

23.3.1) Funciones que crean funciones

Del mismo modo que podemos pasar funciones como argumentos, también podemos devolver funciones desde otras funciones. Esto nos permite crear funciones especializadas de forma dinámica:

python
# Función que crea y devuelve una nueva función
def create_multiplier(factor):
    """Crea una función que multiplica por el factor dado."""
    def multiplier(x):
        return x * factor
    return multiplier
 
# Crea funciones multiplicadoras especializadas
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)
 
# Usa las funciones creadas
print(double(5))      # Output: 10
print(triple(5))      # Output: 15
print(times_ten(5))   # Output: 50

¿Qué está pasando aquí? La función create_multiplier define una función interna llamada multiplier y la devuelve. Cada vez que llamamos a create_multiplier con un factor diferente, obtenemos una nueva función que "recuerda" ese factor específico. Este es nuestro primer vistazo a los closures, que exploraremos en profundidad en la siguiente sección.

23.3.2) Crear validadores personalizados

Devolver funciones es particularmente útil para crear funciones de validación o de procesamiento personalizadas:

python
# Crea validadores de rango de forma dinámica
def create_range_validator(min_value, max_value):
    """Crea una función que valida si un número está en un rango."""
    def validator(number):
        return min_value <= number <= max_value
    return validator
 
# Crea validadores específicos
is_valid_age = create_range_validator(0, 120)
is_valid_percentage = create_range_validator(0, 100)
is_room_temperature = create_range_validator(15, 30)
 
# Usa los validadores
age = 25
print(f"Is {age} a valid age? {is_valid_age(age)}")  # Output: True
 
temp = 22
print(f"Is {temp}°C room temperature? {is_room_temperature(temp)}")  # Output: True
 
score = 150
print(f"Is {score} a valid percentage? {is_valid_percentage(score)}")  # Output: False

23.4) Comprender closures: funciones que recuerdan

23.4.1) ¿Qué es un closure?

Un closure es una función que "recuerda" variables del alcance donde fue creada, incluso después de que ese alcance haya terminado de ejecutarse. En los ejemplos de la Sección 23.3, ya hemos estado usando closures sin nombrarlos explícitamente.

Examinemos cómo funcionan los closures:

python
def create_counter(start=0):
    """Crea una función contador que recuerda su conteo."""
    count = start  # Esta variable queda "capturada" por el closure
    
    def counter():
        nonlocal count  # Accede a la variable capturada
        count += 1
        return count
    
    return counter
 
# Crea dos contadores independientes
counter1 = create_counter(0)
counter2 = create_counter(100)
 
# Cada contador mantiene su propio conteo
print(counter1())  # Output: 1
print(counter1())  # Output: 2
print(counter1())  # Output: 3
 
print(counter2())  # Output: 101
print(counter2())  # Output: 102
 
print(counter1())  # Output: 4 (counter1 is independent of counter2)

La función interna counter forma un closure sobre la variable count. Aunque create_counter ya haya terminado de ejecutarse, la función counter devuelta todavía tiene acceso a count. Cada llamada a create_counter crea un nuevo closure independiente con su propia variable count.

23.4.2) Cómo los closures capturan variables

Cuando se define una función dentro de otra función, puede acceder a variables del alcance de la función externa. Estas variables se "capturan" y permanecen accesibles incluso después de que la función externa devuelva:

Cuando Python crea la función interna, no solo guarda el código de la función: también guarda referencias a todas las variables de la función externa que usa la función interna. A este proceso se le llama "capturar" variables.

python
def create_greeter(greeting):
    """Crea una función de saludo con un saludo personalizado."""
    def greet(name):
        return f"{greeting}, {name}!"
    return greet
 
# Crea distintos saludadores
say_hello = create_greeter("Hello")
say_hi = create_greeter("Hi")
say_bonjour = create_greeter("Bonjour")
 
# Cada saludador recuerda su saludo específico
print(say_hello("Alice"))    # Output: Hello, Alice!
print(say_hi("Bob"))         # Output: Hi, Bob!
print(say_bonjour("Claire")) # Output: Bonjour, Claire!

El parámetro greeting queda capturado por el closure. Cada función saludadora tiene su propio valor de greeting capturado que usa siempre que se la llama.

23.4.3) Uso práctico: funciones de configuración

Los closures son excelentes para crear funciones con un comportamiento preconfigurado:

python
# Crea calculadoras de precio con diferentes tasas de impuestos
def create_price_calculator(tax_rate):
    """Crea una calculadora que aplica una tasa de impuestos específica."""
    def calculate_total(price):
        tax = price * tax_rate
        return price + tax
    return calculate_total
 
# Crea calculadoras para distintas regiones
us_calculator = create_price_calculator(0.07)    # 7% de impuesto
uk_calculator = create_price_calculator(0.20)    # 20% de IVA
japan_calculator = create_price_calculator(0.10) # 10% de impuesto al consumo
 
# Calcula precios en distintas regiones
item_price = 100
 
print(f"US total: ${us_calculator(item_price):.2f}")      # Output: US total: $107.00
print(f"UK total: £{uk_calculator(item_price):.2f}")      # Output: UK total: £120.00
print(f"Japan total: ¥{japan_calculator(item_price):.2f}") # Output: Japan total: ¥110.00

23.4.4) Cuándo usar closures

Los closures son especialmente útiles cuando necesitas:

  • Crear funciones con un comportamiento preconfigurado
  • Mantener estado entre llamadas a funciones sin usar clases
  • Implementar funciones callback que necesitan recordar contexto
  • Crear fábricas de funciones que producen funciones especializadas

23.5) Usar lambda para funciones anónimas cortas

23.5.1) ¿Qué son las expresiones lambda?

Una expresión lambda crea una función pequeña y anónima: una función sin nombre. Las expresiones lambda son útiles cuando necesitas una función simple por un período corto y no quieres definirla formalmente con def.

La sintaxis es:

python
lambda parameters: expression

La lambda toma parámetros (como una función normal) y devuelve el resultado de evaluar la expresión. Aquí tienes un ejemplo simple:

python
# Función normal
def add(x, y):
    return x + y
 
# Expresión lambda equivalente
add_lambda = lambda x, y: x + y
 
# Ambas funcionan de la misma manera
print(add(3, 5))        # Output: 8
print(add_lambda(3, 5)) # Output: 8

Las expresiones lambda están limitadas a una sola expresión: no pueden contener instrucciones como if, for o varias líneas de código. Esta limitación las mantiene simples y enfocadas.

23.5.2) Expresiones lambda como argumentos

Las expresiones lambda brillan cuando necesitas pasar una función simple como argumento y no quieres definir una función con nombre separada:

python
# Ordena estudiantes por nota usando lambda
students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 92},
    {"name": "Charlie", "grade": 78},
    {"name": "Diana", "grade": 95}
]
 
# En lugar de definir una función aparte:
# def get_grade(student):
#     return student["grade"]
# sorted_students = sorted(students, key=get_grade)
 
# Podemos usar directamente una lambda:
sorted_students = sorted(students, key=lambda student: student["grade"])
 
print("Students sorted by grade:")
for student in sorted_students:
    print(f"  {student['name']}: {student['grade']}")
# Output:
#   Charlie: 78
#   Alice: 85
#   Bob: 92
#   Diana: 95

Esto es más conciso cuando la función es simple y solo se usa una vez. La lambda lambda student: student["grade"] es equivalente a una función que toma un estudiante y devuelve su nota.

23.5.3) Lambda con múltiples parámetros

Las expresiones lambda pueden tomar múltiples parámetros, igual que las funciones normales:

python
# Operaciones de calculadora usando lambda
operations = {
    'add': lambda x, y: x + y,
    'subtract': lambda x, y: x - y,
    'multiply': lambda x, y: x * y,
    'divide': lambda x, y: x / y if y != 0 else "Error"
}
 
# Usa las expresiones lambda
print(operations['add'](10, 5))       # Output: 15
print(operations['multiply'](10, 5))  # Output: 50
print(operations['divide'](10, 0))    # Output: Error

Fíjate en cómo podemos usar una expresión condicional (x / y if y != 0 else "Error") dentro de una lambda, pero no podemos usar una sentencia if (lo cual requeriría múltiples líneas).

23.5.4) Cuándo usar lambda y cuándo funciones con nombre

Usa expresiones lambda cuando:

  • La función es muy simple (una expresión)
  • La función se usa solo una vez o en un contexto muy local
  • Definir una función con nombre añadiría verbosidad innecesaria

Usa una función con nombre cuando:

  • La función es compleja o requiere múltiples sentencias
  • La función se reutilizará en varios lugares
  • La función necesita un nombre descriptivo para mayor claridad
  • La función necesita un docstring

23.5.5) Limitaciones de lambda y alternativas

Las expresiones lambda tienen limitaciones importantes:

python
# ❌ Esto no funcionará - lambda no puede contener sentencias
# bad_lambda = lambda x: 
#     if x > 0:
#         return x
#     else:
#         return -x
 
# ✅ Usa una expresión condicional en su lugar
absolute_value = lambda x: x if x > 0 else -x
print(absolute_value(-5))  # Output: 5
print(absolute_value(3))   # Output: 3
 
# ✅ Para múltiples operaciones, usa una función normal
def process_and_double(x):
    print(f"Processing: {x}")
    return x * 2
 
result = process_and_double(5)  # Output: Processing: 5
print(result)                    # Output: 10

Las expresiones lambda son herramientas para situaciones específicas. Cuando hacen que el código sea más claro y más conciso, úsalas. Cuando hacen que el código sea más difícil de entender, usa una función normal con nombre en su lugar.

23.6) Usar map() y filter() con funciones simples

23.6.1) La función map()

La función map() aplica una function dada a cada elemento de un iterable (como una lista, tupla o cadena) y devuelve un iterador que contiene los resultados. Es una forma de transformar cada elemento de una colección sin escribir un bucle explícito.

python
map(function, iterable, *iterables)

Parámetros:

  • function (obligatorio): Una función que toma uno o más argumentos, los procesa y devuelve un valor. La función se llama una vez por cada elemento en los iterable(s).
  • iterable (obligatorio): Una secuencia (lista, tupla, cadena, etc.) cuyos elementos se pasarán a la function.
  • *iterables (opcional): Iterables adicionales para una function de múltiples argumentos.

Si se proporcionan múltiples iterables, la función debe aceptar esa cantidad de argumentos.
map() se detendrá cuando se agote el iterable más corto.

Devuelve:

Un objeto map (iterador) que contiene los resultados devueltos por la function para cada elemento de entrada.

Importante: El objeto map es un iterador, no una secuencia como una list.

python
# Duplica cada número en una lista
numbers = [1, 2, 3, 4, 5]
 
def double(x):
    return x * 2
 
# Aplica double a cada número
doubled = map(double, numbers)
result = list(doubled)  # Convierte el objeto map (iterador) a lista
print(result)  # Output: [2, 4, 6, 8, 10]

23.6.2) Usar map() con Lambda

Las expresiones lambda funcionan perfectamente con map() para transformaciones simples:

python
# Convierte temperaturas de Celsius a Fahrenheit
celsius_temps = [0, 10, 20, 30, 40]
 
fahrenheit_temps = list(map(lambda c: (c * 9/5) + 32, celsius_temps))
print(fahrenheit_temps)  # Output: [32.0, 50.0, 68.0, 86.0, 104.0]

23.6.3) La función filter()

La función filter() aplica una function dada a cada elemento de un iterable y devuelve un iterador que contiene solo los elementos para los cuales la función devuelve True. Es una forma de seleccionar elementos de una colección sin escribir un bucle explícito.

python
filter(function, iterable)

Parámetros:

  • function: Una función que toma un argumento, lo evalúa y devuelve True o False. La función se llama una vez por cada elemento en el iterable.
  • iterable: Una secuencia (lista, tupla, cadena, etc.) cuyos elementos serán evaluados por la function.

Devuelve:

Un objeto filter (iterador) que contiene solo los elementos para los cuales la function devolvió True.

Importante: El objeto filter es un iterador, no una secuencia como una lista.

Ejemplo:

python
# Conserva solo números pares
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 
def is_even(x):
    return x % 2 == 0
 
# Aplica is_even a cada número, conserva solo los que devuelven True
even_numbers = filter(is_even, numbers)
result = list(even_numbers)  # Convierte el objeto filter a lista
print(result)  # Output: [2, 4, 6, 8, 10]

23.6.4) Usar filter() con Lambda

Las expresiones lambda se usan comúnmente con filter() para un filtrado conciso:

python
# Filtra estudiantes que aprobaron (grade >= 60)
students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 55},
    {"name": "Charlie", "grade": 92},
    {"name": "Diana", "grade": 48},
    {"name": "Eve", "grade": 73}
]
 
passed = list(filter(lambda s: s["grade"] >= 60, students))
print("Students who passed:")
for student in passed:
    print(f"  {student['name']}: {student['grade']}")
# Output:
#   Alice: 85
#   Charlie: 92
#   Eve: 73

23.6.5) Combinar map() y filter()

Puedes encadenar operaciones de map() y filter() para realizar transformaciones complejas:

python
# Obtén los cuadrados de los números pares
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 
# Primero filtra números pares, luego elévalos al cuadrado
even_numbers = filter(lambda x: x % 2 == 0, numbers)
squared = map(lambda x: x ** 2, even_numbers)
result = list(squared)
print(result)  # Output: [4, 16, 36, 64, 100]

Comparación visual: map() vs filter()

filter() - Conserva ALGUNOS elementos

Entrada: [1, 2, 3, 4, 5]

Probar: is_even(x)

Salida: [2, 4](igual o más corta)

map() - Transforma TODOS los elementos

Entrada: [1, 2, 3, 4, 5]

Aplicar: double(x) = x * 2

Salida: [2, 4, 6, 8, 10](misma longitud)

Diferencias clave:

  • map(): Aplica una función para transformar cada elemento → la salida tiene la misma longitud
  • filter(): Prueba cada elemento y conserva solo los que pasan → la salida tiene una longitud igual o menor

En este capítulo, hemos explorado las potentes características de programación funcional de Python. Aprendimos que las funciones son objetos de primera clase que pueden pasarse como cualquier otro valor, lo que habilita patrones de código flexibles y reutilizables. Descubrimos cómo las funciones pueden devolver otras funciones, creando closures que recuerdan su entorno. Exploramos expresiones lambda para definiciones de funciones concisas, y usamos map() y filter() para procesar colecciones de forma elegante.

Estos conceptos forman la base para técnicas avanzadas de programación en Python. En el Capítulo 38, construiremos sobre este conocimiento para dominar los decoradores, una de las características más elegantes de Python.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai