Introducción al desarrollo con Python

python programación

Desarrollo con Python

Los lenguajes de programación se dividen principalmente por su nivel de abstracción en lenguajes de bajo y alto nivel. Entre los de bajo nivel se encuentran lenguajes como ensamblador y C (aunque este último también puede considerarse de medio nivel). Por otro lado, los lenguajes de alto nivel incluyen Python, Java y C#.

Introducción al desarrollo con Python

A su vez, también se clasifican según si son compilados o interpretados. Los lenguajes compilados se traducen a código máquina antes de ser ejecutados, con lo cual se obtiene un mejor rendimiento en tiempo de ejecución. En cambio, los lenguajes interpretados son ejecutados línea por línea por un intérprete, lo que puede resultar en un rendimiento inferior pero mayor flexibilidad durante el desarrollo.

Por último, los lenguajes de programación también pueden clasificarse según su paradigma, como la programación orientada a objetos, la programación funcional o la programación imperativa. La programación orientada a objetos se centra en la creación de “objetos” que combinan datos y comportamiento, mientras que la programación funcional enfatiza el uso de funciones puras y evita el estado mutable. La programación imperativa, por otro lado, se basa en dar instrucciones al ordenador sobre cómo realizar tareas.

Con este contexto, es posible entender que Python es un lenguaje de alto nivel, interpretado y multiparadigma, lo que lo hace versátil y adecuado para una amplia gama de aplicaciones. Por ello, y a pesar de ser un lenguaje “joven” en comparación con otros como C o Java, ha ganado una gran popularidad en la comunidad tanto académica como profesional, y se pueden encontrar numerosas librerías y frameworks que facilitan su uso en diferentes dominios.

Como nota, en 2025, Python ha mejorado de forma notable su rendimiento gracias a las optimizaciones de CPython en versiones recientes como 3.9, 3.11, 3.12 y 3.13, que incorporan vectorcall para acelerar llamadas, adaptive bytecode en un intérprete especializado, mejoras en diccionarios y gestión de memoria, logrando incrementos de hasta un 11% en velocidad y reducciones del 10-15% en consumo sin cambios en el código. En paralelo, implementaciones alternativas como PyPy aprovechan un compilador Just-in-Time (JIT) que traduce bytecode a código máquina en tiempo de ejecución, ofreciendo en muchos casos un rendimiento significativamente superior al de CPython. Esta combinación de avances hace que Python sea cada vez más competitivo frente a lenguajes compilados en aplicaciones de alta demanda computacional como lo son el aprendizaje automático y la ciencia de datos.

Programación Orientada a Objetos

La programación orientada a objetos (POO) es un paradigma que utiliza “objetos” para representar datos y comportamientos. Python soporta POO de manera nativa, permitiendo a los desarrolladores crear clases que encapsulan datos y métodos. Esto promueve la reutilización del código y la modularidad, facilitando el mantenimiento y la extensión de las aplicaciones.

Clases y Objetos

En Python, una clase se define utilizando la palabra clave class, seguida del nombre de la clase y dos puntos. Los objetos son instancias de clases y se crean llamando a la clase como si fuera una función.

class PowerPlantController:
    def __init__(self, name, location):
        self.name = name
        self.location = location

    def start(self):
        print(f"{self.name} at {self.location} is starting.")

power_plant = PowerPlantController("Main Power Plant", "Sector 7G")
power_plant.start()

Métodos y Atributos

Los métodos son funciones definidas dentro de una clase que describen los comportamientos de los objetos. Los atributos, por otro lado, son variables que almacenan el estado de un objeto. En Python, se puede acceder a los métodos y atributos de un objeto utilizando la notación de punto.

class PowerPlantController:
    def __init__(self, name, location):
        self.name = name
        self.location = location

    def start(self):
        print(f"{self.name} at {self.location} is starting.")

power_plant = PowerPlantController("Main Power Plant", "Sector 7G")
power_plant.start()

Herencia y Polimorfismo

La herencia permite crear nuevas clases basadas en clases existentes, facilitando la reutilización del código. El polimorfismo, por otro lado, permite que diferentes clases implementen métodos con el mismo nombre, lo que simplifica la interacción con objetos de diferentes tipos.

class ControllerPowerPlant(PowerPlantController):
    def __init__(self, name, location, capacity):
        super().__init__(name, location)
        self.capacity = capacity

    def start(self):
        super().start()
        print(f"Capacity: {self.capacity} MW")

controller = ControllerPowerPlant("Main Power Plant", "Sector 7G", 150)
controller.start()

Composición

