EAS Latam Academy Logo EAS Latam

Módulo 4: Python Experto

El GIL (Global Interpreter Lock)

El Global Interpreter Lock (GIL) es una característica de CPython (la implementación más común de Python) que asegura que solo un hilo de Python puede ejecutar bytecode de Python a la vez.

Impacto del GIL

  • Programas CPU-bound: Para tareas que requieren mucha CPU (ej. cálculos intensivos), el GIL puede limitar el paralelismo real, ya que solo un hilo puede usar la CPU en un momento dado.
  • Programas I/O-bound: Para tareas que esperan E/S (ej. lectura de archivos, solicitudes de red), el GIL se libera durante las operaciones de E/S, permitiendo que otros hilos se ejecuten. En estos casos, el threading puede ofrecer ganancias de rendimiento.

Cómo Mitigar el GIL

  • multiprocessing: Utiliza procesos separados, cada uno con su propio intérprete de Python y su propio GIL. Esto permite un paralelismo real en CPU-bound tasks.
  • asyncio: Framework para programación asíncrona que gestiona la concurrencia en un solo hilo de forma eficiente, ideal para E/S.
  • Módulos en C/C++: Las extensiones escritas en C/C++ pueden liberar el GIL durante su ejecución, permitiendo que otras tareas de Python se ejecuten.

Concurrencia Avanzada: threading vs multiprocessing vs asyncio

threading (Múltiples Hilos)

  • Uso: Principalmente para tareas I/O-bound.
  • Ventajas: Menor sobrecarga que los procesos, comparte memoria fácilmente.
  • Desventajas: Sujeto al GIL para CPU-bound tasks.
import threading
import time

def tarea_hilo(nombre):
    print(f"Hilo {nombre}: iniciando...")
    time.sleep(2) # Simula una operación I/O
    print(f"Hilo {nombre}: terminando.")

hilo1 = threading.Thread(target=tarea_hilo, args=("Uno",))
hilo2 = threading.Thread(target=tarea_hilo, args=("Dos",))

hilo1.start()
hilo2.start()

multiprocessing (Múltiples Procesos)

  • Uso: Para tareas CPU-bound, ya que cada proceso tiene su propio intérprete y GIL.
  • Ventajas: Paralelismo real, utiliza múltiples núcleos de CPU.
  • Desventajas: Mayor sobrecarga al crear procesos, comunicación entre procesos más compleja.
import multiprocessing
import time

def tarea_proceso(nombre):
    print(f"Proceso {nombre}: iniciando...")
    time.sleep(2) # Simula trabajo intensivo
    print(f"Proceso {nombre}: terminando.")

if __name__ == "__main__": # Necesario en Windows
    proceso1 = multiprocessing.Process(target=tarea_proceso, args=("Uno",))
    proceso2 = multiprocessing.Process(target=tarea_proceso, args=("Dos",))

    proceso1.start()
    proceso2.start()

asyncio (Concurrencia Asíncrona)

  • Uso: Ideal para tareas I/O-bound cuando el número de operaciones concurrentes es muy alto.
  • Ventajas: Alta eficiencia con un solo hilo, bajo consumo de recursos.
  • Desventajas: Requiere que las funciones asíncronas sean await-ables, el código puede volverse complejo.

Descriptores y __slots__

Descriptores

Permiten un control muy fino sobre cómo se accede, asigna o elimina un atributo en una clase.

class ValidadorEntero:
    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = '_' + name

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError(f"'{self.public_name}' debe ser un entero")
        setattr(obj, self.private_name, value)

class Punto:
    x = ValidadorEntero()
    y = ValidadorEntero()

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Punto(10, 20)
# p.x = 3.14 # Lanzaría TypeError

__slots__

Una técnica de optimización de memoria. Al definir __slots__, evitas la creación del diccionario __dict__ por instancia, lo que puede ahorrar mucha memoria para clases con muchas instancias.

class MemoriaEficiente:
    __slots__ = ['nombre', 'edad']

    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad

# Consume menos memoria que una clase sin __slots__ para muchas instancias

Optimización y Herramientas

  • Profiling: cProfile y timeit para identificar cuellos de botella en el rendimiento.
  • Type Hinting: Usar anotaciones de tipo (typing module) para mejorar la legibilidad, la capacidad de mantenimiento y permitir herramientas de análisis estático de código.
  • Extensiones en C/C++: Para partes críticas del código CPU-bound.
  • JIT Compilers: PyPy como alternativa a CPython para algunas cargas de trabajo.

Conclusión

Este módulo ha cubierto temas expertos en Python, desde el entendimiento del GIL y la elección de la estrategia de concurrencia adecuada, hasta técnicas avanzadas como descriptores y __slots__ para un control más fino y optimización. Con este conocimiento, estás equipado para construir aplicaciones Python de alto rendimiento, robustas y escalables, entendiendo las implicaciones a nivel de sistema.

¿Listo para el desafío?

Has revisado la teoría. Ahora es momento de poner a prueba tus conocimientos.

Iniciar Desafío