Эффективное программирование на 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.pyline_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] ), особенно для больших данных.