Python Difícil · Teoría

Comprende antes de memorizar

24 lecciones originales con ejemplos breves, puntos clave y referencias para profundizar.

01

Modelo de datos

El modelo de datos conecta sintaxis y operaciones con métodos especiales. type, identidad, valor y mutabilidad determinan cómo participa un objeto en el lenguaje.

Ejemplo
class Caja:
    def __init__(self, valor): self.valor = valor
    def __repr__(self): return f"Caja({self.valor!r})"

Puntos importantes

  • La sintaxis delega en protocolos documentados.
  • No llames métodos especiales de forma directa salvo que el protocolo lo requiera.
02

Iteradores personalizados avanzados

Un iterable personalizado implementa __iter__ y puede crear un iterador independiente con __next__; este conserva su estado y termina con StopIteration. Separar iterable e iterador permite recorridos simultáneos y reiniciables, mientras otros métodos como __len__ y __contains__ completan un protocolo coherente.

Ejemplo
class CuentaAtras:
    def __init__(self, inicio): self.inicio = inicio
    def __iter__(self): return IteradorCuentaAtras(self.inicio)
class IteradorCuentaAtras:
    def __init__(self, actual): self.actual = actual
    def __iter__(self): return self
    def __next__(self):
        if self.actual == 0: raise StopIteration
        self.actual -= 1
        return self.actual + 1

Puntos importantes

  • __iter__ del iterable crea estado independiente; __iter__ del iterador devuelve self.
  • StopIteration es parte del protocolo y no un error que deba ocultarse.
Practicar este concepto
03

Descriptores

Un descriptor define __get__, __set__ o __delete__ y controla acceso a atributos desde la clase propietaria. Un descriptor de datos tiene prioridad sobre el diccionario de instancia.

Ejemplo
class Positivo:
    def __set_name__(self, owner, name): self.name = "_" + name
    def __get__(self, obj, owner=None): return getattr(obj, self.name)
    def __set__(self, obj, value):
        if value <= 0: raise ValueError("debe ser positivo")
        setattr(obj, self.name, value)

Puntos importantes

  • __set_name__ recibe el nombre declarado.
  • property, classmethod y funciones usan el protocolo descriptor.
Practicar este concepto
04

MRO y C3

El MRO linealiza una jerarquía respetando precedencia local y monotonía. Clase.mro() permite inspeccionar el orden usado para resolver atributos y super.

Ejemplo
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.mro())

Puntos importantes

  • El orden de bases forma parte del contrato.
  • Una jerarquía inconsistente se rechaza al crear la clase.
05

Herencia múltiple cooperativa

En una jerarquía cooperativa cada método consume sus argumentos y llama super para continuar por el MRO. Saltarse la cadena rompe combinaciones posteriores.

Ejemplo
class Registro:
    def guardar(self, dato): return [dato]
class Auditoria(Registro):
    def guardar(self, dato): return ["audit", *super().guardar(dato)]

Puntos importantes

  • super no nombra una clase padre fija.
  • Las firmas compatibles facilitan cooperación.
Practicar este concepto
06

Clases base abstractas

ABC y @abstractmethod declaran operaciones obligatorias y evitan instanciar clases incompletas. También pueden proporcionar una implementación reutilizable.

Ejemplo
from abc import ABC, abstractmethod
class Repositorio(ABC):
    @abstractmethod
    def obtener(self, id): ...

Puntos importantes

  • Una ABC usa herencia nominal.
  • Registrar una subclase virtual no copia métodos.
07

Protocol y tipado estructural

typing.Protocol describe una interfaz por estructura para análisis estático. Una clase compatible no necesita heredar del protocolo.

Ejemplo
from typing import Protocol
class Cerrable(Protocol):
    def close(self) -> None: ...
def cerrar(recurso: Cerrable) -> None:
    recurso.close()

Puntos importantes

  • Protocol reduce acoplamiento nominal.
  • @runtime_checkable sólo admite comprobaciones estructurales limitadas.
Practicar este concepto
08

Genéricos

Los parámetros de tipo expresan relaciones entre valores. Los límites y restricciones documentan qué operaciones puede asumir una implementación.

