Команда Python for Devs подготовила перевод статьи о том, как делать код на Python быстрее без переписывания проектов с нуля. В статье 10 практичных приёмов — от sets и bisect до локальных функций и предвыделения памяти — которые дают реальный прирост скорости в типовых сценариях.
В быстро меняющемся мире разработки Python прочно занял место одного из ведущих языков благодаря своей простоте, читаемости и универсальности. Он лежит в основе огромного числа приложений — от веб-разработки до искусственного интеллекта и data engineering. Однако под его элегантным синтаксисом скрывается сложность: узкие места производительности, способные превратить вполне рабочие скрипты в откровенно медленные процессы.
Будь то обработка больших датасетов, создание систем реального времени или повышение вычислительной эффективности — оптимизация питонячего кода под скорость нередко становится ключевым фактором для получения выдающихся результатов.
В этом гайде собраны 10 тщательно проверенных стратегий ускорения. Используя встроенные возможности Python, эффективные структуры данных и низкоуровневые техники оптимизации, он предлагает практичные методы повышения скорости выполнения без ущерба для присущей языку ясности и выразительности. Подкреплённые реальными бенчмарками и показателями, а также наглядными примерами кода, эти приёмы демонстрируют, как небольшие улучшения способны дать значительный прирост производительности — помогая разработчикам пройти путь от уверенного владения Python до уровня настоящих мастеров высокопроизводительного программирования.
Поехали — прокачаем твоё питонячье мастерство!
Приём 1: Используйте sets для проверки принадлежности
Когда нужно узнать, содержится ли элемент в наборе данных, использование списка может быть неэффективным — особенно по мере его роста. Проверка принадлежности в списке (x in some_list) требует последовательного обхода всех элементов, что даёт линейную сложность по времени (O(n)):
big_list = list(range(1000000))
big_set = set(big_list)
start = time.time()
print(999999 in big_list)
print(f"List lookup: {time.time() - start:.6f}s")
start = time.time()
print(999999 in big_set)
print(f"Set lookup: {time.time() - start:.6f}s")
Измеренное время:
List lookup: ~0.015000s
Set lookup: ~0.000020s
В отличие от списков, sets в Python реализованы как хеш-таблицы, что обеспечивает в среднем постоянное время поиска (O(1)). Поэтому проверка того, существует ли значение в set, значительно быстрее, особенно при работе с большими объемами данных.
Для задач вроде удаления дублей, валидации входных данных или сопоставления элементов между коллекциями sets куда эффективнее списков. Они ускоряют не только проверку принадлежности, но и такие операции, как объединение, пересечение и разность — делая их и быстрее, и выразительнее.
Заменив списки на sets в местах, где важна производительность проверки принадлежности, можно получить ощутимый прирост скорости, практически не меняя логику программы.
Приём 2: Избегайте лишних копий
Копирование крупных объектов — таких как списки, словари или массивы — может быть дорогим как по времени, так и по памяти. Каждая копия создаёт новый объект в памяти, что приводит к заметным накладным расходам, особенно при работе с большими наборами данных или внутри плотных циклов.
Когда это возможно, изменяйте объекты на месте вместо создания дубликатов. Это сокращает потребление памяти и повышает производительность, поскольку не требует выделения и заполнения новых структур. Многие встроенные структуры данных Python имеют методы для изменения на месте (sort, append, update), что позволяет обходиться без копий.
numbers = list(range(1000000))
def modify_list(lst):
lst[0] = 999
return lst
start = time.time()
result = modify_list(numbers)
print(f"In-place: {time.time() - start:.4f}s")
def copy_list(lst):
new_lst = lst.copy()
new_lst[0] = 999
return new_lst
start = time.time()
result = copy_list(numbers)
print(f"Copy: {time.time() - start:.4f}s")
Измеренное время:
In-place: ~0.0001s
Copy: ~0.0100s
В производительно критичном коде важно осознавать, когда и как создаются копии объектов — это может заметно повлиять на скорость. Используя ссылки и операции “на месте”, вы можете писать более быстрый и экономный по памяти код, особенно при работе с крупными или сложными структурами данных.
Приём 3: Используйте slots для экономии памяти
По умолчанию экземпляры классов в Python хранят свои атрибуты в динамическом словаре (__dict__). Это даёт гибкость, но сопровождается лишними расходами памяти и чуть более медленным доступом к атрибутам.
slots позволяет явно задать фиксированный набор атрибутов для класса. В этом случае dict не создаётся, что снижает потребление памяти — особенно заметно при создании большого количества экземпляров. Кроме того, за счёт упрощённой внутренней структуры доступ к атрибутам становится немного быстрее.
Хотя slots ограничивает возможность динамического добавления атрибутов, в условиях ограниченной памяти или требований к производительности эта жертва себя оправдывает. Для “лёгких” классов или контейнеров данных использование slots — простой способ сделать код эффективнее.
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
start = time.time()
points = [Point(i, i+1) for i in range(1000000)]
print(f"With slots: {time.time() - start:.4f}s")
Измеренное время:
With slots: ~0.1200s
Without slots: ~0.1500s
With __slots__: ~0.1200s
Without __slots__: ~0.1500s
Приём 4: Используйте функции модуля math вместо операторов
Для численных вычислений модуль math предлагает функции, реализованные на C, — они работают быстрее и точнее, чем аналогичные операции, написанные на чистом Python.
Например, math.sqrt() обычно быстрее и точнее, чем возведение числа в степень 0.5 через оператор **. Точно так же функции math.sin(), math.exp(), math.log() и другие сильно оптимизированы по скорости и надёжности.
Эти преимущества особенно заметны в плотных циклах или при вычислениях в больших масштабах. Используя модуль math для тяжёлых численных операций, вы получаете и более быструю работу, и более стабильные результаты — что делает его предпочтительным выбором для научных вычислений, симуляций и любого кода, насыщенного математикой.

