Шесть способов сделать flatten в Python — какой быстрее в 500 раз
Перевод туториала Real Python: разворачиваем вложенный список шестью способами и сравниваем их производительность на матрице 1000×1000. Разница между лидером и аутсайдером — в 500 раз.
Если в коде встречается список списков и нужно превратить его в один плоский, на ум приходит как минимум пять подходов — от обычного for до itertools.chain и NumPy. Какие из них работают за миллисекунды, а какие — за секунды? Перевод и адаптация туториала Real Python с разбором всех способов и замером производительности.
Flatten — операция приведения вложенной структуры к одномерной. Самый распространённый кейс: матрица представлена как список списков, а вам нужен плоский список всех значений, чтобы передать в алгоритм или ML-модель. В отличие от других языков, в Python нет встроенного .flatten() у списков — поэтому стандартное «как это сделать?» имеет несколько ответов разной степени читаемости и эффективности.
Пусть у нас есть матрица 4×4:
Цель — получить:
Ключевые выводы
Flatten в Python: чем пользоваться
Что показал замер на матрице 1000×1000
- Быстрее всего работает обычный цикл с in-place мутацией: += или .extend(). Время — около 5–7 мс.
- itertools.chain() — около 16 мс, но в 3 раза экономнее по памяти; подходит, когда финальный список не нужен полностью.
- List comprehension — около 24 мс. Читаемо и без побочных эффектов, но создаёт промежуточные объекты.
- functools.reduce() + lambda и sum() — от 2,6 секунд. Это в 400–500 раз медленнее за счёт постоянного создания новых промежуточных списков.
- Для произвольной вложенности — рекурсия или итеративный обход со стеком.
- В data-science для NumPy-массивов используйте np.ndarray.flatten() — он быстрее любого варианта на чистых списках.
Способ 1. Цикл for + .extend()
Самый прямолинейный и читаемый вариант: проходим по каждой подсписку и добавляем её элементы к итоговому списку через .extend().
.extend() принимает любой iterable и поэлементно добавляет его в конец списка. Альтернативно можно использовать augmented-concatenation +=:
Обе функции делают одно и то же. Real Python отмечает: flatten_extend читается чуть лучше, потому что метод named — намекает на смысл операции. По скорости варианты идут в первой тройке.
Способ 2. List comprehension
Pythonic-альтернатива однострочнику. Двойной for внутри comprehension перебирает сначала строки, потом элементы строк:
Подход компактнее цикла и не плодит промежуточных переменных. Скорость — заметно медленнее .extend() из-за двух вложенных циклов на уровне comprehension, но всё ещё в диапазоне десятков миллисекунд.
Способ 3. itertools.chain() — экономно по памяти
chain() возвращает итератор, а не список. Это значит, что вы можете обходить элементы потокового, не материализуя весь массив в памяти — полезно, когда матрица большая и хранить полный flat-список нерационально.
На большой матрице chain.from_iterable примерно в три раза медленнее цикла .extend(): накладные расходы на материализацию через list(). Но если итоговый список как таковой не нужен — можно отдать сам итератор потребителю и сэкономить и время, и память.
Способ 4. functools.reduce() и sum() — почему так медленно
Эти варианты часто появляются в туториалах для красоты, но на практике плохо масштабируются. Причина одна: каждая итерация создаёт новый промежуточный список вместо мутации существующего.
iconcat мутирует аккумулятор и держится в топ-3 по скорости. А вот вариант с lambda и оператором + на каждом шаге копирует обе стороны — отсюда и взрывной рост времени на больших данных.
С sum() ситуация ровно такая же:
sum() работает на любых сложениях, но для списков это О(n²) по аллокациям. На матрице 1000×1000 он в ~500 раз медленнее цикла с .extend().
Способ 5. Произвольная вложенность — рекурсия и стек
Все предыдущие способы предполагают, что вложенность ровно одна. Если структура произвольно глубокая или неоднородная, нужен другой подход:
Рекурсия удобна, пока глубина не упирается в sys.getrecursionlimit() (по умолчанию 1000). Для очень глубоких структур стоит переписать через итеративный обход со стеком:
Способ 6. NumPy для data-science
Если вы работаете с матрицами в data-science, у numpy.ndarray есть готовый метод .flatten(). Он быстрее любого варианта на чистых Python-списках, потому что под капотом работает с непрерывным блоком памяти.
Замер производительности
Real Python прогнали все варианты на матрице 1000×1000 через timeit. Результаты, отсортированные по времени:
Между первыми тремя и последними четырьмя — пропасть в 400–500 раз. Причина: топовые функции мутируют существующий список, а reduce(+), reduce(operator.add), reduce(lambda x, y: x + y) и sum() на каждой итерации создают копии.
FAQ
Часто задаваемые вопросы
Какой способ flatten выбрать по умолчанию?
Цикл for с .extend() или +=. Это самые быстрые и читаемые варианты, работающие с любыми объектами в подсписках. List comprehension уступает по скорости, но даёт чистый функциональный код без промежуточной переменной — выбирайте по контексту.
Когда стоит брать itertools.chain()?
Когда вы планируете обходить элементы потоково и не хотите держать весь flat-список в памяти. Например, для streaming-обработки или при работе с матрицами, которые не помещаются целиком в RAM. Если итог всё равно нужен как список — берите .extend(), он быстрее.
Почему sum(matrix, []) — анти-паттерн?
Каждая итерация создаёт новый список через конкатенацию, что даёт квадратичную сложность по аллокациям. На матрице 1000×1000 это около 2,7 секунд против 5 миллисекунд у .extend(). На небольших данных разница не критична, но привычку лучше не закреплять.
Что использовать для произвольно вложенных списков?
Рекурсию с проверкой типа через isinstance() или итеративный обход со стеком. Рекурсия проще, но ограничена sys.getrecursionlimit() (по умолчанию 1000). Для произвольных глубин — итерация со стеком.
Выводы
Если вам нужен один-в-один поведенческий рецепт: for row in matrix: flat.extend(row). Этот код понятен любому, кто знает Python, и упирается в производительность только когда матрица переваливает за миллионы элементов. Если объёмы такие, что нужна потоковая обработка — переключитесь на itertools.chain. Если данные numerical — используйте NumPy.
Главное, чего стоит избегать — sum(matrix, []) и reduce с + или lambda. Они смотрятся изящно в туториалах, но превращают O(n) в O(n²) и ставят вас в первую же боттлнек-историю на проде.
Оригинальный туториал с пошаговыми объяснениями и сравнением — на Real Python, автор — Leodanis Pozo Ramos.