Эффективные приёмы написания Python-кода

Раздел: Основы Python -> Стиль кода

Основные стили написания кода при работе с последовательностями

При обработке списков и других итерируемых объектов в Python можно применять разные подходы. Каждый из них имеет свои цели, преимущества и ограничения. В этой части рассматриваются наиболее распространённые стили: от классических циклов до современных генераторов и функций высшего порядка. Все примеры сопровождаются пояснениями и разбором типичных ошибок.

Как компактно и быстро создать новый список на основе существующего с преобразованием элементов?

Списковое включение (list comprehension) - это наиболее питоничный и эффективный способ. Оно сочетает в себе краткость и хорошую производительность за счёт оптимизаций на уровне C.


numbers = [1, 2, 3, 4, 5]
squares = [x ** 2 for x in numbers]
print(squares)

Python стили (стили программирования в python)

[1, 4, 9, 16, 25]

Выражение [x ** 2 for x in numbers] читается как «создать список, где каждый элемент равен квадрату x для каждого x из numbers». Внутренний цикл выполняется быстро, а код остаётся лаконичным.

Типичные проблемы и ошибки:

  • Чрезмерная вложенность приводит к потере читаемости. Например, [x+y for x in a for y in b] ещё приемлемо, но три уровня и более лучше заменить на обычные циклы.
  • Использование сложных условий (if) внутри включения может усложнить понимание. Рекомендуется выносить логику в отдельную функцию.
  • Попытка изменить исходный список внутри включения (например, через side effect) - плохая практика, так как включение предназначено для порождения нового списка.

Как получить список квадратов чисел, используя цикл for?

Классический императивный стиль - цикл for с накоплением результатов в заранее созданном списке.


numbers = [1, 2, 3, 4, 5]
squares = []
for x in numbers:
    squares.append(x ** 2)
print(squares)
[1, 4, 9, 16, 25]

Этот способ нагляден и легко отлаживается. Однако он более многословен и медленнее спискового включения из‑за многократных вызовов append.

Типичные проблемы и ошибки:

  • Изменение списка во время итерации (добавление или удаление элементов) может привести к пропуску элементов или бесконечному циклу. Лучше создавать новый список.
  • Забытая инициализация пустого списка перед циклом - частая ошибка, ведущая к NameError.

Как применить функцию к каждому элементу списка, не используя цикл?

Функциональный стиль предлагает функции map() и filter(). Они возвращают итераторы, которые затем можно преобразовать в список.


numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x ** 2, numbers))
print(squares)
[1, 4, 9, 16, 25]

Здесь map применяет анонимную функцию lambda к каждому элементу. Такой подход удобен, когда уже есть именованная функция, или при цепочках преобразований (например, map+filter).

Типичные проблемы и ошибки:

  • Забытый вызов list() - map возвращает объект-итератор, который нельзя напрямую вывести или индексировать.
  • Чрезмерное использование lambda снижает читаемость. Для нетривиальных преобразований лучше определить функцию отдельно.
  • При работе с большими данными map без преобразования в список даёт преимущество по памяти, но после преобразования теряется.

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

Генераторы (generator expressions) похожи на списковые включения, но используют круглые скобки и возвращают итератор, вычисляющий элементы по мере необходимости. Это позволяет обрабатывать огромные наборы данных без загрузки в память.


numbers = range(1, 1000001)  # миллион чисел
squares_gen = (x ** 2 for x in numbers)
# Для получения первых 5 элементов можно преобразовать в список
first_five = list(next(squares_gen) for _ in range(5))
print(first_five)
[1, 4, 9, 16, 25]

Генераторы незаменимы при работе с потоками данных или бесконечными последовательностями. Они также поддерживают все возможности списковых включений (условия, вложенность).

Типичные проблемы и ошибки:

  • Генератор можно обойти только один раз. Повторное использование потребует создания нового генератора.
  • Ошибка StopIteration возникает, если пытаться получить следующий элемент после исчерпания генератора. Используйте for или явные проверки.
  • Генераторы сложнее отлаживать, так как их состояние не сохраняется после исчерпания.

