Сначала измеряй, потом оптимизируй: timeit и cProfile, что на самом деле блокирует GIL, экономность генераторов и ловушки памяти со строками и срезами.
Открыть этот урок в KodokonНикогда не угадывай, измеряй. timeit изолирует надёжный микробенчмарк: он отключает сборщик мусора и повторяет выполнение, чтобы сгладить шум. cProfile строит карту вызовов целой программы - сортируй по cumtime (накопленное время вместе с вложенными вызовами), чтобы найти настоящих виновников. Золотое правило любой работы над производительностью: медленная строка почти никогда не та, на которую ты думаешь.
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))"GIL (Global Interpreter Lock) гарантирует, что в рамках одного процесса байт-код Python в каждый момент выполняет только один поток. Следствие: потоки не ускоряют чистые вычисления. Зато они по-прежнему отлично подходят для ввода-вывода, потому что на время чтения из сети или с диска GIL освобождается, как и во многих библиотеках на C вроде NumPy. Чтобы загрузить вычислениями несколько ядер, используй multiprocessing или concurrent.futures.ProcessPoolExecutor. В Python 3.13 есть экспериментальный вариант «free-threaded» без GIL, пока редкий в продакшене.
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))Генератор весит несколько сотен байт независимо от длины последовательности: он хранит только своё состояние выполнения и выдаёт каждое значение по запросу. Соединяй их в конвейеры - sum(x * x for x in data) - чтобы обрабатывать потоки данных, ни разу не материализуя промежуточный список. Компромисс, о котором стоит знать: генератор одноразовый. Как только он исчерпан, он больше ничего не выдаёт, и молча - классический источник багов, когда второй проход выглядит «пустым».
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 для генератора остаётся крошечным по сравнению с эквивалентным списком?''.join(parts) выигрывает у out += part, повторяемого в цикле?