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

Недавно я столкнулся именно с такой ситуацией. Нужно было обработать большой лог-файл — около 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²)

Почему так происходит.

Для каждой строки:

  1. читаем user_id

  2. перебираем весь список пользователей

  3. проверяем совпадение

Если строк миллионы, этот цикл начинает работать катастрофически медленно.

Первый тест

Я взял лог на 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)


  1. Z55
    18.03.2026 05:31

    2. Код выглядит правильным

    вернее, он правильный с точки зрения результата. Но опытному взгляду, подобная реализация сразу бросается в глаза.


    1. Alex-Freeman
      18.03.2026 05:31

      Даже не очень опытному должен бросаться в глаза. Это очевидная проблема исходного кода, даже после онлайн курсов такое должно резать глаз


  1. ohrenet
    18.03.2026 05:31

    Скрипт работал почти 9 минут,

    А времени на все оптимизации потратили гораздо больше.


    1. kAIST
      18.03.2026 05:31

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


      1. ohrenet
        18.03.2026 05:31

        1. Если.

        2. Даже в таких случаях, зачастую это бывает некритично чтобы лезть заморачиваться. А то и вовсе необходимость в самом скрипте отпадает через пару дней.

        Перфекционизм обыкновенный, МКБ-11 MB28.C


        1. Alex-Freeman
          18.03.2026 05:31

          А потом удивляемся откуда столько говнокода

          Скрытый текст


          1. 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.


            1. Alex-Freeman
              18.03.2026 05:31

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


              1. ohrenet
                18.03.2026 05:31

                если разница в 42 раза

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


    1. tenzink
      18.03.2026 05:31

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


      1. ohrenet
        18.03.2026 05:31

        Чисто в качестве образовательного процесса - согласен, сгодится.

        А понимание "можно, но зачем" - оно уже потом, с опытом придёт.


  1. koreec
    18.03.2026 05:31

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


    1. ABATAPA
      18.03.2026 05:31

      Отличный вопрос! Давайте разберём всё по порядку. Во-первых, ...
      :)


    1. censor2005
      18.03.2026 05:31

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


    1. vldmrmlkv
      18.03.2026 05:31

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


  1. vldmrmlkv
    18.03.2026 05:31

    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
    

    А если уникальных юзеров будут миллионы, то словарь users может занять много памяти.
    А если строка не соответствует паттерну и user_id не будет нужным id, вместо id будет текст ошибки или пробелы или второго элемента вообще не будет, или это пустая строка? Ещё может быть проблема с кодировкой при чтении файла. И ещё если бы user_id был числом, то должно быть быстрее и словарь users будет занимать меньше места, т.е. нужно перед users[user_id] += 1 переводить user_id в int.


  1. axion-1
    18.03.2026 05:31

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


  1. SaX_KT
    18.03.2026 05:31

    А если загрузить в pandas dataframe, быстрее не будет?


  1. sunnyfox
    18.03.2026 05:31

    Квадратичная сложность прямо бросается в глаза тем, кто хоть чуть-чуть щупал алгоритмы и структуры данных. И до профайлинга не дошло бы. Использовать Counter было бы ещё проще.