La composición es otra forma de reutilización de código que implica construir clases complejas a partir de clases más simples. En lugar de heredar de una clase base, una clase puede contener instancias de otras clases como atributos.

class Sensor:
    def __init__(self, sensor_id):
        self.sensor_id = sensor_id

    def read(self):
        return 42  # Simulación de lectura de sensor

class ControllerPowerPlant:
    def __init__(self, name, location, capacity):
        self.name = name
        self.location = location
        self.capacity = capacity
        self.sensor = Sensor("sensor_1")

    def start(self):
        print(f"Starting {self.name} at {self.location} with capacity {self.capacity} MW")
        print(f"Sensor {self.sensor.sensor_id} reading: {self.sensor.read()}")

controller = ControllerPowerPlant("Main Power Plant", "Sector 7G", 150)
controller.start()

Encapsulamiento

El encapsulamiento es el principio de ocultar los detalles internos de una clase y exponer solo lo necesario a través de una interfaz pública. En Python, esto se puede lograr utilizando convenciones de nomenclatura, como prefijos de guion bajo para indicar que un atributo o método es privado.

class ControllerPowerPlant:
    def __init__(self, name, location, capacity):
        self.name = name
        self.location = location
        self.capacity = capacity
        self._sensor = Sensor("sensor_1")  # Private Attribute

    def start(self):
        print(f"Starting {self.name} at {self.location} with capacity {self.capacity} MW")
        print(f"Sensor {self._sensor.sensor_id} reading: {self._sensor.read()}")

controller = ControllerPowerPlant("Main Power Plant", "Sector 7G", 150)
controller.start()

Test-Driven Development

El desarrollo guiado por pruebas (TDD) es una práctica de desarrollo de software que enfatiza la creación de pruebas automatizadas antes de escribir el código funcional. Este enfoque ayuda a garantizar que el código cumpla con los requisitos y facilita la detección de errores en etapas tempranas.

Ciclo de Vida de TDD

En @domain_driven_design se describe el ciclo de vida de TDD se puede resumir en los siguientes pasos:

  1. Escribir una prueba: Antes de implementar una nueva funcionalidad, se escribe una prueba que defina el comportamiento esperado.
  2. Ejecutar la prueba: Se ejecuta la prueba y se verifica que falle, lo que confirma que la prueba es válida.
  3. Implementar la funcionalidad: Se escribe el código mínimo necesario para hacer que la prueba pase.
  4. Refactorizar: Se mejora el código, asegurándose de que todas las pruebas sigan pasando.

Beneficios de TDD

La adopción de TDD aporta múltiples beneficios al proceso de desarrollo. En primer lugar, permite la detección temprana de errores, ya que escribir pruebas antes del código funcional ayuda a identificar fallos desde las primeras etapas. Además, este enfoque promueve la mejora de la calidad del código, fomentando la creación de soluciones más limpias, modulares y fáciles de mantener. Por último, las pruebas automatizadas actúan como una documentación viva, describiendo de manera precisa el comportamiento esperado del sistema y facilitando la comprensión y evolución del proyecto a lo largo del tiempo.

Ejemplos de TDD

En @domain_driven_design se describen diferentes tipos de pruebas que se pueden implementar en este contexto:

  1. Pruebas unitarias: Escribir pruebas para funciones individuales antes de implementarlas.
  2. Pruebas de integración: Asegurarse de que los diferentes módulos del sistema funcionen juntos correctamente.
  3. Pruebas de aceptación: Validar que el sistema cumple con los requisitos del cliente.

En este apartado, se presentará un ejemplo práctico de TDD utilizando el marco de pruebas unittest. Comenzaremos definiendo una clase PowerPlantController, el cual tendrá un método para iniciar la planta cambiando el estado del atributo started. En el test unitario, verificaremos que al llamar a este método, el atributo started se establezca en True. Primero, verificamos que al instanciar la clase, el atributo started esté en False y luego de llamar al método start(), esté en True. Si en algún caso, falla la prueba, se deberá ajustar la implementación hasta que todas las pruebas pasen.

import unittest

class PowerPlantController:
    def __init__(self, name, location):
        self.name = name
        self.location = location
        self.started = False

    def start(self) -> None:
        self.started = True

class TestPowerPlantController(unittest.TestCase):
    def test_start_sets_started_true(self):
        controller = PowerPlantController("Main Power Plant", "Sector 7G")
        self.assertFalse(controller.started)

        controller.start()
        self.assertTrue(controller.started)

if __name__ == "__main__":
    unittest.main()

Framework FastAPI

