Paradigmas

Paradigmas de Programación · Unidad 1

Programación funcional

Qué es un paradigma, cuáles se usan hoy y cómo se aplica el paradigma funcional en Python: funciones como objetos, callbacks, lambdas y los operadores map, filter y reduce.

3 bloques · 14 lecciones ≈ 40 min de lectura Python 3.9+ Pulsa / para buscar

Punto de partida

Introducción y objetivos

La unidad repasa los paradigmas de programación que se usan hoy y las necesidades que cubre cada uno. Muchos ya los usamos sin haberles puesto nombre; saber diferenciarlos es clave para desarrollar y mantener software.

La asignatura es totalmente práctica y se centra en Python, un lenguaje muy versátil para explorar distintos paradigmas. Se trabaja con Visual Studio Code, entornos virtuales y ficheros .py e .ipynb. Aunque Python no es el lenguaje que mejor encaja con el paradigma funcional, permite implementar callbacks o wrappers.

Objetivos de la unidad (4)
  1. Comprender qué es un paradigma de programación, su importancia y cómo la elección de uno afecta a la claridad, la eficiencia y el mantenimiento del código.
  2. Conocer los principales paradigmas actuales: características, ámbitos de aplicación, ventajas, desventajas y lenguajes representativos.
  3. Iniciarse en Python y los entornos virtuales: instalar y configurar Python con buenas prácticas y crear y gestionar entornos para aislar dependencias.
  4. Entender en profundidad el paradigma funcional en Python: sus construcciones, su combinación con otros paradigmas y cuándo conviene usarlo para mejorar legibilidad y mantenibilidad.

Bloque 1

Introducción a los paradigmas de programación

Concepto de paradigma de programación

Según la RAE, un paradigma es:

  • Ejemplo o ejemplar.
  • Teoría o conjunto de teorías cuyo núcleo central se acepta sin cuestionar y que suministra la base y modelo para resolver problemas y avanzar en el conocimiento (p. ej., el paradigma newtoniano). Es la acepción más próxima al software.
  • Relación de elementos que comparten un mismo contexto fonológico, morfológico o sintáctico.
  • Esquema formal en el que se organizan las palabras que admiten modificaciones flexivas o derivativas.

Ninguna habla de software, pero todas comparten la idea de un conjunto de entidades con una base común que sirve como modelo de clasificación.

Definición

Un paradigma de programación es un modelo que define un estilo fundamental de programación y sirve de base para avanzar en el desarrollo de software.

El concepto surge en 1978 para clasificar los estilos de programación, ofreciendo conceptos y abstracciones (funciones, variables, objetos, flujos de datos…) que permiten abstraerse de la arquitectura básica del sistema.

  • Un lenguaje no tiene por qué cumplir un solo paradigma: la mayoría de lenguajes modernos son multiparadigma.
  • Todos los paradigmas buscan la modularidad para mejorar eficiencia y costes de mantenimiento. La modularización también limita el alcance del software en el equipo donde se ejecuta, lo que aporta seguridad.
  • Hoy se dividen en dos grandes bloques: imperativo y declarativo.
Clasificación de los paradigmas Los paradigmas se dividen en imperativo (estructurado, procedimental y orientado a objetos) y declarativo (funcional y reactivo). El procedimental deriva del estructurado y el funcional deriva del procedimental. Casi todos los lenguajes modernos son multiparadigma. Paradigmas Imperativo cómo: paso a paso Declarativo qué: el resultado Estructurado Procedimental Orientado a objetos Funcional Reactivo deriva el funcional deriva del procedimental Multiparadigma: la mayoría de lenguajes modernos combinan varios
Clasificación según el material: dos grandes bloques y sus subparadigmas.

Programación imperativa

El paradigma más antiguo. Viene de imperare (ordenar): consiste en dar órdenes concretas para que el equipo las procese y ejecute.

  • Se define paso a paso cómo obtener el resultado con instrucciones bien definidas.
  • Las sentencias cambian el estado del programa (modifican sus datos).
  • Se ejecutan de forma secuencial, lo que da un claro determinismo en la salida.
Analogía: la receta de una tarta

Los pasos están bien definidos y siguiéndolos uno a uno se llega al resultado. Cada paso modifica la tarta (el estado) y la receta se sigue en orden.

Suelen ser lenguajes cercanos al sistema: obligan a gestionar memoria, datos a nivel de bit o incluso registros de CPU, lo que supone muchas líneas de código para tareas que en otros paradigmas son mucho más cortas.

  • C/C++
  • Java
  • Pascal
  • Cobol
  • FORTRAN
  • C#
  • Python

Dentro del imperativo están la programación estructurada, la procedimental y la orientada a objetos.

Programación estructurada