Используйте функции math вместо операторов. PyCharm ещё больше упрощает использование модуля math благодаря умному автодополнению. Стоит ввести math., и вы получите выпадающий список доступных математических функций и констант — таких как sqrt(), sin(), cos(), log(), pi и множество других — с встроенной документацией.
Это ускоряет разработку: не нужно помнить точные названия функций, а также появляется стимул использовать оптимизированные встроенные реализации вместо самописных или основанных на операторах. Благодаря таким подсказкам разработчики быстрее осваивают возможности модуля и пишут более чистый и быстрый численный код.
import math
numbers = list(range(10000000))
start = time.time()
roots = [math.sqrt(n) for n in numbers]
print(f"Math sqrt: {time.time() - start:.4f}s")
start = time.time()
roots = [n ** 0.5 for n in numbers]
print(f"Operator: {time.time() - start:.4f}s")
Измеренное время:
math.sqrt: ~0.2000s
Operator: ~0.2500s
Приём 5: Предварительно выделяйте память, если известен размер
Когда вы динамически формируете списки или массивы, Python по мере их роста автоматически перераспределяет память. Это удобно, но такое перераспределение требует выделения памяти и копирования данных — а значит, добавляет накладные расходы, особенно в больших или производительно критичных циклах.
Если конечный размер структуры данных известен заранее, предварительное выделение памяти может заметно улучшить производительность. Инициализировав список или массив фиксированной длины, вы избегаете повторных перераспределений и позволяете Python (или таким библиотекам, как NumPy) эффективнее управлять памятью.
Этот приём особенно полезен в численных расчётах, симуляциях и масштабной обработке данных, где даже небольшие оптимизации суммируются. Предварительное выделение снижает фрагментацию, улучшает локальность данных в кэше и делает выполнение более предсказуемым.
start = time.time()
result = [0] * 1000000
for i in range(1000000):
result[i] = i
print(f"Pre-allocated: {time.time() - start:.4f}s")
start = time.time()
result = []
for i in range(1000000):
result.append(i)
print(f"Dynamic: {time.time() - start:.4f}s")
Измеренное время:
Pre-allocated: ~0.0300s
Dynamic: ~0.0400s
Приём 6: Избегайте обработки исключений в горячих циклах
Механизм исключений в Python мощный и чистый, он отлично подходит для обработки неожиданных ситуаций. Но он не предназначен для использования в высокочастотных участках кода. Генерация и перехват исключений требует разворачивания стека и переключения контекста, что является довольно дорогой операцией.
В горячих циклах — участках кода, которые выполняются многократно или обрабатывают большие объёмы данных — использование исключений в качестве управляющей логики может серьёзно замедлить выполнение. Вместо этого лучше использовать условные проверки (if, in, is и т.п.), чтобы предотвратить ошибку до её возникновения. Такой подход гораздо быстрее и работает предсказуемее.
Оставляя исключения для действительно исключительных случаев, а не для нормального потока управления, вы пишете более чистый и более быстрый код — особенно в плотных циклах и системах реального времени, где производительность критична.
numbers = list(range(10000000))
start = time.time()
total = 0
for i in numbers:
if i % 2 != 0:
total += i // 2
else:
total += i
print(f"Conditional: {time.time() - start:.4f}s")
start = time.time()
total = 0
for i in numbers:
try:
total += i / (i % 2)
except ZeroDivisionError:
total += i
print(f"Exception: {time.time() - start:.4f}s")
Измеренное время:
Conditional: ~0.3000s
Exception: ~0.6000s
Приём 7: Используйте локальные функции для повторяющейся логики
Когда какой-то фрагмент логики многократно используется внутри одной функции, имеет смысл оформить его как локальную (вложенную) функцию — или, иначе, замыкание. Локальные функции выигрывают в производительности за счёт более быстрого поиска имён: Python обращается к локальным областям видимости быстрее, чем к глобальной.
Помимо прироста скорости, локальные функции помогают инкапсулировать логику, делают код чище и более модульным. Они могут захватывать переменные из внешней области видимости, что позволяет писать более гибкую и переиспользуемую внутреннюю логику без передачи лишних аргументов.
Этот приём особенно полезен в функциях, где одна и та же операция выполняется много раз — в циклах, при трансформации данных или в рекурсивных процессах. Держа часто используемую логику рядом (локально), вы уменьшаете и накладные расходы при выполнении, и когнитивную нагрузку при чтении кода.
def outer():
def add_pair(a, b):
return a + b
result = 0
for i in range(10000000):
result = add_pair(result, i)
return result
start = time.time()
result = outer()
print(f"Local function: {time.time() - start:.4f}s")
def add_pair(a, b):
return a + b
start = time.time()
result = 0
for i in range(10000000):
result = add_pair(result, i)
print(f"Global function: {time.time() - start:.4f}s")
Измеренное время:
Local function: ~0.4000s
Global function: ~0.4500s
Приём 8: Используйте itertools для комбинаторных операций
Когда нужно работать с перестановками, комбинациями, декартовым произведением и другими задачами, основанными на итераторах, модуль itertools в Python предоставляет набор очень эффективных инструментов, оптимизированных на C специально под такие случаи.
Функции вроде product(), permutations(), combinations() и combinations_with_replacement() генерируют элементы лениво — то есть не хранят весь результат в памяти. Это позволяет работать с большими или даже бесконечными последовательностями без тех накладных расходов по памяти и скорости, которые возникают у ручных реализаций.
Помимо высокой скорости, функции itertools хорошо сочетаются друг с другом и экономно расходуют память, что делает их отличным выбором для сложной обработки данных, разработки алгоритмов и решения задач, встречающихся в симуляциях, поисковых алгоритмах или спортивном программировании. Когда важны производительность и масштабируемость, itertools — одно из первых мест, куда стоит обратиться.
from itertools import product
items = [1, 2, 3] * 10
start = time.time()
result = list(product(items, repeat=2))
print(f"Itertools: {time.time() - start:.4f}s")
start = time.time()
result = []
for x in items:
for y in items:
result.append((x, y))
print(f"Loops: {time.time() - start:.4f}s")
Измеренное время:
itertools: ~0.0005sLoops: ~0.0020s
Приём 9: Используйте bisect для работы с отсортированными списками
При работе с отсортированными списками линейный поиск или ручная логика вставки могут быть неэффективны — особенно по мере роста списка. Модуль bisect в Python предоставляет быстрые и удобные инструменты для поддержания отсортированного порядка на базе двоичного поиска.
С помощью функций bisect_left(), bisect_right() и insort() можно выполнять вставки и поиск за O(log n), в отличие от O(n) при простом последовательном обходе. Это особенно полезно в задачах вроде поддержки таблиц лидеров, временных линий событий или реализации эффективных диапазонных запросов.
Используя bisect, вы избегаете повторной сортировки после каждого изменения и получаете заметный прирост производительности при работе с динамическими отсортированными данными. Это лёгкий и мощный инструмент, который приносит алгоритмическую эффективность в повседневные операции со списками.
import bisect
numbers = sorted(list(range(0, 1000000, 2)))
start = time.time()
bisect.insort(numbers, 75432)
print(f"Bisect: {time.time() - start:.4f}s")
start = time.time()
for i, num in enumerate(numbers):
if num > 75432:
numbers.insert(i, 75432)
break
print(f"Loop: {time.time() - start:.4f}s")
Измеренное время:
bisect: ~0.0001sLoop: ~0.0100s
Приём 10: Избегайте повторных вызовов функции в циклах
Если одна и та же функция вызывается много раз внутри цикла — особенно если она дорогая по вычислениям или каждый раз даёт один и тот же результат, — это создаёт лишние накладные расходы. Даже относительно быстрые функции при миллионах вызовов могут заметно замедлить код.
Чтобы оптимизировать, вычислите значение один раз до входа в цикл и сохраните его в локальную переменную. Это уменьшает накладные расходы на вызовы функции и ускоряет выполнение, особенно в критичных по производительности участках.
Приём очень простой, но эффективный. Он не только ускоряет выполнение, но и делает код понятнее — сразу видно, что значение внутри цикла постоянно. Кэширование результата функции — один из самых лёгких способов убрать лишние вычисления и сделать код эффективнее.
def expensive_operation():
time.sleep(0.001)
return 42
start = time.time()
cached_value = expensive_operation()
result = 0
for i in range(1000):
result += cached_value
print(f"Cached: {time.time() - start:.4f}s")
start = time.time()
result = 0
for i in range(1000):
result += expensive_operation()
print(f"Repeated: {time.time() - start:.4f}s")
Измеренное время:
Cached: ~0.0010s
Repeated: ~1.0000s
Русскоязычное сообщество про Python

Друзья! Эту статью подготовила команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!