Python es de los principales lenguajes en el desarrollo web, además de por su sencillez y baja curva de aprendizaje, por su popularidad tiene una gran variedad de liberias y frameworks. Uno de esos frameworks populares por su robustez es Django. Sin embargo, cuando se necesitaba construir aplicaciones de complejidad baja o intermedia, se acudía a Flask, un microframework que permite una mayor flexibilidad y simplicidad en el desarrollo. Pero todo esto cambió con la llegada de FastAPI, el cual mantiene un enfoque de simplificación de Flask pero con un rendimiento superior y características adicionales modernas. Reflejo de esta disrupción no es solo la adopción creciente entre la comunidad de desarrolladores, sino también por las empresas como Netflix, Uber, Microsoft y otras.

Asincronía

En los servidores web tradicionales, si un endpoint se tarda mucho en responder, el servidor se bloquea y no puede atender otras solicitudes hasta que se complete la respuesta. Esto puede llevar a problemas de rendimiento y escalabilidad, especialmente en aplicaciones que requieren un alto rendimiento y la capacidad de manejar múltiples conexiones simultáneamente. Además de cara al usuario, esto puede resultar en tiempos de espera prolongados y una experiencia de usuario deficiente.

Los servidores modernos, como FastAPI, abordan este problema al ser asíncronos por diseño. Esto significa que puede manejar múltiples solicitudes simultáneamente sin bloquear el hilo principal. Al utilizar la palabra clave async en las funciones de los endpoints, FastAPI puede liberar el bucle de eventos mientras espera que se completen las operaciones de entrada/salida, lo que permite que se puedan atender miles de solicitudes concurrentemente.

from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.get("/sync")
def read_sync():
    # Bloquea durante 3 segundos
    import time
    time.sleep(3)
    return {"mensaje": "Respuesta síncrona"}

@app.get("/async")
async def read_async():
    # Libera el loop mientras espera
    await asyncio.sleep(3)
    return {"mensaje": "Respuesta asíncrona"}

Sin embargo, para aplicaciones que requieren un nivel de computo sobre la CPU, la programación asíncrona puede no ser suficiente. En estos casos, es recomendable utilizar workers en segundo plano o técnicas de procesamiento paralelo.

API REST

Una API REST (Representational State Transfer) es un conjunto de reglas y convenciones para construir servicios web que permiten la comunicación entre diferentes sistemas a través de HTTP. Las APIs REST son ampliamente utilizadas en el desarrollo de aplicaciones web y móviles debido a su simplicidad, escalabilidad y flexibilidad.

Para ello se crean endpoints que permiten interactuar con el dominio de la aplicación. Cada recurso se identifica de forma única a través de una URL y se puede manipular utilizando los métodos HTTP estándar (GET, POST, PUT, DELETE).

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(item_id: int):
    # Obtener un ítem por su ID
    return {"item_id": item_id}

@app.post("/items/")
def create_item(item: Item):
    # Crear un nuevo ítem
    return {"item": item}

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    # Actualizar un ítem existente
    return {"item_id": item_id, "item": item}

@app.delete("/items/{item_id}")
def delete_item(item_id: int):
    # Eliminar un ítem por su ID
    return {"item_id": item_id}

Validación con Pydantic

Uno de las cualidades que hacen a Python tan popular es que es de tipado dinámico, lo cual significa que las variables pueden mutar de tipo en tiempo de ejecución, es decir, una variable tipo boolean en primera instancia puede aceptar valores float. Sin embargo, esta flexibilidad puede llevar a errores sutiles y difíciles de detectar. Para abordar este problema, se puede utilizar Pydantic, una biblioteca de validación de datos que aprovecha las anotaciones de tipo de Python.

Pydantic permite definir modelos de datos utilizando clases y anotaciones de tipo, lo que facilita la validación y la serialización de datos. Al utilizar Pydantic, se pueden definir claramente el tipo de los datos esperados sobre la entrada y salida, lo que ayuda a prevenir errores y a mejorar la calidad del código.

Ejemplo de uso de Pydantic

En este ejemplo, se define un modelo de datos para una planta de energía utilizando Pydantic:

from pydantic import BaseModel

class PowerPlant(BaseModel):
    name: str
    location: str
    capacity: float

Para este caso, se puede utilizar el modelo PowerPlant para validar los datos de entrada en una API REST construida con FastAPI. Al definir los parámetros de entrada de un endpoint utilizando este modelo, FastAPI se encargará automáticamente de validar los datos y devolver errores claros si la entrada no cumple con las expectativas definidas en el modelo. Esto ayuda a garantizar que la API reciba datos en el formato correcto y reduce la posibilidad de errores en tiempo de ejecución.