Ejemplo
class Pila[T]:
    def __init__(self): self._datos: list[T] = []
    def push(self, valor: T) -> None: self._datos.append(valor)
    def pop(self) -> T: return self._datos.pop()

Puntos importantes

  • La varianza depende de cómo se usa el parámetro.
  • El tipado se borra en gran medida durante runtime.
Practicar este concepto
09

Closures y celdas

Una función interna conserva celdas del ámbito envolvente. nonlocal reasigna la celda; capturar una variable de bucle sin fijarla produce late binding.

Ejemplo
def multiplicador(factor):
    def aplicar(valor):
        return valor * factor
    return aplicar

Puntos importantes

  • El closure conserva referencias, no fotografías.
  • Un argumento predeterminado puede fijar un valor por iteración.
Practicar este concepto
10

Decoradores parametrizados

Un decorador parametrizado añade una fábrica: la llamada exterior recibe configuración, devuelve el decorador y este devuelve el wrapper.

Ejemplo
from functools import wraps
def repetir(veces):
    def decorar(funcion):
        @wraps(funcion)
        def wrapper(*args, **kwargs):
            return [funcion(*args, **kwargs) for _ in range(veces)]
        return wrapper
    return decorar

Puntos importantes

  • La configuración queda en un closure.
  • wraps conserva __wrapped__ y metadatos.
Practicar este concepto
11

send, throw y close

send entrega un valor al yield suspendido, throw inyecta una excepción y close provoca GeneratorExit. El primer avance debe usar next o send(None).

Ejemplo
def acumulador():
    total = 0
    while True:
        valor = yield total
        if valor is not None:
            total += valor

Puntos importantes

  • Un finally permite limpiar al cerrar.
  • No captures GeneratorExit para seguir produciendo valores.
Practicar este concepto
12

Context managers personalizados y contextlib

Un context manager personalizado implementa __enter__ y __exit__ para adquirir, liberar y decidir si propaga una excepción. @contextmanager expresa el mismo contrato con un generador de un único yield, y ExitStack compone dinámicamente varios recursos.

Ejemplo
class Recurso:
    def __enter__(self):
        print("abrir")
        return self
    def __exit__(self, exc_type, exc, traceback):
        print("cerrar")
        return False

Puntos importantes

  • Devolver False desde __exit__ conserva cualquier excepción del bloque.
  • @contextmanager necesita exactamente un yield efectivo y limpieza en finally.
13

Coroutines y async/await

async def crea funciones de coroutine. await suspende la coroutine actual hasta completar un awaitable sin bloquear activamente el hilo del event loop.

Ejemplo
import asyncio
async def calcular():
    await asyncio.sleep(0)
    return 7
print(asyncio.run(calcular()))

Puntos importantes

  • Llamar async def produce un objeto coroutine.
  • El trabajo de CPU sigue bloqueando el hilo si no cede control.
Practicar este concepto
14

Event loop y Tasks

El event loop agenda callbacks y avanza Tasks cuando sus coroutines pueden continuar. create_task mantiene una referencia a una ejecución concurrente cooperativa.

Ejemplo
import asyncio
async def paso(nombre):
    await asyncio.sleep(0)
    return nombre
async def main():
    tarea = asyncio.create_task(paso("A"))
    return await tarea

Puntos importantes

  • Concurrencia no implica paralelismo de CPU.
  • Conserva referencias a Tasks en segundo plano.
15

Concurrencia estructurada con TaskGroup

asyncio.TaskGroup espera todas las tareas al salir. Si una falla, cancela las restantes y agrupa errores no relacionados con cancelación.

Ejemplo
import asyncio
async def main():
    async with asyncio.TaskGroup() as grupo:
        uno = grupo.create_task(asyncio.sleep(0, result=1))
        dos = grupo.create_task(asyncio.sleep(0, result=2))
    return uno.result() + dos.result()

Puntos importantes

  • Las tareas no escapan del bloque sin completar.
  • Los fallos pueden llegar como ExceptionGroup.
Practicar este concepto
16

Errores asíncronos y cancelación

Una excepción de una Task se conserva hasta observar su resultado o esperarla. Task.cancel solicita cancelación inyectando CancelledError en el siguiente punto de suspensión; la limpieza debe usar finally y normalmente volver a propagar la cancelación.