Busca mejorar la claridad, la calidad y el tiempo de desarrollo usando únicamente subrutinas y funciones. Surge en 1966 con el teorema de la programación estructurada.

Antes era habitual la instrucción GoTo (similar a JUMP en ensamblador MIPS): se define una etiqueta o línea y el programa salta a ese punto. Parece útil para reutilizar código, pero es muy poco eficiente en mantenibilidad y legibilidad (el famoso spaghetti code).

El material original incluye un ejemplo en Fortran que calcula π usando GoTo; la imagen no está disponible en la exportación.

Programación procedimental

Deriva directamente de la estructurada y se basa en la llamada a procedimientos. Un procedimiento (coloquialmente, «función») es una subrutina con una serie de pasos que puede llamarse en cualquier momento: más reutilización, menos tiempo de desarrollo y programas más cortos y legibles.

Es tan relevante que los procesadores añaden soporte hardware: registro de pila e instrucciones de llamada que guardan el punto de retorno casi sin penalización.

Variables locales

Solo existen en el contexto de un procedimiento: se crean dentro y se destruyen al terminar.

Variables globales

No están ligadas a ningún procedimiento: pueden crearse, modificarse y eliminarse en cualquier punto.

Programación orientada a objetos

Organiza el software en torno a datos u objetos en lugar de funciones y lógica. Un objeto es un campo de datos con atributos y comportamientos propios. Encaja con programas grandes, complejos y en mantenimiento activo, y favorece el trabajo en equipo gracias a su alta modularización.

Encapsulación

La implementación queda dentro del objeto; los demás solo usan sus métodos públicos (seguridad).

Abstracción

Solo se muestran los mecanismos internos relevantes para manipular el objeto.

Herencia

Reutilización de código entre clases.

Polimorfismo

Un objeto puede adoptar varias «formas» según el contexto; el programa decide su uso en cada ejecución.

Programación declarativa

Al contrario que la imperativa, describe qué solución se quiere (condiciones, proposiciones, afirmaciones o transformaciones) sin indicar los pasos para obtenerla, lo que ayuda a evitar efectos secundarios. Normalmente se considera declarativo todo lo que no es imperativo.

En el imperativo se define todo el procedimiento a más bajo nivel, pudiendo afectar a los datos. En el declarativo se describe el resultado y es el lenguaje, framework o librería quien lo produce.

Ejemplo ilustrativo (el material lo muestra en JavaScript con imágenes no disponibles; aquí en Python): obtener los cuadrados de los números pares.

numeros = [1, 2, 3, 4, 5, 6]
cuadrados_pares = []
for n in numeros:              # cómo: recorrer...
    if n % 2 == 0:             # ...comprobar...
        cuadrados_pares.append(n * n)   # ...y modificar el estado
print(cuadrados_pares)         # [4, 16, 36]
numeros = [1, 2, 3, 4, 5, 6]
cuadrados_pares = [n * n for n in numeros if n % 2 == 0]   # qué quiero
print(cuadrados_pares)         # [4, 16, 36]

No son excluyentes: a menudo se escriben procedimientos imperativos para luego usarlos de forma declarativa.

Características del modelo

  • Alto nivel de abstracción: programas complejos expresados de forma compacta.
  • Optimización sin tocar el algoritmo: basta con sustituir los procedimientos a los que se accede (p. ej., un método de ordenación más rápido).
  • Desarrollo muy rápido: ideal para prototipado en metodologías ágiles.

Lenguajes únicamente declarativos:

  • HTML/CSS
  • SQL
  • Haskell
  • Prolog

Solo describen el resultado; una implementación ajena hace el trabajo. En una consulta SQL se indica qué obtener o insertar y el motor se encarga del proceso; en HTML se describen elementos y aspecto, no el pintado de píxeles.

SELECT nombre FROM alumnos WHERE nota >= 5;   -- qué datos quiero, no cómo buscarlos

Programación funcional

Deriva de la procedimental y trabaja directamente con funciones, tratándolas como cualquier otra entidad del lenguaje. Se desarrolla en el bloque 3.

Programación reactiva

Trabaja con flujos de datos asíncronos: los datos se propagan y los módulos «reaccionan» ejecutando eventos al recibirlos.

En un enfoque imperativo, la CPU comprueba continuamente si han llegado datos (polling):

while(!exit){
    if(checkDataReceive()){
        receiveData();
    }
}

La reactiva se basa en el patrón Observer: los nuevos datos se notifican directamente a los interesados en lugar de que estos pregunten, invirtiendo el diseño tradicional de entrada/salida. La CPU solo trabaja cuando un módulo recibe una notificación.

Polling frente a patrón Observer A la izquierda, un cliente pregunta repetidamente a la fuente si hay datos. A la derecha, el sujeto notifica a tres observadores cuando llegan datos. POLLING (imperativo) Cliente Fuente ¿hay datos? ¿y ahora? ¿y ahora?… OBSERVER (reactivo) Sujeto Observer A Observer B Observer C notifica()
Diagrama propio: el material hace referencia a una imagen del patrón Observer no disponible.

