Списковые включения (list comprehension) в Python: понятные примеры и лучшие практики

Что такое списковое включение и как оно работает
Списковое включение — это компактный синтаксис для создания списка на основе существуемых итерируемых объектов (списков, диапазонов, результатов функций и т. д.). Внутри одного выражения вы можете вычислить значение, перебрать элементы и при необходимости отфильтровать их.
Ключевые элементы: выражение (expression), итератор (for), необязательное условие (if). В одном предложении: списковое включение создаёт новый список, применяя выражение ко всем элементам входного итерируемого объекта и (опционально) отбирая те, которые удовлетворяют условию.
Простейший шаблон:
ComprehensionVariable = [expression for item in iterable]Пример: преобразовать диапазон чисел в их удвоенные значения:
multiplesOf3 = [i*3 for i in range(1, 11)]
print(multiplesOf3)
# Вывод: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]Важно: списковое включение — выражение, а не цикл с телом. Поэтому нельзя поместить в него несколько логических операторов с побочными эффектами (например, print() и одновременно возвращать значение) без явного проектирования.
Основные случаи использования с примерами
- Простая трансформация:
myList = []
for i in range(1, 11):
myList.append(i * 3)
print(myList)
# Альтернатива в виде comprehension:
multiplesOf3 = [i*3 for i in range(1, 11)]- Фильтрация с if:
oddNumbers = [i for i in range(1, 11) if i % 2 == 1]
print(oddNumbers)
# Вывод: [1, 3, 5, 7, 9](В исходном тексте было неверное условие if not i%2==2 — это избыточно и потенциально вводит в заблуждение. Реальное условие для нечётных чисел: i % 2 == 1 или not i % 2 == 0.)
- Несколько условий (цепочка if):
oddNumbers = [i for i in range(1, 11) if i % 2 == 1 if i < 4]
print(oddNumbers)
# Вывод: [1, 3]- Вложенные циклы:
someNums = [i*2 for i in range(1, 3) for k in range(4)]
# эквивалентно:
# for i in range(1,3):
# for k in range(4):
# output.append(i*2)- Вложенное списковое включение (список списков):
someNums = [[i*2 for i in range(1, 3)] for _ in range(4)]
print(someNums)
# Вывод: [[2, 4], [2, 4], [2, 4], [2, 4]]- Работа со строками — счётчик слов:
word = ["This is a python list comprehension tutorial"]
wordCounter = [i.count(' ') + 1 for i in word]
print(wordCounter)
# Вывод: [7]- Вызов функции внутри comprehension:
Numbers = [4, 7, 8, 15, 17, 10]
def multiplier(n):
multiple = n*2
return multiple
multipleEven = [multiplier(i) for i in Numbers if i % 2 == 0]
print(multipleEven)
# Вывод: [8, 16, 20]- Построение словаря и множества (см. раздел ниже).
Словарные и множественные включения
Python поддерживает синтаксис аналогичный списковому включению для словарей (dict) и множеств (set).
Пример словарного включения:
corresponding = {i: i*2 for i in range(10) if i % 2 == 1}
print(corresponding)
# Ожидаемый вывод: {1: 2, 3: 6, 5: 10, 7: 14, 9: 18}Пример множественного включения:
numbers = {i**2 for i in range(10) if i % 4 == 0}
print(numbers)
# Вывод: {0, 16, 64}Отличие множества: оно автоматически убирает дубликаты — структура не гарантирует порядок элементов.
Когда не стоит использовать списковые включения — контрпримеры
- Слишком длинные выражения (больше ~1 строки в читаемом виде). Если выражение нельзя уместить и прочитать быстро, лучше вынести логику в отдельную функцию и использовать обычный цикл.
- Побочные эффекты: если внутри вам нужны вызовы, изменяющие состояние (запись в файл, логирование, изменение внешних переменных), comprehension снижает читаемость побочных эффектов.
- Сложная вложенная логика с несколькими уровнями условий и циклов — лучше обычные циклы и комментарии.
- Очень большие входные данные: список создаёт всю коллекцию в памяти. Для ленивой обработки предпочтительнее генераторные выражения или itertools.
Пример, когда comprehension не подходит:
# Плохой пример: комбинированные побочные эффекты и возврат значения
results = [log_and_return(x) for x in large_iterable]
# Лучше:
for x in large_iterable:
result = log_and_return(x)
handle(result)Альтернативы и оптимизации
- Генераторные выражения: похожи на списковые включения, но возвращают итератор и не строят весь список в памяти.
gen = (i*2 for i in range(10**6))
for val in gen:
process(val)Функции map() и filter(): полезны, когда нужно применить простую функцию ко всем элементам или отфильтровать их; в сочетании с lambda компактны, но иногда менее читабельны.
itertools: эффективны для ленивой обработки и комбинирования сложных итераторов.
Практические эвристики и ментальные модели
- Правило трёх элементов: если comprehension занимает более трёх логических частей (например: несколько вложенных for и несколько if), стоит задуматься о рефакторинге.
- «Выражение — одно действие»: думайте, что comprehension выполняет над элементом один трансформирующий шаг и, возможно, фильтрацию.
- Читаемость важнее краткости: компактный код — это не всегда лучший код.
Чек-листы для ролей
Для разработчика:
- Убедиться, что comprehension легко понять за 5–10 секунд.
- Проверить отсутствие побочных эффектов.
- Оценить использование памяти (будет ли список большим?).
Для ревьюера кода:
- Есть ли тесты, покрывающие крайние случаи (пустой вход, None, неправильные типы)?
- Не скрывает ли comprehension ошибки (например, подавляет исключения внутри вызова)?
Для инженера по производительности:
- Нужно ли заменить на генератор или streaming-пайплайн?
- Как ведёт себя память при больших входных данных?
Мини-методология: как прийти к корректному использованию
- Определите цель: трансформация, фильтрация или построение структуры.
- Напишите простую версию в обычном цикле, чтобы проверить логику.
- Перепишите в comprehension только если выражение остаётся понятным.
- Добавьте тесты для граничных случаев.
- Если объём данных велик — используйте генератор.
Шпаргалка (cheat sheet)
- Списковое включение: [expr for x in iterable if condition]
- Словарь: {k: v for k, v in iterable}
- Множество: {expr for x in iterable}
- Генератор: (expr for x in iterable)
Короткие примеры:
# Уникальные квадраты чётных чисел
unique_even_squares = {x*x for x in range(20) if x % 2 == 0}
# Словарь: число -> строка
num_to_str = {i: str(i) for i in range(5)}Критерии приёмки
- Код работает для пустого и типичных входов.
- Нет побочных эффектов внутри comprehension (если это нежелательно).
- Тесты покрывают неизвестные и граничные кейсы.
- При больших объёмах данных проверено использование памяти или применён генератор.
Примеры использования в реальных задачах
- Сбор данных с парсинга (BeautifulSoup): собрать списки имен и цен отдельно, затем поместить их в DataFrame.
Products = [i.text for i in bs.find_all('name tags')]
Price = [i.text for i in bs.find_all('price tags')]Преобразование колонок при обработке CSV: применить преобразование к каждой строке и собрать новый список значений для Pandas.
Быстрая фильтрация логов: выбрать только строки, соответствующие условию, без явного цикла.
Безопасность и приватность
Списковые включения не изменяют политику безопасности данных сами по себе. Однако будьте внимательны при обработке персональных данных: избегайте ненужной агрегации или записи чувствительной информации в логи внутри выражений.
Короткое резюме
Списковые включения — мощный инструмент для компактной и обычно эффективной трансформации и фильтрации коллекций в Python. Применяйте их для простых, чистых операций. Для сложной логики, побочных эффектов или обработки больших объёмов данных выбирайте традиционные циклы, генераторы или специализированные библиотеки.
Important: помните о читаемости кода и тестах — это важнее одной строки «красивого» comprehension.