Расширенные примеры и нераспространённые приёмы

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

Вложенные списковые включения для матриц

Создание матрицы 3x3 и транспонирование:

Пример

matrix = [[i * 3 + j + 1 for j in range(3)] for i in range(3)]
print("Исходная матрица:")
for row in matrix:
    print(row)

transposed = [[matrix[j][i] for j in range(3)] for i in range(3)]
print("Транспонированная:")
for row in transposed:
    print(row)
Исходная матрица:
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
Транспонированная:
[1, 4, 7]
[2, 5, 8]
[3, 6, 9]

Использование вложенных включений позволяет компактно преобразовывать двумерные структуры, но при большом размере лучше применить NumPy.

map с несколькими итераторами

Поэлементное сложение двух списков:

Пример

a = [1, 2, 3]
b = [4, 5, 6]
sums = list(map(lambda x, y: x + y, a, b))
print(sums)
[5, 7, 9]

map принимает несколько итерируемых объектов; функция должна иметь соответствующее количество аргументов. Если списки разной длины, итерация остановится по кратчайшему.

Генератор с yield и yield from

Реализация собственного генератора для последовательности Фибоначчи:

Пример

def fibonacci(limit):
    a, b = 0, 1
    while a < limit:
        yield a
        a, b = b, a + b

fib_gen = fibonacci(100)
print(list(fib_gen))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Ключевое слово yield from позволяет делегировать часть работы другому генератору. Например, объединение нескольких последовательностей:

Пример

def chain_generators(*iters):
    for it in iters:
        yield from it

result = list(chain_generators(range(3), range(10, 13)))
print(result)
[0, 1, 2, 10, 11, 12]

Комбинирование filter и map с ленивыми вычислениями

Отфильтровать чётные числа, затем возвести их в квадрат, используя только функции высшего порядка:

Пример

numbers = range(1, 11)
even_squares = map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, numbers))
print(list(even_squares))
[4, 16, 36, 64, 100]

Такая цепочка работает лениво: filter проверяет условие, map преобразует, и только вызов list() запускает реальные вычисления. Это полезно при работе с большими данными, когда преобразования нужно выполнить только один раз.

Использование itertools для сложных итераций

Модуль itertools предоставляет множество инструментов для функционального стиля. Например, itertools.accumulate для накопления сумм:

Пример

from itertools import accumulate

numbers = [1, 2, 3, 4, 5]
cumsum = list(accumulate(numbers))
print(cumsum)
[1, 3, 6, 10, 15]

А itertools.chain объединяет несколько итераторов в один, что аналогично yield from но готовыми средствами:

Пример

from itertools import chain

combined = list(chain([1, 2], {3, 4}, "56"))
print(combined)
[1, 2, 3, 4, '5', '6']

Производительность различных стилей

Для точного измерения времени выполнения можно использовать модуль timeit. Сравнение спискового включения, цикла и map на миллионе элементов:

Пример

import timeit

setup = 'numbers = range(1, 1000001)'

list_comp_time = timeit.timeit('[x ** 2 for x in numbers]', setup=setup, number=10)
loop_time = timeit.timeit('result = []; [result.append(x ** 2) for x in numbers]', setup=setup, number=10)
map_time = timeit.timeit('list(map(lambda x: x ** 2, numbers))', setup=setup, number=10)

print(f"list comprehension: {list_comp_time:.3f}s")
print(f"loop with append: {loop_time:.3f}s")
print(f"map + list: {map_time:.3f}s")
list comprehension: 0.345s
loop with append: 0.512s
map + list: 0.421s

Результаты показывают, что списковое включение обычно быстрее, хотя конкретные цифры зависят от версии Python и аппаратного обеспечения. Генераторное выражение без преобразования в список будет ещё быстрее по памяти, но не по времени при полном обходе.

Стили программирования в Python - comments

En
Python стили (python)