Иногда тормоза в коде выглядят как что-то сложное: тяжёлые алгоритмы, огромные базы данных, медленный диск. Но чаще всё намного банальнее — один неудачный цикл, который выполняется миллионы раз.

Недавно я столкнулся именно с такой ситуацией. Нужно было обработать большой лог-файл — около 8 ГБ — и извлечь из него статистику по пользователям. Скрипт работал почти 9 минут, хотя логика казалась предельно простой.
Разбор показал, что проблема была в одном месте.
Исходная задача
Есть большой лог:
timestamp user_id action
Например:
1690039200 421 login 1690039211 421 view 1690039220 182 login 1690039230 421 logout
Нужно посчитать количество действий каждого пользователя.
Наивная реализация выглядела примерно так.
def count_actions(file_path): users = [] with open(file_path) as f: for line in f: user_id = line.split()[1] found = False for u in users: if u["id"] == user_id: u["count"] += 1 found = True break if not found: users.append({"id": user_id, "count": 1}) return users
Код простой и понятный. Он даже работает корректно.
Но есть нюанс.
Где здесь проблема
Проблема в этом фрагменте.
for u in users: if u["id"] == user_id:
Мы ищем пользователя линейным поиском.
Если пользователей много, сложность становится:
O(n²)
Почему так происходит.
Для каждой строки:
читаем user_id
перебираем весь список пользователей
проверяем совпадение
Если строк миллионы, этот цикл начинает работать катастрофически медленно.
Первый тест
Я взял лог на 10 миллионов строк.
Результат выполнения:
8 минут 47 секунд
Процессор почти всё время был загружен.
При профилировании стало видно, где происходит основная потеря времени.
import cProfile cProfile.run("count_actions('log.txt')")
Самая тяжёлая функция — поиск пользователя в списке.
Самое простое решение
Очевидная оптимизация — заменить список на словарь.
Почему.
Словарь Python реализован как хеш-таблица, поэтому поиск происходит за:
O(1)
Перепишем код.
def count_actions(file_path): users = {} with open(file_path) as f: for line in f: user_id = line.split()[1] if user_id in users: users[user_id] += 1 else: users[user_id] = 1 return users
Теперь мы не перебираем всех пользователей.
Мы просто обращаемся к словарю по ключу.
Второй тест
Запускаем тот же лог.
12 секунд
Скрипт ускорился примерно в 42 раза.
Без изменения алгоритма обработки файла.
Без многопоточности.
Без C-расширений.
Просто убрали один цикл.
Можно ли ускорить ещё
Да. В Python есть ещё более удобный инструмент — defaultdict.
Он избавляет от проверки наличия ключа.
from collections import defaultdict def count_actions(file_path): users = defaultdict(int) with open(file_path) as f: for line in f: user_id = line.split()[1] users[user_id] += 1 return users
Код становится не только быстрее, но и чище.
Ещё одна маленькая оптимизация
split() разбивает всю строку.
Но нам нужен только второй элемент.
Можно использовать split с ограничением.
user_id = line.split(" ", 2)[1]
Это уменьшает количество создаваемых объектов.
На больших файлах это тоже заметно.
Итоговая версия
from collections import defaultdict def count_actions(file_path): users = defaultdict(int) with open(file_path) as f: for line in f: parts = line.split(" ", 2) user_id = parts[1] users[user_id] += 1 return users
Что показали тесты
версия |
время |
|---|---|
наивная |
8 мин 47 сек |
словарь |
12 сек |
defaultdict |
10 сек |
Разница — примерно 50×.
Почему такие вещи часто остаются незамеченными
Есть несколько причин.
1. На маленьких данных всё работает быстро
На файле из 1000 строк разница почти незаметна.
Проблема проявляется только на больших данных.
2. Код выглядит правильным
Логика читается легко.
Но читаемость и сложность алгоритма — разные вещи.
3. Python скрывает стоимость операций
Например:
x in list
выглядит как одна операция, но внутри происходит перебор.
Когда стоит задуматься об оптимизации
Есть простой сигнал.
Если в коде есть конструкция:
цикл внутри цикла
и количество данных может расти — почти всегда есть более эффективный способ.
Чаще всего это:
словари
множества
индексы
предварительная агрегация
Небольшой вывод
Иногда ускорение программы не требует сложных оптимизаций.
Достаточно задать один вопрос:
Какая сложность у этого участка кода?
В моём случае всё ускорилось в десятки раз после удаления одного цикла.
И это одна из самых частых причин медленных скриптов, которые я встречаю в Python-проектах.
Интересно, что почти все медленные Python-скрипты, которые я видел в реальных проектах, тормозили не из-за Python.
Проблема почти всегда была в алгоритмах.
Чаще всего встречаются три вещи:
линейный поиск в списке
лишние вложенные циклы
повторные вычисления, которые можно было кешировать
Python в таких случаях просто честно выполняет то, что ему написали.
Поэтому мне стало интересно:
какая самая странная оптимизация кода давала вам самый большой прирост скорости?
Иногда ведь достаточно удалить одну строчку — и программа начинает работать на порядок быстрее.
Делитесь примерами, думаю получится интересная коллекция инженерных историй.
Комментарии (19)

