Эффективное программирование на Python: секреты производительности

Раздел: Python -> Оптимизация кода

Основные принципы эффективного Python кода

Как заменить медленные циклы на быстрые конструкции?

List comprehensions - основной инструмент для ускорения. Вместо цикла for с append:

# Медленно
squares = []
for x in range(10):
    squares.append(x**2)
# Быстро
squares = [x**2 for x in range(10)]

эффективный python (эффективное программирование на python)

Генераторные выражения экономят память, особенно при работе с большими данными:

squares_gen = (x**2 for x in range(10**7))

Альтернатива - функция map() с lambda, но читаемость ниже:

squares = list(map(lambda x: x**2, range(10)))

В некоторых случаях filter() с comprehension также эффективен.

Проблемы: list comprehension создает весь список в памяти, что может быть критично при больших объемах. Ошибка: применение comprehension там, где нужен break/continue - тогда лучше цикл.

Как уменьшить потребление памяти при работе с большими данными?

Генераторы - экономят память, вычисляя значения по мере необходимости. Пример с чтением файла:

def read_large_file(file):
    while True:
        line = file.readline()
        if not line:
            break
        yield process(line)

Или использование yield from для делегирования другому генератору.

Итераторы из модуля itertools для цепочек преобразований без создания промежуточных списков:

import itertools
data = itertools.islice(itertools.chain(iter1, iter2), 100)

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

Как избежать повторных вычислений дорогих функций?

lru_cache из модуля functools - простой способ мемоизации с автоматическим кешированием:

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

Результаты предыдущих вызовов сохраняются, что ускоряет рекурсивные алгоритмы.

Ручное кэширование через словарь для тонкого контроля над ключами:

cache = {}
def expensive(x):
    if x not in cache:
        cache[x] = compute(x)
    return cache[x]

Проблемы: утечка памяти при maxsize=None для большого числа различных аргументов. Ошибка: забыть про кеширование для функций с побочными эффектами.

Как выбрать правильную структуру данных для быстрого поиска?

Множества (set) для проверки вхождения - O(1) против O(n) у списка:

valid = set(['a','b','c'])
if item in valid:
    print('найдено')

Словари (dict) - для быстрого доступа по ключу.

frozenset для неизменяемых множеств, collections.defaultdict для группировки:

from collections import defaultdict
groups = defaultdict(list)
for key, value in pairs:
    groups[key].append(value)

Проблема: set требует хэшируемых элементов. Нельзя использовать списки. Ошибка: попытка добавить изменяемый объект вызывает TypeError.

Как ускорить конкатенацию строк?

f-строки - самый быстрый и читаемый способ форматирования:

name = "world"
greeting = f"Hello {name}!"

Для объединения многих строк лучше использовать join():

parts = ['a', 'b', 'c']
result = ''.join(parts)

Использование io.StringIO для накопления строк:

import io
buf = io.StringIO()
for part in parts:
    buf.write(part)
result = buf.getvalue()

Проблемы: конкатенация через оператор + в цикле создает множество промежуточных строк, замедляя выполнение. Ошибка: использовать + в цикле для большого количества строк.

Как профилировать код для выявления узких мест?

timeit для микро-бенчмарков отдельных операций:

import timeit
timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)

cProfile для полного анализа выполнения скрипта:

python -m cProfile -s time my_script.py

line_profiler для построчного профилирования времени выполнения:

@profile
def my_func():
    # код

Проблемы: timeit может быть неточен для функций с побочными эффектами. cProfile добавляет накладные расходы, замедляя выполнение. Ошибка: профилировать без четкой гипотезы о медленных участках.

Расширенные примеры для углубленного понимания оптимизации.

Сравнение методов фильтрации

Тестирование производительности различных способов фильтрации списка чисел:

Пример
import timeit

test_list = list(range(1000))

# Способ 1: цикл for
stmt1 = '''
result = []
for i in test_list:
    if i % 2 == 0:
        result.append(i)
'''

# Способ 2: list comprehension
stmt2 = '[i for i in test_list if i % 2 == 0]'

# Способ 3: filter
stmt3 = 'list(filter(lambda x: x % 2 == 0, test_list))'

print(timeit.timeit(stmt1, globals=globals(), number=10000))
print(timeit.timeit(stmt2, globals=globals(), number=10000))
print(timeit.timeit(stmt3, globals=globals(), number=10000))
0.823
0.452
0.612

List comprehension оказывается самым быстрым в этом сценарии.

Экономия памяти с __slots__

Для классов, создающих много экземпляров, __slots__ уменьшает потребление памяти:

Пример
class Point:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

class PointNoSlots:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p1 = Point(1, 2)
p2 = PointNoSlots(1, 2)
print(sys.getsizeof(p1), sys.getsizeof(p2))
48 56

Разница может быть значительной для тысяч экземпляров.

Использование array.array для числовых массивов

Вместо списка целых чисел можно использовать array.array с типом 'i' (signed int), что уменьшает память и ускоряет операции:

Пример
import array
import sys

lst = list(range(100000))
arr = array.array('i', range(100000))

print(sys.getsizeof(lst))
print(sys.getsizeof(arr))

# Операции с array часто быстрее при работе с числами
824464
400096

Память уменьшается примерно вдвое.

Объединение вложенных списков с itertools.chain.from_iterable

Для сглаживания списка списков:

Пример
import itertools

nested = [[1, 2], [3, 4], [5]]
flat = list(itertools.chain.from_iterable(nested))
print(flat)
[1, 2, 3, 4, 5]

Этот подход эффективнее вложенных list comprehensions ( [x for sub in nested for x in sub] ), особенно для больших данных.

Эффективное программирование на Python - comments

En
эффективный python (python)