Ejemplo
import asyncio
async def trabajo():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        print("cancelada")
        raise
    finally:
        print("limpieza")

Puntos importantes

  • CancelledError hereda directamente de BaseException.
  • Observa los errores de las Tasks y no suprimas cancelación salvo que restaures deliberadamente el contrato.
17

Iteradores asíncronos

async for usa __aiter__ y __anext__; este devuelve un awaitable y termina con StopAsyncIteration. Un generador asíncrono combina async def con yield.

Ejemplo
import asyncio
async def eventos():
    for valor in range(3):
        await asyncio.sleep(0)
        yield valor

Puntos importantes

  • async for puede ceder entre elementos.
  • aclose permite finalizar un generador asíncrono.
Practicar este concepto
18

Context managers asíncronos

async with espera __aenter__ y __aexit__. @asynccontextmanager permite expresar adquisición y limpieza asíncronas con un generador.

Ejemplo
from contextlib import asynccontextmanager
@asynccontextmanager
async def sesion():
    try:
        yield "activa"
    finally:
        pass

Puntos importantes

  • La limpieza puede contener await.
  • aclosing asegura aclose al abandonar temprano.
19

Copia superficial y profunda

copy.copy crea otro contenedor y comparte referencias internas. copy.deepcopy recorre el grafo con memo para preservar ciclos y evitar duplicar el mismo objeto varias veces.

Ejemplo
from copy import copy, deepcopy
original = {"config": {"activo": True}}
superficial = copy(original)
profunda = deepcopy(original)

Puntos importantes

  • Una copia profunda no siempre es deseable.
  • Las clases pueden personalizar __copy__ y __deepcopy__.
20

Igualdad y hash

Los objetos hashables deben mantener un hash estable y los objetos iguales deben compartir hash. Sobrescribir __eq__ suele desactivar __hash__ para evitar claves mutables incoherentes.

Ejemplo
from dataclasses import dataclass
@dataclass(frozen=True)
class Clave:
    id: int
print(hash(Clave(3)))

Puntos importantes

  • La inmutabilidad práctica favorece hash estable.
  • Las colisiones son válidas; la igualdad las distingue.
Practicar este concepto
21

Sistema de importación

import consulta sys.modules y usa finders y loaders descritos por ModuleSpec. Los imports relativos dependen del paquete, no de la carpeta de trabajo.

Ejemplo
import importlib
modulo = importlib.import_module("json")
print(modulo.__spec__.name)

Puntos importantes

  • sys.modules es la caché de módulos.
  • Modificar sys.path globalmente vuelve frágil la resolución.
22

Pattern matching estructural

Los patrones pueden desestructurar secuencias, mappings y clases. Las guardas se evalúan después de que el patrón capture nombres; el orden de casos es significativo.

Ejemplo
def clasificar(valor):
    match valor:
        case {"tipo": "punto", "x": x, "y": y} if x == y:
            return "diagonal"
        case [primero, *resto]:
            return primero, resto
        case _:
            return None

Puntos importantes

  • Un nombre simple captura; no compara una constante.
  • __match_args__ define posiciones de patrones de clase.
Practicar este concepto
23

partial y singledispatch

functools.partial fija argumentos sin ejecutar todavía. singledispatch selecciona una implementación por el tipo del primer argumento en runtime.

Ejemplo
from functools import partial, singledispatch
doble = partial(pow, exp=2)
@singledispatch
def describir(valor): return "objeto"
@describir.register
def _(valor: int): return "entero"

Puntos importantes

  • singledispatch no implementa despacho múltiple.
  • partial conserva atributos func, args y keywords.
24

Cachés con functools

@cache memoriza por argumentos hashables sin límite; @lru_cache puede acotar entradas. Sólo es correcto reutilizar resultados cuando la función es estable para esos argumentos.

Ejemplo
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
    return n if n < 2 else fibonacci(n - 1) + fibonacci(n - 2)

Puntos importantes

  • La caché mantiene referencias a argumentos y resultados.
  • cache_info permite observar aciertos, fallos y tamaño.
Practicar este concepto