ohrenet
18.03.2026 05:31Скрипт работал почти 9 минут,
А времени на все оптимизации потратили гораздо больше.

kAIST
18.03.2026 05:31Да, но если у тебя этот скрипт запускается периодически в каком нибудь celery и его выполнения ждут остальные задачи...

ohrenet
18.03.2026 05:31Если.
Даже в таких случаях, зачастую это бывает некритично чтобы лезть заморачиваться. А то и вовсе необходимость в самом скрипте отпадает через пару дней.
Перфекционизм обыкновенный, МКБ-11 MB28.C

Alex-Freeman
18.03.2026 05:31А потом удивляемся откуда столько говнокода
Скрытый текст


ohrenet
18.03.2026 05:31"Premature optimization is the root of all evil" is a famous maxim by Donald Knuth (1960s), advising developers to focus on clarity and correctness over micro-optimizations. It argues that spending time on performance improvements before identifying true bottlenecks wastes resources, increases complexity, and yields harder-to-maintain code.

Alex-Freeman
18.03.2026 05:31Если мы говорим о коде выше, то это высказывание не применимо, во первых это не микро оптимизация, если разница в 42 раза, во вторых она повышает читаемость и лаконичность. Ваши комментарии можно использовать как иллюстрацию эффекта Даннинга-Крюгера

ohrenet
18.03.2026 05:31если разница в 42 раза
Да хоть в 142 раза. Скрипт выполняется всего 9 минут. И возможно больше не будет выполнятся никогда. Либо посмотрев первичный результат, у автора появятся какие-то ещё идеи и вводные, которые вообще выкинут оптимизируемую строчку. Но всё это познаётся только с годами опыта, да.

tenzink
18.03.2026 05:31Что менее важно, такая оптимизация делается быстрее 9 минут. Важнее - научиться видеть подобные пессимизации на пустом месте. Так что автор молодец - это окупится десятки раз

ohrenet
18.03.2026 05:31Чисто в качестве образовательного процесса - согласен, сгодится.
А понимание "можно, но зачем" - оно уже потом, с опытом придёт.

koreec
18.03.2026 05:31Оптимизацию делал ИИ, он же и пост писал. Да?

censor2005
18.03.2026 05:31"Придумай неоптимальный код на Python, который медленно работает, и который можно легко ускорить за счёт оптимизации. Напиши статью по этой теме для Хабра"

vldmrmlkv
18.03.2026 05:31Это делал человек с помощью ИИ, что ещё забавнее т.к. улучшать код ещё есть куда, но этого не сделано. Можно же было просто закинуть статью в ллм и попросить улучшить, проверить на ошибки, etc.

vldmrmlkv
18.03.2026 05:31from collections import defaultdict def count_actions(file_path): users = defaultdict(int) with open(file_path) as f: for line in f: user_id = line.split()[1] users[user_id] += 1 return usersА если уникальных юзеров будут миллионы, то словарь users может занять много памяти.
А если строка не соответствует паттерну и user_id не будет нужным id, вместо id будет текст ошибки или пробелы или второго элемента вообще не будет, или это пустая строка? Ещё может быть проблема с кодировкой при чтении файла. И ещё если бы user_id был числом, то должно быть быстрее и словарь users будет занимать меньше места, т.е. нужно перед users[user_id] += 1 переводить user_id в int.

axion-1
18.03.2026 05:31Изначальная реализация не только медленная, но ещё и менее читабельна. Выглядит как написанная студентом ещё не освоившим словари.

sunnyfox
18.03.2026 05:31Квадратичная сложность прямо бросается в глаза тем, кто хоть чуть-чуть щупал алгоритмы и структуры данных. И до профайлинга не дошло бы. Использовать Counter было бы ещё проще.
Z55
вернее, он правильный с точки зрения результата. Но опытному взгляду, подобная реализация сразу бросается в глаза.
Alex-Freeman
Даже не очень опытному должен бросаться в глаза. Это очевидная проблема исходного кода, даже после онлайн курсов такое должно резать глаз