Los sistemas reactivos son:

Responsivos

Cumplen tiempos de respuesta establecidos; los problemas de rendimiento se detectan fácilmente.

Resilientes

Siguen respondiendo incluso ante errores.

Elásticos

Siguen respondiendo ante aumentos de carga.

Orientados a mensajes

Bajo acoplamiento mediante intercambio asíncrono de mensajes.

Programación multiparadigma

La mayoría de lenguajes modernos implementan más de un paradigma, lo que permite elegir o combinar según el proyecto. La elección depende menos del lenguaje y más de las decisiones de diseño de la arquitectura.

Comparativa de paradigmas

Imperativo

Secuencia de instrucciones que modifican el estado.

Ventajas
Control detallado, ejecución determinista, fácil de seguir en problemas simples.
Desventajas
Más errores por efectos secundarios; poco expresivo en tareas de alto nivel.
Casos de uso
Programas de sistemas, videojuegos, software muy ligado al hardware.

Estructurado

Subparadigma del imperativo: secuencias, condicionales y bucles en lugar de saltos incontrolados.

Ventajas
Código más claro y mantenible; evita el spaghetti code.
Desventajas
Menos flexible en tareas muy específicas de bajo nivel.
Casos de uso
Software de propósito general, algoritmos educativos.

Procedimental

Subparadigma del estructurado: divide el programa en procedimientos reutilizables.

Ventajas
Reutilización, claridad y modularidad.
Desventajas
Dependencia de variables globales si no se estructura bien.
Casos de uso
Aplicaciones científicas, scripts, herramientas de línea de comandos.

Orientado a objetos

Software organizado en objetos con datos y comportamientos.

Ventajas
Modularidad, herencia, encapsulación, polimorfismo.
Desventajas
Curva de aprendizaje; riesgo de aumentar la complejidad.
Casos de uso
Aplicaciones empresariales, videojuegos, sistemas complejos y colaborativos.

Declarativo

Describe el qué sin indicar el cómo.

Ventajas
Más abstracción, menos código, fácil cambiar la implementación.
Desventajas
Menos control del flujo; depende de optimizaciones internas.
Casos de uso
Consultas a bases de datos, interfaces web, reglas de negocio.

Funcional

Funciones puras, inmutabilidad y composición de funciones.

Ventajas
Código predecible, fácil de testear, ideal para paralelismo.
Desventajas
Curva de aprendizaje inicial; a veces menos eficiente en memoria.
Casos de uso
Procesamiento de datos, algoritmos matemáticos, concurrencia.

Reactivo

Procesamiento asíncrono de flujos de datos y eventos; patrón Observer.

Ventajas
Escalabilidad, tiempo real, bajo acoplamiento, buen manejo de E/S y concurrencia.
Desventajas
Complejidad conceptual (asincronía, errores), depuración difícil.
Casos de uso
Interfaces reactivas, streaming, sistemas event-driven, IoT, notificaciones.

Multiparadigma

Combina características de varios paradigmas.

Ventajas
Flexibilidad y adaptabilidad; aprovecha lo mejor de cada enfoque.
Desventajas
Mezcla incoherente de estilos si no hay disciplina.
Casos de uso
Aplicaciones complejas con enfoques distintos en cada capa.
Ejercicio

Paradigmas por lenguaje

Indica qué paradigmas puede implementar cada lenguaje: C, Java, C++, Python, JavaScript, SQL, CSS y HTML.

Ver solución del material
Sí Parcial / limitado
LenguajeImper.Estruct.Proced.POOFuncionalDeclar.ReactivoMulti
C✓ sí✓ sí✓ sí
Java✓ sí✓ sí~ limitado✓ sí
C++✓ sí✓ sí~ limitado✓ sí
Python✓ sí✓ sí✓ sí✓ sí✓ sí~ parcial✓ sí✓ sí
JavaScript✓ sí✓ sí, con prototipos✓ sí~ parcial~ con librerías✓ sí
SQL✓ sí
CSS✓ sí
HTML✓ sí

En JavaScript la orientación a objetos se basa en prototipos y la reactividad se consigue con librerías.

Aplicaciones prácticas y tendencias futuras

Procesamiento de datos

El funcional transforma y filtra grandes volúmenes de forma concisa y paralelizable.

Desarrollo web

El declarativo es clave en HTML, CSS y en frameworks reactivos que describen el estado de la UI.

Automatización

El imperativo es práctico cuando hace falta controlar cada paso.

Aplicaciones híbridas

En Python: POO para la estructura, imperativo para el flujo y funcional para las colecciones.

