Полный гайд по List Comprehensions в Python
Привет! В Python существует множество способов решить одну и ту же задачу, но не все подходы можно считать Pythonic. Это такой код ("питоничный"), который не просто работает, а решает элегантно, кратко и максимально выразительно, используя возможности языка по максимуму.
В этой статье мы досконально разберем один из таких инструментов — List Comprehensions (их еще называют списковыми включениями). Если ты все еще создаешь списки через for и .append(), приготовься к серьезному апгрейду своего инструментария. И рассмотрим мы не только основы, но и продвинутые техники, которые могут быть полезны и опытным специалистам.
Зачем нужны List Comprehensions? Проблема классического цикла
Прежде чем изучать новый инструмент, давай поймем, какую проблему он решает. Представим простую задачу: нам нужен список, содержащий квадраты чисел от 0 до 9.
Если мыслить в рамках базовых конструкций языка, решение "в лоб" будет выглядеть так:
# Классический подход: цикл for и метод .append()
squares = [] # 1. Инициализируем пустой список
for i in range(10):
squares.append(i * i) # 2. Наполняем его в цикле
print(squares)
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]Этот код абсолютно корректен и работает. Но давай посмотрим на него критически:
- Многословность. Нам потребовалось три строки для операции, суть которой можно описать в нескольких словах: "создай список квадратов".
- Логический разрыв. Мы сначала объявляем пустой список
squaresв одном месте, а наполняем его — в другом. В нашем простом примере это не кажется проблемой, но в реальном коде между этими двумя действиями могут оказаться десятки других строк, что ухудшает читаемость. Мы видим список и не сразу понимаем, откуда в нем появятся данные.
Именно эти, на первый взгляд, мелкие неудобства и привели к появлению более декларативного и лаконичного синтаксиса. Вместо того чтобы приказывать Python, как по шагам создавать список, мы хотим просто объявить, каким этот список должен быть.
И здесь на сцену выходят list comprehensions.
Разбираем синтаксис List Comprehension
Давай возьмем наш предыдущий пример с квадратами чисел и перепишем его с использованием list comprehension. Сравни два подхода:
# Классический цикл
squares_loop = []
for i in range(10):
squares_loop.append(i * i)
# List comprehension
squares_comp = [i * i for i in range(10)]
print(squares_loop == squares_comp)
# TrueРезультат идентичен, но решение с comprehension уместилось в одну строку. Оно объединяет идею создания списка и логику его наполнения в единую, неделимую конструкцию.
Базовый синтаксис выглядит так:
Чтобы его было проще читать и запомнить, можно разбить эту конструкцию на три семантических блока:
- expression — Что делаем? Это выражение, которое будет вычислено для каждого элемента и станет частью нового списка. В нашем примере это
i * i. - for item in iterable — Откуда берем элементы? Это обычный цикл
for, который перебирает исходную последовательность (например,range(10)). - [] — Квадратные скобки говорят Python, что результатом всей этой операции должен быть новый список.
Но это еще не все. Часто нам нужно не просто преобразовать элементы, а отфильтровать их. В list comprehensions для этого есть необязательный блок if:
- if condition — При каком условии? Этот блок позволяет включить в результирующий список только те элементы, для которых condition истинно.
Например, создадим список квадратов только для нечетных чисел:
# Нам нужны квадраты только для нечетных чисел от 0 до 9 odd_squares = [i * i for i in range(10) if i % 2 != 0] print(odd_squares) # [1, 9, 25, 49, 81]
Здесь выражение i * i будет вычислено только для тех i, которые прошли проверку if i % 2 != 0.
Эта полная форма — «Что, Откуда, Когда» — и есть вся суть list comprehensions.
Базовые сценарии использования
Теория — это хорошо, но давай рассмотрим несколько типовых задач, где list comprehensions показывают себя во всей красе. Я разделил их на категории, чтобы ты мог распознавать эти паттерны в своем коде.
Фильтрация: отбор элементов по условию
Это классика. У нас есть список, и мы хотим получить новый, содержащий только элементы, которые удовлетворяют определенному критерию.
Задача: Из списка чисел оставить только положительные.
numbers = [10, -5, 0, 8, -1, 3] positive_numbers = [num for num in numbers if num > 0] print(positive_numbers) # [10, 8, 3]
Задача: Из списка строк убрать пустые строки-заглушки.
data = ["user1", "", "user2", " ", "user3"] # Пустая строка и строка с пробелом при булевом преобразовании дадут False # Метод .strip() уберет пробелы по краям non_empty_data = [item.strip() for item in data if item.strip()] print(non_empty_data) # ['user1', 'user2', 'user3']
Обрати внимание на небольшой нюанс в последнем примере. Мы используем item.strip() и в выражении, и в условии. Чуть позже мы разберем, как сделать такой код еще эффективнее.
Трансформация: применение операции к каждому элементу
Здесь мы берем каждый элемент исходной последовательности и как-то его изменяем, создавая новый список.
Задача: Список цен в долларах нужно перевести в другую валюту по заданному курсу.
prices = [12.5, 20, 8.75] exchange_rate = 100 new_prices = [price * exchange_rate for price in prices] print(new_prices) # [1193.75, 1910.0, 835.625]
Задача: Получить список длин всех слов в предложении.
sentence = "The quick brown fox jumps over the lazy dog" word_lengths = [len(word) for word in sentence.split()] print(word_lengths) # [3, 5, 5, 3, 5, 4, 3, 4, 3]
Создание сложных структур
Comprehensions позволяют удобно работать с вложенными циклами.
Задача: Создать "плоский" список (flatten) из списка списков.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# Классический подход
flat_list_loop = []
for row in matrix:
for item in row:
flat_list_loop.append(item)
# Подход с list comprehension
flat_list_comp = [item for row in matrix for item in row]
print(flat_list_comp)
# [1, 2, 3, 4, 5, 6, 7, 8, 9]Обрати внимание: порядок for во вложенном comprehension точно такой же, как и в классическом варианте с циклами. Это упрощает переход от одного синтаксиса к другому.
А почему не sum(matrix, [])? Хотя этот код выглядит короче, он крайне неэффективен. На каждом шаге sum создает новый временный список, что приводит к очень медленной работе на больших данных. List comprehension — это идиоматичный способ для этой задачи.
Вложенные list comprehension — это использование одних list comprehension внутри других, что очень напоминает вложенные циклы. Ниже приведен пример использования вложенного цикла.
Задача: Транспонировать матрицу (поменять строки и столбцы местами).
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
# Внешний цикл `for i in range(3)` перебирает индексы столбцов.
# Внутренний цикл `[row[i] for row in matrix]` строит новый столбец,
# собирая i-й элемент из каждой строки.
print(transposed)
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]Как видишь, эти три паттерна — фильтрация, трансформация и генерация — покрывают огромное число ежедневных задач.
Вопросы производительности: действительно ли comprehensions быстрее?
Споры о производительности — вечная тема в программировании. Принято считать, что list comprehensions работают быстрее, чем эквивалентные циклы for. Давай разберемся, так ли это и почему.
Причина превосходства comprehensions кроется в том, как Python их выполняет. Когда интерпретатор видит list comprehension, он исполняет итерацию в более оптимизированном слое, близком к C-реализации. В обычном цикле for на каждой итерации происходит больше шагов на уровне интерпретатора (например, поиск метода .append у объекта списка и его вызов), что вносит небольшие, но накапливающиеся издержки.
Но лучший способ что-то проверить — провести замер. Для этого мы используем стандартный модуль timeit. Он многократно запускает код, чтобы получить усредненный и более точный результат, исключая случайные флуктуации системы.
Сравним три подхода к созданию списка квадратов миллиона чисел:
- Классический цикл
for. - List comprehension.
- Функция
mapс lambda-функцией (часто рассматривается как альтернатива).
# setup.py - код, который будет выполнен один раз перед замерами
# Здесь мы определяем функции для тестирования
SETUP_CODE = """
def classic_loop(n):
result = []
for i in range(n):
result.append(i*i)
return result
def list_comp(n):
return [i*i for i in range(n)]
def map_lambda(n):
return list(map(lambda i: i*i, range(n)))
"""
# test_cases.py - код, который мы будем замерять
# Мы вызываем функции, определенные в setup, с одним и тем же аргументом
N = 5_000_000
TEST_CODE_LOOP = f"classic_loop({N})"
TEST_CODE_COMP = f"list_comp({N})"
TEST_CODE_MAP = f"map_lambda({N})"
import timeit
# Запускаем замеры. number=10 означает, что каждый тест выполнится 10 раз
# timeit вернет общее время за все запуски
time_loop = timeit.timeit(stmt=TEST_CODE_LOOP, setup=SETUP_CODE, number=10)
time_comp = timeit.timeit(stmt=TEST_CODE_COMP, setup=SETUP_CODE, number=10)
time_map = timeit.timeit(stmt=TEST_CODE_MAP, setup=SETUP_CODE, number=10)
print(f"Классический цикл: {time_loop:.4f} сек")
print(f"List comprehension: {time_comp:.4f} сек")
print(f"map() + lambda: {time_map:.4f} сек")
# Вывод на моей машине (у тебя он может отличаться):
# Классический цикл: 6.1010 сек
# List comprehension: 5.5822 сек
# map() + lambda: 9.1955 сек- List comprehension — явный победитель. Наш тест убедительно показывает, что list comprehension является самым быстрым способом для решения этой задачи, опережая и классический цикл, и
map. - map с lambda — не всегда хорошая идея. Вопреки распространенному мнению, map в паре с lambda-функцией часто оказывается медленнее. Это связано с высокими издержками на вызов Python-функции (lambda) на каждой итерации из C-кода map.
- Читаемость и скорость здесь идут рука об руку. List comprehension в данном случае — это решение, которое одновременно и самое производительное, и одно из самых читаемых.
Главный вывод: Выбирай list comprehension не только ради скорости, но и ради читаемости. В 99% случаев выигрыш в ясности кода важнее, чем несколько сэкономленных миллисекунд. Но приятно знать, что элегантное решение здесь еще и более производительное.
Расширяем горизонты: продвинутые техники
Освоив базу, можно рассмотреть и более сложные конструкции. Но здесь важно помнить золотое правило: краткость не должна вредить читаемости.
Тернарный оператор для условного выражения
Что если нам нужно не просто отфильтровать элементы, а применить к ним разную логику в зависимости от условия? Например, для списка чисел мы хотим получить строку "even" для четных и "odd" для нечетных.
Здесь на помощь приходит тернарный условный оператор value_if_true if condition else value_if_false.
# Классический if-else в цикле
result = []
for i in range(5):
if i % 2 == 0:
result.append("even")
else:
result.append("odd")
# Элегантное решение с тернарным оператором
result_comp = ["even" if i % 2 == 0 else "odd" for i in range(5)]
print(result_comp)
# ['even', 'odd', 'even', 'odd', 'even']Важный синтаксический момент: Обрати внимание, что конструкция if-else пишется до цикла for. Это потому, что она является частью expression (блока "Что делаем?"). Простой фильтрующий if без else пишется после цикла, так как он относится к блоку "При каком условии?". Не путай их.
Каскадные фильтры if
Ты можешь использовать несколько if подряд. Это не аналог if ... and ..., а скорее вложенные if.
Задача: Найти все числа от 0 до 99, которые делятся и на 7, и на 11.
# Несколько if подряд multiples = [i for i in range(100) if i % 7 == 0 if i % 11 == 0] # Это полностью эквивалентно вложенным if: # result = [] # for i in range(100): # if i % 7 == 0: # if i % 11 == 0: # result.append(i) print(multiples) # [0, 77]
Такая запись может быть полезна, но если условий становится больше двух, код рискует превратиться в ребус. В таких случаях лучше использовать and в одном if или вынести логику в отдельную функцию.
Оптимизация вычислений с помощью оператора :=
Вспомни наш пример с очисткой списка строк. Там мы дважды вызывали .strip(). На такой простой операции это не страшно, но если бы вместо .strip() была "тяжелая" функция, мы бы делали двойную работу.
# Неэффективно: "тяжелая" функция func() вызывается дважды # cleaned_data = [func(item) for item in data if func(item)]
В Python 3.8 появился моржовый оператор (:=), который решает эту проблему элегантнее всего. Он позволяет присвоить значение переменной прямо внутри выражения.
Задача: Очистить список строк от пустых значений, вызвав .strip() только один раз.
data = ["user1", "", "user2", " ", "user3"]
# С помощью моржового оператора
cleaned_data = [
cleaned_item
for item in data
if (cleaned_item := item.strip()) # Вызываем .strip() и присваиваем результат
]
print(cleaned_data)
# ['user1', 'user2', 'user3']Здесь item.strip() выполняется только один раз. Его результат присваивается переменной cleaned_item, которая тут же используется в if для проверки на истинность (непустая строка) и затем, если проверка пройдена, попадает в итоговый список. Это идиоматичный, читаемый и производительный способ избежать повторных вычислений.
Неочевидные возможности и приемы
Этот раздел — для самых любознательных. Здесь мы рассмотрим техники, которые расширяют возможности list comprehensions, но использовать их стоит с осторожностью. Главный вопрос, который нужно себе задавать: "Поймет ли мой коллега этот код через полгода?".
Обработка исключений внутри comprehension
Напрямую в синтаксисе list comprehension нет конструкции try-except. Что если мы работаем с "грязными" данными и хотим обработать возможные ошибки, не прерывая цикл?
Решение: вынести логику с try-except в отдельную функцию-обертку.
Задача: Преобразовать список строк в числа, игнорируя некорректные значения.
def to_int_safe(value):
"""Безопасно преобразует значение в int, возвращая None в случае ошибки."""
try:
return int(value)
except (ValueError, TypeError):
return None
raw_data = ["10", "25", "не число", "42", None, "100"]
# Используем нашу безопасную функцию в comprehension
# и сразу же отфильтровываем None
numbers = [num for item in raw_data if (num := to_int_safe(item)) is not None]
print(numbers)
# [10, 25, 42, 100]Мы инкапсулировали логику обработки ошибок в переиспользуемую функцию, а сам comprehension остался ясным и декларативным. Это не идеальное решение, учитывая, что нам нужна вспомогательная функция, но это лучшее, что мы можем сделать, поскольку предложение (PEP 463), в котором пытались ввести соответствующий синтаксис, было отклонено.
Синергия с itertools
List comprehensions бывает удобно использовать в паре с модулем itertools. Эта библиотека предоставляет высокопроизводительные строительные блоки для сложных итераций, а comprehensions позволяют собирать из них результат.
Прием 1: Имитация break для прерывания итерации
В list comprehension нет аналога оператора break. Но мы можем добиться того же эффекта с помощью itertools.takewhile.
Задача: Взять все числа из списка до первого отрицательного.
from itertools import takewhile data = [2, 8, 10, 4, -5, 12, 7] # takewhile создает итератор, который возвращает элементы # до тех пор, пока предикат (lambda-функция) истинен. result = [x for x in takewhile(lambda n: n > 0, data)] print(result) # [2, 8, 10, 4]
Использование takewhile — это самый "питоничный" способ решить такую задачу. Он явно говорит о нашем намерении: "брать, пока условие выполняется".
Прием 2: Работа с соседними элементами
Задача: "Отменить" накопительную сумму, то есть найти разницу между соседними элементами в списке.
from itertools import pairwise # Доступен в Python 3.10+ cumulative_sums = [100, 104, 109, 121, 129, 130, 140] # pairwise(iterable) возвращает итератор по парам соседних элементов: # (100, 104), (104, 109), (109, 121) и т.д. original_values = [y - x for x, y in pairwise(cumulative_sums)] print(original_values) # [4, 5, 12, 8, 1, 10]
Эти примеры показывают, что list comprehension — это не просто синтаксический сахар, а гибкий инструмент, который в правильных руках способен на многое.
За гранью списков: краткий взгляд на dict и set comprehensions
Мы досконально разобрали всё, что касается list comprehensions. Однако этот подход не ограничивается одними лишь списками.
В Python существуют абсолютно аналогичные конструкции для создания словарей (dict) и множеств (set).
- Set Comprehensions используют фигурные скобки
{}и позволяют создавать множества — коллекции уникальных элементов. Это удобно, когда нужно получить уникальные значения из другой коллекции. - Dict Comprehensions также используют фигурные скобки
{}, но с паройключ: значение. Они позволяют конструировать словари из других данных, например, "переворачивать" существующий словарь или создавать его из списка кортежей.
Мы не будем углубляться в них здесь, так как каждая из этих тем заслуживает собственного исчерпывающего гайда. Просто запомни: освоив list comprehensions, ты уже на 90% освоил и их "собратьев" для словарей и множеств.
Хочешь копнуть глубже? Этим темам посвящены наши отдельные подробные руководства:
Заключение: принцип разумной достаточности
Мы прошли путь: от проблемы классического цикла for до продвинутых техник с itertools и моржовым оператором. Теперь ты знаешь, что list comprehensions — это не просто "синтаксический сахар", а полезный производительный инструмент в арсенале Python-разработчика. Они позволяют писать код, который ближе к описанию сути задачи, а не к пошаговой инструкции для машины.
Но, овладев этой силой, важно научиться применять ее с умом. И если бы из всей этой статьи я попросил тебя запомнить только одну вещь, это была бы она:
Прежде чем написать сложный list comprehension с тройной вложенностью и каскадом if, остановись на секунду и задай себе вопрос: "Будет ли этот код понятен мне самому через три месяца? А моему коллеге, который увидит его впервые?".
Если ответ "нет" или "не уверен", смело разбивай эту конструкцию на несколько строк или возвращайся к старому доброму циклу for. Читаемый и поддерживаемый код всегда ценнее, чем самая изощренная однострочная магия.
P.S. Если статья сэкономила тебе время или научила чему-то новому, лучшей благодарностью будет поддержка проекта. Это позволяет мне и дальше создавать качественный и бесплатный контент для всех. Спасибо, что дочитал до конца!