Mide antes de optimizar: timeit y cProfile, lo que el GIL bloquea realmente, la frugalidad de los generadores y las trampas de memoria de las cadenas y los slices.
Abrir esta lección en KodokonNunca adivines, mide. timeit aísla un micro-benchmark fiable: desactiva el recolector de basura y repite la ejecución para suavizar el ruido. cProfile cartografía las llamadas de todo un programa - ordena por cumtime (tiempo acumulado, subllamadas incluidas) para identificar a los verdaderos culpables. La regla de oro de cualquier trabajo de rendimiento: la línea lenta casi nunca es la que sospechas.
python -m timeit "sum(x * x for x in range(1000))"
python -m timeit -s "s = 'abc' * 100" "s.upper()"
python -m cProfile -s cumtime -m timeit "min(range(50))"El GIL (Global Interpreter Lock) garantiza que solo un hilo ejecuta bytecode de Python a la vez dentro de un proceso. Consecuencia: los hilos no aceleran el cálculo puro. Siguen siendo, en cambio, perfectos para la E/S, porque el GIL se libera durante las lecturas de red o de disco, así como por muchas bibliotecas en C como NumPy. Para saturar varios núcleos con cálculo, pasa por multiprocessing o concurrent.futures.ProcessPoolExecutor. Python 3.13 ofrece una variante experimental "free-threaded" sin GIL, todavía poco habitual en producción.
import sys
squares_list = [x * x for x in range(1_000_000)]
squares_gen = (x * x for x in range(1_000_000))
print(sys.getsizeof(squares_list), "bytes")
print(sys.getsizeof(squares_gen), "bytes")
print(sum(squares_gen))Un generador pesa unos pocos cientos de bytes sea cual sea la longitud de la secuencia: solo almacena su estado de ejecución y produce cada valor bajo demanda. Encadénalos en pipelines - sum(x * x for x in data) - para procesar flujos sin materializar nunca una lista intermedia. Un compromiso que conviene conocer: un generador es de un solo uso. Una vez agotado, no produce nada más, en silencio - una fuente clásica de errores en la que la segunda iteración parece "vacía".
import timeit
def concat(n: int) -> str:
out = ""
for i in range(n):
out += str(i)
return out
def join(n: int) -> str:
return "".join(str(i) for i in range(n))
t1 = timeit.timeit(lambda: concat(20_000), number=10)
t2 = timeit.timeit(lambda: join(20_000), number=10)
print(f"concat: {t1:.3f} s / join: {t2:.3f} s")sys.getsizeof de un generador se mantiene minúsculo comparado con la lista equivalente?''.join(parts) gana a out += part repetido dentro de un bucle?