Ejemplo: sistema de comercio electrónico

ParadigmaUso en la tienda
DeclarativoDefinir la interfaz y las consultas a la base de datos.
ImperativoGestionar la lógica de negocio paso a paso.
FuncionalFiltrar y transformar datos de productos y pedidos.

Tendencias futuras

Crecen los lenguajes y entornos multiparadigma, porque la complejidad actual exige soluciones flexibles:

  • Más programación funcional en lenguajes imperativos (Java, Python, C#), impulsada por el paralelismo y la reactividad.
  • Orientación a datos y flujos: arquitecturas reactivas y sistemas distribuidos favorecen lo declarativo y lo funcional.
  • Herramientas inteligentes y generación de código: paradigmas que se adaptan al problema, combinando instrucciones imperativas con descripciones declarativas optimizadas por IA.
  • Mantenibilidad y escalabilidad: la modularidad y la reducción de efectos secundarios serán cada vez más valoradas.

Bloque 2

Preparación del entorno de trabajo

Python como lenguaje interpretado

Python es multiparadigma e interpretado: no se compila a código máquina, sino que un programa instalado en el equipo (el intérprete) lee las instrucciones y las traduce en tiempo real.

Hay muchas versiones de Python y de sus librerías y cada proyecto arrastra muchas dependencias, así que hace falta compartimentar los paquetes: los entornos virtuales.

Instalación del entorno de trabajo

  1. Instala el intérprete de Python desde su web o repositorio oficial. Vale cualquier versión ≥ 3.9. Durante la instalación, incluye las librerías tcl/tk.
  2. Instala Visual Studio Code con las extensiones Python y Jupyter.
  3. En Windows 10/11, permite la ejecución de scripts de PowerShell (vienen bloqueados si no están firmados). Abre PowerShell como administrador:
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser

Recomendación (no incluida en el material): RemoteSigned también permite ejecutar Activate.ps1 y es más restrictiva, porque sigue bloqueando los scripts descargados de internet sin firmar.

Se asume Windows, pero puede usarse cualquier sistema operativo.

Entornos virtuales

Tener muchos paquetes en el mismo intérprete genera conflictos de versiones.

El problema

Un programa necesita requests 1.2.3 y otro requests ≥ 2.0.0. Solo puede haber una versión instalada, así que uno de los dos no funcionará. Y un proyecto real tiene cientos de dependencias.

Un entorno virtual es un entorno aislado por proyecto: sus dependencias no interfieren con las de otros. La herramienta habitual es venv; la mayoría de gestores (Anaconda, Miniforge) dependen de ella.

Creación

python -m venv nombre_del_entorno

Crea un directorio con ese nombre que contiene una copia aislada de la instalación de Python.

Activación

nombre_del_entorno\Scripts\activate
nombre_del_entorno\Scripts\Activate.ps1
source nombre_del_entorno/bin/activate

Ejemplo completo

Crear y activar un entorno llamado venv e instalar requests:

python -m venv venv
source venv/Scripts/Activate.ps1
pip install requests
Ojo con la línea de activación del material

source es de bash y Activate.ps1 es de PowerShell, así que esa combinación no funciona tal cual. Usa la de tu terminal: venv\Scripts\Activate.ps1 en PowerShell, venv\Scripts\activate en CMD o source venv/Scripts/activate en Git Bash.

El paquete se instala solo en el entorno virtual, no en la instalación global. Al ejecutar un script en VS Code, este te preguntará qué entorno usar.

Bloque 3

Programación funcional. Uso de callbacks

El paradigma funcional resuelve grandes problemas del enfoque imperativo: en lugar de describir cómo resolver una tarea paso a paso, se centra en qué resultado se quiere, apoyándose en funciones puras e inmutabilidad. Python no es funcional en su núcleo, pero permite aplicar muchos de sus principios.

Paradigma de programación funcional

Se basa en la evaluación de funciones puras y la inmutabilidad de los datos, inspirándose en la matemática funcional.

Función pura

Mismos argumentos → mismo resultado, que depende solo de las entradas. Sin efectos secundarios: no modifica variables globales, no escribe en disco…

Inmutabilidad

Los datos no se modifican: si algo cambia, se crea una estructura nueva. Simplifica la concurrencia y la depuración.

Ejemplo ilustrativo (no incluido en el material):

contador = 0

def impura(x):
    global contador
    contador += 1          # efecto secundario: modifica estado externo
    return x + contador    # el resultado depende de algo más que x

def pura(x):
    return x + 1           # mismo x → mismo resultado, siempre

Como todo consiste en evaluar funciones, hay mucha recursividad y llamadas anidadas, sin modificar datos como en el imperativo.

Ventajas

  • Alto nivel: se describe el resultado, no los pasos. Sentencias simples con mucha carga computacional.
  • Transparencia: una función pura solo depende de sus entradas; sin efectos secundarios, depurar y mantener es más fácil.
  • Paralelización: sin efectos secundarios ni dependencias externas, las rutinas se ejecutan concurrentemente con facilidad.

Casi todos los lenguajes admiten algo de programación funcional; en otros prácticamente todo el código es funcional (Haskell, Clojure…).

Programación funcional en Python

Para soportar el paradigma, las funciones deben poder:

  1. Recibir una función como argumento.
  2. Devolver una función a quien la llamó.

En Python todo es un objeto, incluidas las funciones (un objeto con un atributo que indica que puede invocarse). Por eso se tratan como cualquier otra variable:

def func():
	print("I am function func()!")

func()
another_name = func      # alias: sin paréntesis, no se ejecuta
another_name()
print(another_name)
I am function func()!
I am function func()!
<function func at 0x0000019425632CA0>

El alias sigue referenciando a la función original func.

print("cat", func, 42)     # se imprime como cualquier valor

objects = ["cat", func, 42]
myfunc = objects[1]        # se accede como a cualquier elemento
myfunc()
objects[1]()               # o se invoca directamente
d = {"cat": 1, func: 2, 42: 3}   # la función como clave
print(d[func])                   # 2

Callbacks

Definición

Un callback es una función que se pasa como argumento a otra para que esta la invoque en un momento dado (ante un evento, al terminar una operación o para delegar parte de su lógica). Conserva el contexto de su definición (clausura) y su ejecución queda bajo el control de la función receptora. También se habla de composición de funciones.

def inner():
	print("I am function inner()!")

def outer(function):
	function()

outer(inner)

Al pasar inner no se ejecuta (va sin ()). Solo se ejecuta dentro de outer, en function().

Ejemplo: sorted y el argumento key

sorted devuelve una nueva lista ordenada:

animals = ["ferret", "vole", "dog", "gecko"]
print(sorted(animals))
['dog', 'ferret', 'gecko', 'vole']

Parece alfabético, pero compara los valores ASCII de los caracteres. Si una palabra empieza en mayúscula, su valor es menor que el de cualquier minúscula y el orden deja de ser el esperado.

El argumento key recibe una función (un callback) que se aplica a cada elemento; su resultado es el criterio de orden:

print(sorted(animals, key=len))
print(sorted(animals, key=len, reverse=True))   # descendente
['dog', 'vole', 'gecko', 'ferret']
['ferret', 'gecko', 'vole', 'dog']
Ejercicio

Orden descendente por longitud sin reverse

Implementa una función para pasar como key a sorted que ordene la lista de mayor a menor longitud sin usar reverse.

animals = ["ferret", "vole", "dog", "gecko"]
sorted_animals = sorted(animals, key=...)  # completar
print(sorted_animals)
Ver solución del material
def reverse_len(s):
	return -len(s)

animals = ["ferret", "vole", "dog", "gecko"]
print(sorted(animals, key=reverse_len))
['ferret', 'gecko', 'vole', 'dog']

Al negar la longitud, las palabras más largas tienen la clave más pequeña y quedan primero.

Devolver una función

def outer():
	def inner():
		print("I am function inner()!")
	# Function outer() returns function inner()
	return inner

function = outer()
function        # referencia a inner, no la ejecuta
function()
outer()()
  • Se pueden definir funciones dentro de otras: inner solo existe en el contexto de outer.
  • Al devolver inner y guardarla en function, sigue siendo accesible.
  • outer()(): el primer () ejecuta outer, que devuelve inner; el segundo ejecuta inner.

Pasar una función vs. pasar su resultado

No es lo mismo. En C, printText(helloText()) no es programación funcional: primero se ejecuta helloText y lo que se pasa es su resultado.

void printText(char* text){
	printf("%s\n", text);
}
char* helloText(){
	return "Hello World!";
}
int main(){
	printText(helloText());
	return 0;
}
int main(){
	char* mText = helloText();   // se ejecuta primero...
	printText(mText);            // ...y se pasa el resultado
	return 0;
}
Ejercicio

Edad de mi mascota

Implementa edad_de_mi_mascota, que devuelva la edad de una mascota en años humanos. Recibe un entero con la edad y una función conversora; crea una conversora por animal:

  • Perro: 1 año = 7 años humanos
  • Gato: 1 año = 5
  • Pez: 1 año = 12
Ver solución propuesta · no incluida en el material
def perro(edad):
    return edad * 7

def gato(edad):
    return edad * 5

def pez(edad):
    return edad * 12

def edad_de_mi_mascota(edad, conversor):
    return conversor(edad)          # conversor es el callback

print(edad_de_mi_mascota(3, perro))
print(edad_de_mi_mascota(3, gato))
print(edad_de_mi_mascota(3, pez))
21
15
36

Ejercicio guiado: callbacks con requests y la API de OMDB

Objetivo: buscar películas con la API de OMDB (omdbapi.com), una API gratuita con datos de IMDB accesible por HTTP.

  1. En la web, entra en la sección API Key.
  2. Regístrate (puedes usar el correo de U-tad) indicando uso educativo.
  3. Activa la clave desde el email recibido.

Llamada de prueba (el parámetro s indica búsqueda). Devuelve un JSON con todos los resultados:

http://www.omdbapi.com/?apikey=<API_KEY>&s=interstellar

Uso de requests, con el entorno virtual activado:

pip install requests
from requests import get

response = get("http://www.omdbapi.com/?apikey=API_KEY&s=interstellar")
print(response.text)

if response.status_code == 200:
    ...
else:
    ...

El material escribe response.status_code is 200. Para comparar números usa ==: is compara la identidad de los objetos y puede dar resultados inesperados (Python 3.8+ incluso muestra un SyntaxWarning).

Para leer los datos como diccionario:

import json
data_dict = json.loads(response.content)
print(data_dict["Search"][0])

# o directamente con el método de la respuesta
print(response.json()["Search"][0])
Ejercicio

Buscador de películas con callbacks

Implementa get_movie_data: pide un título por teclado y consulta OMDB. Si todo va bien, muestra los resultados (Título – Año: URL_del_póster); si hay error, muestra el mensaje.

Condición: get_movie_data no trata los datos ni muestra resultados o errores. Tras la petición, delega en un callback.

Así la función queda desacoplada y sirve para cualquier GET, por lo que conviene renombrarla:

def make_get_request(url, on_success, on_error):
    ...

Ampliación: tras obtener la información, descarga el póster. get solo puede llamarse desde make_get_request, que se invoca dos veces: una con success_get_movie_func (obtiene la URL del póster) y otra con success_get_poster_func (crea una Image a partir de los bytes y la guarda). Para ello instala Pillow:

pip install Pillow
from PIL import Image
from io import BytesIO

img = Image.open(BytesIO(image_bytes))
Flujo de make_get_request con callbacks make_get_request hace get(url). Si el código es 200 llama a on_success con los datos; si no, a on_error con el código. make_get_request get(url) ¿status 200? sí no on_success(data) on_error(code)
La función genérica solo decide a quién llamar; qué hacer lo deciden los callbacks.
Ver solución propuesta · no incluida en el material
import json
from io import BytesIO
from urllib.parse import quote

from PIL import Image
from requests import get

API_KEY = "TU_API_KEY"


def make_get_request(url, on_success, on_error):
    """Hace la petición GET y delega el resultado en los callbacks."""
    response = get(url)
    if response.status_code == 200:
        on_success(response.content)
    else:
        on_error(response.status_code)


def show_error(status_code):
    print(f"Error en la petición (HTTP {status_code})")


def success_get_poster_func(data):
    img = Image.open(BytesIO(data))
    img.save("poster.png")
    print("Póster guardado en poster.png")


def success_get_movie_func(data):
    movies = json.loads(data)
    # OMDB responde 200 aunque no encuentre nada: lo indica en "Response"
    if movies.get("Response") != "True":
        print("Error:", movies.get("Error"))
        return
    for movie in movies["Search"]:
        print(f'{movie["Title"]} – {movie["Year"]}: {movie["Poster"]}')
    poster_url = movies["Search"][0]["Poster"]
    if poster_url != "N/A":
        make_get_request(poster_url, success_get_poster_func, show_error)


if __name__ == "__main__":
    title = input("Título: ")
    url = f"http://www.omdbapi.com/?apikey={API_KEY}&s={quote(title)}"
    make_get_request(url, success_get_movie_func, show_error)

Detalles importantes:

  • get solo aparece en make_get_request, que se reutiliza para el JSON y para la imagen.
  • OMDB devuelve 200 aunque no encuentre la película ("Response": "False"), así que ese caso se trata en el callback de éxito.
  • quote codifica el título para la URL (espacios, tildes…).

Funciones anónimas (lambda)

El estilo funcional usa muchas funciones cortas que a veces se llaman una sola vez. Para eso existen las funciones lambda: anónimas y definidas sobre la marcha, sin def.

lambda <parameter_list>: <expression>
lambda s: s[::-1]                    # invierte una cadena

callable(lambda s: s[::-1])          # True: también es un objeto invocable

reverse = lambda s: s[::-1]          # con alias...
reverse("I am a string")

def reverse(s):                      # ...equivale a
	return s[::-1]

(lambda s: s[::-1])("I am a string") # o se usa directamente
'gnirts a ma I'

Pueden recibir varios parámetros:

(lambda x1, x2, x3: (x1 + x2 + x3) / 3)(9, 6, 6)        # 7.0
(lambda x1, x2, x3: (x1 + x2 + x3) / 3)(1.4, 1.1, 0.5)  # 1.0
Limitación

Una lambda solo admite una única expresión, así que no sirve para lógica compleja como la del ejercicio de OMDB.

Operador map()

Función integrada que aplica una función a cada elemento de un iterable y devuelve un iterador con los resultados. Sustituye a un bucle for o while explícito.

map(<f>, <iterable>)
Entrada"cat""dog""hedgehog""gecko"
map(reverse, …) · se aplica a cada elemento
Salida"tac""god""gohegdeh""okceg"
def reverse(s):
	return s[::-1]

animals = ["cat", "dog", "hedgehog", "gecko"]
iterator = map(reverse, animals)
print(iterator)
<map object at 0x...>

map no devuelve una lista, sino un iterador. Para obtener los valores:

for i in map(reverse, animals):
    print(i)
print(list(map(reverse, animals)))
# ['tac', 'god', 'gohegdeh', 'okceg']
list(map(lambda s: s[::-1], ["cat", "dog", "hedgehog", "gecko"]))

Un iterador solo se puede recorrer una vez: si lo conviertes en lista, después estará vacío.

Ejemplo práctico con str.join

str.join concatena una lista de cadenas intercalando el separador, pero falla con números:

print("+".join(["cat", "dog", "hedgehog", "gecko"]))   # cat+dog+hedgehog+gecko
print("+".join([1, 2, 3, 4, 5]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sequence item 0: expected str instance, int found

Solución: convertir cada elemento a texto con map:

print("+".join(map(str, [1, 2, 3, 4, 5])))   # 1+2+3+4+5

map con varios iterables

map(<f>, <iterable₁>, <iterable₂>, ..., <iterableₙ>)

La función se aplica «en columnas», tomando un elemento de cada iterable. Debe tener tantos parámetros como iterables.

PosiciónABCf(a, b, c)
0110100111
1220200222
2330300333
def f(a, b, c):
	return a + b + c

print(list(map(f, [1, 2, 3], [10, 20, 30], [100, 200, 300])))   # [111, 222, 333]
Ejercicios

Practica con map

  1. Triplica todos los elementos de una lista de enteros.
  2. Imprime por pantalla todos los elementos de una lista de strings.
  3. Convierte los caracteres de una lista a mayúsculas y a minúsculas, y elimina las letras duplicadas de una secuencia.
Ver soluciones propuestas · no incluidas en el material
# 1. Triplicar
print(list(map(lambda x: x * 3, [1, 2, 3, 4])))      # [3, 6, 9, 12]

# 2. Imprimir: map es perezoso, hay que consumirlo para que se ejecute print
list(map(print, ["cat", "dog"]))

# 3. Mayúsculas / minúsculas y sin duplicados
letras = ["a", "B", "c"]
print(list(map(str.upper, letras)))                  # ['A', 'B', 'C']
print(list(map(str.lower, letras)))                  # ['a', 'b', 'c']
# dict.fromkeys conserva el orden y descarta repetidos
print("".join(dict.fromkeys(map(str.lower, "Mississippi"))))   # misp

Operador filter()

Como map, aplica una función a cada elemento, pero la usa para seleccionar: la función devuelve un booleano y filter conserva los elementos cuyo resultado es True.

filter(<f>, <iterable>)
Entrada111122223333
filter(lambda x: x > 100, …)
Salida1 (descartado)1112 (descartado)2223 (descartado)333
def greater_than_100(x):
	return x > 100

list(filter(greater_than_100, [1, 111, 2, 222, 3, 333]))   # [111, 222, 333]
list(filter(lambda x: x > 100, [1, 111, 2, 222, 3, 333]))  # igual, con lambda

A diferencia de map, filter solo acepta un iterable.

Ejercicios

Practica con filter

  1. Devuelve los números pares de una lista (defínela con range).
  2. Imprime las palabras escritas en MAYÚSCULAS (.isupper) de ["cat", "Cat", "CAT", "dog", "Dog", "DOG", "emu", "Emu", "EMU"].
Ver soluciones propuestas · no incluidas en el material
# 1. Pares
print(list(filter(lambda n: n % 2 == 0, range(10))))   # [0, 2, 4, 6, 8]

# 2. Palabras en mayúsculas: str.isupper ya es una función que devuelve bool
palabras = ["cat", "Cat", "CAT", "dog", "Dog", "DOG", "emu", "Emu", "EMU"]
print(list(filter(str.isupper, palabras)))             # ['CAT', 'DOG', 'EMU']

Operador reduce()

Aplica una función a los elementos por pares, combinándolos hasta obtener un único resultado. No está en el núcleo de Python: se importa de functools (instalada por defecto).

from functools import reduce
reduce(<f>, <iterable>)
Entrada12345
Acumulación de izquierda a derecha 1 + 2 = 3 3 + 3 = 6 6 + 4 = 10 10 + 5 = 15
Salida15
from functools import reduce

def f(x, y):
	return x + y

reduce(f, [1, 2, 3, 4, 5])   # 15
Cómo agrupa realmente

El material lo describe como [(1+2)+(3+4)+5], pero reduce acumula de izquierda a derecha: ((((1+2)+3)+4)+5). Con la suma da igual, pero con operaciones no asociativas (resta, división) el orden importa.

reduce no impone tipos, así que no se limita a la aritmética:

reduce(f, ["cat", "dog", "hedgehog", "gecko"])
# 'catdoghedgehoggecko'

lista = ["cat", "dog", "hedgehog", "gecko"]
spaced_list = map(lambda s: s + " ", lista)   # combinando map y reduce
reduce(f, spaced_list)
# 'cat dog hedgehog gecko '
Ejercicio

Máximo con reduce

Implementa una función que, mediante reduce, calcule el valor máximo de una lista.

Ver solución propuesta · no incluida en el material
from functools import reduce

def maximo(lista):
    # en cada paso se queda con el mayor de los dos
    return reduce(lambda a, b: a if a > b else b, lista)

print(maximo([3, 17, 4, 9]))   # 17

El material incluye vídeos (entornos virtuales, lambdas, resumen de operadores y corrección de OMDB) que no están disponibles en la exportación.

Cierre

Conclusiones de la unidad

Paradigmas

Imperativo, declarativo, reactivo, orientado a objetos, funcional y multiparadigma: conocerlos permite elegir con criterio o combinarlos.

Imperativo vs. declarativo

Instrucciones secuenciales claras frente a describir el resultado, con más abstracción y posibilidad de optimizar sin tocar la lógica.

Python y entornos virtuales

Lenguaje multiparadigma; los venv dan entornos aislados, consistentes y reproducibles.

Funcional en Python

Funciones puras, inmutabilidad y composición: callbacks, lambda, map, filter y reduce.

No existe un único enfoque correcto: el paradigma depende del problema, las restricciones del proyecto y las características del lenguaje.

Autoevaluación

Repaso rápido

Intenta responder antes de desplegar cada pregunta.

¿Qué es un paradigma de programación y en qué dos grandes bloques se dividen?

Un modelo que define un estilo fundamental de programación. Se dividen en imperativo y declarativo.

¿Qué caracteriza a la programación imperativa?

Instrucciones paso a paso, ejecutadas secuencialmente, que cambian el estado del programa, con salida determinista. Incluye estructurada, procedimental y orientada a objetos.

¿Qué problema resolvió la programación estructurada y cuándo surgió?

Surgió en 1966 con el teorema de la programación estructurada y eliminó el uso de GoTo, que perjudicaba la legibilidad y la mantenibilidad (spaghetti code).

¿Cuáles son los cuatro principios de la POO?

Encapsulación, abstracción, herencia y polimorfismo.

¿Por qué la programación reactiva usa menos CPU que el polling?

Porque usa el patrón Observer: los datos se notifican a los interesados y la CPU solo trabaja cuando hay una notificación, en lugar de preguntar continuamente.

¿Cuáles son las cuatro propiedades de un sistema reactivo?

Responsivo, resiliente, elástico y orientado a mensajes.

¿Para qué sirve un entorno virtual y cómo se crea?

Para aislar las dependencias de cada proyecto y evitar conflictos de versiones. Se crea con python -m venv nombre.

¿Qué es una función pura?

La que siempre devuelve el mismo resultado para los mismos argumentos y no tiene efectos secundarios.

¿Qué dos condiciones deben cumplir las funciones para soportar el paradigma funcional?

Poder recibir una función como argumento y poder devolver una función.

¿Qué diferencia hay entre outer(inner) y outer(inner())?

En el primero se pasa la función (callback) y la ejecuta outer. En el segundo se ejecuta inner primero y se pasa su resultado.

¿Qué hace outer()()?

El primer () ejecuta outer, que devuelve una función; el segundo ejecuta esa función devuelta.

¿Cuál es la principal limitación de una lambda?

Solo puede contener una única expresión.

¿Qué devuelve map y qué devuelve filter? ¿Cuántos iterables aceptan?

Ambos devuelven un iterador. map transforma cada elemento y acepta varios iterables; filter conserva los elementos cuyo resultado es True y solo acepta uno.

¿De dónde se importa reduce y qué devuelve?

De functools. Combina los elementos por pares, de izquierda a derecha, hasta devolver un único valor.

¿Por qué no se debe usar is para comparar status_code con 200?

Porque is compara la identidad de los objetos, no su valor. Para números hay que usar ==.