Списочные включения
Представьте ситуацию: вам нужно создать список квадратов всех чисел от 1 до 10. Как бы вы это сделали? Скорее всего, написали бы что-то вроде этого:
Этот код работает отлично и выдаёт нужный результат: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]. Но давайте посмотрим на него критически. Нам понадобилось три строки кода, мы создали пустой список, запустили цикл, и в каждой итерации добавляли новый элемент. Это классический подход, который изучают все начинающие программисты.
Однако у этого подхода есть несколько существенных недостатков, которые становятся заметными при работе с большими объёмами данных. Во-первых, метод append() не самый эффективный с точки зрения производительности. При каждом вызове Python может потребоваться перераспределить память для списка, если его текущий размер исчерпан. Это означает создание нового блока памяти большего размера и копирование туда всех существующих элементов – операция, которая повторяется многократно в процессе работы цикла.
Во-вторых, такой код требует больше места и менее читаем. Логика разбита на несколько строк, и чтобы понять, что делает этот фрагмент, нужно прочитать все три строки и мысленно собрать картину воедино. В простых примерах это не проблема, но в реальных программах, где таких конструкций десятки, читаемость кода серьёзно страдает.
Теперь усложним задачу. Допустим, вам нужно получить только чётные квадраты из того же диапазона. Код становится ещё длиннее:
Уже четыре-пять строк для такой простой операции! Здесь недостатки традиционного подхода проявляются ещё ярче. Мы вводим дополнительную переменную square, создаём вложенную структуру с условием, и при этом список всё так же наполняется поэлементно через append(). Каждый вызов append() – это отдельная операция, которая проверяет, достаточно ли места в списке, и при необходимости выделяет новую память.
С точки зрения быстродействия разница может показаться незначительной на маленьких списках из 10 элементов, но представьте, что вы обрабатываете тысячи или миллионы элементов. Накладные расходы на многократные вызовы append() и перераспределение памяти складываются в ощутимую задержку.
Кроме того, есть проблема с читаемостью и вероятностью ошибок. В длинном коде легко забыть инициализировать пустой список, случайно использовать неправильное имя переменной или допустить ошибку в отступах. Чем больше строк кода, тем больше мест, где можно ошибиться.
Особенно остро эта проблема стоит на экзаменах, в том числе на ЕГЭ по информатике. Когда у вас ограничено время, каждая минута на счету. Вам нужно не только правильно решить задачу, но и сделать это быстро, оставив время на проверку и другие задания. Написание трёх-четырёх строк кода вместо одной – это не просто лишние символы, это дополнительное время на написание, отладку и проверку.
Именно здесь в игру вступают списочные включения (list comprehensions) – один из самых удобных и полезных инструментов Python. Они позволяют создавать новые списки на основе существующих последовательностей всего в одну строку кода. То, что раньше занимало три-четыре строки с циклами и условиями, теперь можно записать компактно и наглядно.
Списочные включения работают быстрее, потому что Python оптимизирует их на уровне интерпретатора. Размер итогового списка часто может быть предсказан заранее, что позволяет выделить нужный объём памяти сразу, без множественных перераспределений. Кроме того, списочные включения используют более эффективные внутренние механизмы Python, что даёт выигрыш в скорости.
В заданиях ЕГЭ по информатике часто встречаются задания на обработку последовательностей: фильтрация данных по условию, преобразование элементов, создание новых структур на основе существующих. Все эти операции значительно упрощаются с использованием списочных включений. Более того, такой код легче проверять – когда логика умещается в одну строку, проще охватить её взглядом и убедиться в правильности.
Далее в статье мы подробно разберём, как работают списочные включения, изучим их синтаксис и научимся применять для решения различных задач. Вы узнаете, как создавать вложенные списочные включения для работы с многомерными структурами, как применять их к словарям и множествам. К концу этой статьи вы будете уверенно использовать этот инструмент и значительно ускорите решение задач по программированию.
Синтаксис списочных включений
Списочное включение – это компактный способ создания нового списка путём применения выражения к каждому элементу итерируемого объекта (например, другого списка, кортежа или результата функции range()). Общий синтаксис выглядит так:
Давайте разберём каждую часть этой конструкции:
- Квадратные скобки [] – обозначают, что мы создаём список
- Выражение – это то, что будет применено к каждому элементу (может быть операция, вызов функции или просто сам элемент)
- for элемент in последовательность – цикл, который перебирает все элементы из исходной последовательности
- Элемент – переменная, которая последовательно принимает значения из последовательности
Вернёмся к нашей первой задаче – создать список квадратов чисел от 1 до 10. Вот как это выглядит со списочным включением:
Всего одна строка вместо трёх! Давайте прочитаем этот код как предложение на русском языке: «Создай список, где каждый элемент – это квадрат числа i, для всех i от 1 до 10». Именно так и работает наш мозг, когда мы формулируем задачу, и списочное включение позволяет записать её на Python почти таким же естественным образом.
Вот ещё несколько простых примеров для закрепления:
Списочные включения с условием
Теперь усложним задачу и добавим фильтрацию. Иногда нам нужно не просто преобразовать все элементы, но и отобрать только те, которые удовлетворяют определённому условию. Для этого в списочном включении используется блок if:
Условие записывается в конце конструкции после if. В новый список попадут только те элементы, для которых условие истинно (возвращает True).
Вспомним нашу задачу про чётные квадраты. С традиционным подходом код выглядел так:
А вот как элегантно это решается со списочным включением:
Код стал не только короче, но и понятнее: «Создай список квадратов чисел i от 1 до 10, но только если квадрат чётный». Вся логика видна сразу, не нужно отслеживать создание пустого списка и многократные вызовы append().
Можно сделать код ещё более читаемым, если условие применить к самому числу, а не к его квадрату. Например, мы знаем, что квадрат чётного числа всегда чётен:
Здесь мы берём только чётные числа (2, 4, 6, 8, 10) и возводим их в квадрат. Результат тот же, но код проще.
Вот ещё примеры с условиями:
Сравнение производительности
Мы уже говорили, что списочные включения работают быстрее традиционных циклов. Давайте проверим это на практике с помощью модуля timeit, который позволяет точно измерить время выполнения кода.
Напишем функции, в которых будем тестировать производительность двух подходов: с использованием метода append() и списочных включений. Будем создавать список, в который поместим квадраты чётных чисел от 0 до 1 000. Каждую функцию запустим по 10 000 раз для получения более точного результата.
На большинстве компьютеров вы увидите, что списочное включение работает примерно в 1,2-1,5 раза быстрее. Разница может показаться небольшой, но, когда вы обрабатываете миллионы элементов или выполняете такие операции многократно, экономия времени становится существенной.
Почему списочные включения быстрее?
Причина в том, что Python оптимизирует списочные включения на низком уровне. Когда интерпретатор видит списочное включение, он:
- Предсказывает размер списка (в некоторых случаях), что позволяет выделить нужный объём памяти сразу
- Использует более эффективные внутренние операции вместо вызова метода append() в каждой итерации
- Минимизирует накладные расходы на работу с переменными и вызовы функций
Кроме того, списочные включения более читаемы и компактны, что снижает вероятность ошибок и ускоряет написание кода – критически важное преимущество на экзаменах.
Вложенные списочные включения
До этого момента мы работали с простыми одномерными списками – последовательностями элементов, которые можно перебрать одним циклом. Однако в реальных задачах, особенно в заданиях ЕГЭ, часто приходится работать с более сложными структурами: матрицами, таблицами, вложенными списками. Для обработки таких данных нужны вложенные циклы, и списочные включения отлично справляются и с этой задачей.
Представим простую задачу: создать таблицу умножения для чисел от 1 до 3. Традиционный подход с вложенными циклами выглядит так:
Этот код работает, но занимает шесть строк и требует создания двух пустых списков – внешнего и внутреннего. Логика размазана по нескольким уровням вложенности, и чтобы понять, что происходит, нужно мысленно отследить работу обоих циклов.
Ещё один распространённый случай – «развёртывание» двумерной структуры в одномерную. Например, у нас есть список списков, и мы хотим получить все элементы в одном плоском списке:
Снова четыре строки для такой простой операции, плюс необходимость отслеживать два уровня вложенности циклов.
Вложенные списочные включения позволяют записать вложенные циклы в одну строку. Общий синтаксис выглядит так:
Важно понимать порядок циклов. В списочном включении циклы записываются в том же порядке, что и в традиционном коде: сначала внешний цикл, затем внутренний.
Сравним синтаксис классических вложенных циклов и вложенных списочных включений (код из первого примера):
А с использованием вложенных списочных включений его можно сократить до одной строки:
Давайте прочитаем это выражение изнутри наружу:
- i * j – выражение, которое вычисляется для каждой пары (i, j)
- for j in range(1, 4) – внутренний цикл, создающий элементы одной строки
- [i * j for j in range(1, 4)] – это создаёт один список (одну строку таблицы)
- for i in range(1, 4) – внешний цикл, создающий строки
- [[…] for i in range(1, 4)] – создаёт список строк таблицы (список списков)
Теперь проверим работу вложенных списочных включений на обратной операции: превратим двумерный список в одномерный:
Обратите внимание: здесь у нас один список (одинарные квадратные скобки), но два цикла for. Мы не создаём список списков, а собираем все элементы в один плоский список.
И классический пример: транспонирование матрицы. Транспонирование – это операция, при которой строки и столбцы матрицы меняются местами.
Рассмотрим сначала подход через вложенные циклы:
Со списочным включением это выглядит намного лаконичнее:
Списочные включения со словарями
Представьте задачу: у вас есть словарь с результатами экзамена по разным предметам, и нужно создать новый словарь, содержащий только те предметы, по которым ученик набрал больше 75 баллов, при этом увеличив каждый балл на 5 (допустим, за особые достижения).
Представьте задачу: у вас есть словарь с результатами экзамена по разным предметам, и нужно создать новый словарь, содержащий только те предметы, по которым ученик набрал больше 75 баллов, при этом увеличив каждый балл на 5 (допустим, за особые достижения).
Четыре строки кода для относительно простой операции! Мы создаём пустой словарь, перебираем все пары ключ-значение, проверяем условие и добавляем отфильтрованные данные с преобразованием.
Или возьмём другую частую задачу: у нас есть два списка – один с названиями фруктов, другой с их ценами, и нужно создать из них словарь. Классический подход:
Снова несколько строк, работа с индексами, создание пустого словаря. Как и в случае со списками, этот подход работает, но требует больше времени на написание и больше внимания при проверке – легко ошибиться с индексами или забыть инициализировать пустой словарь.
Наверное, вы уже нашли сходство этих ситуаций с теми, что мы описывали в самом начале. Да, механизм списочных включений можем применяться и на другие структуры данных, например, на словари. Называться такие включения уже будут «словарными» (dictionary comprehensions).
Словарные включения очень похожи на списочные, но вместо квадратных скобок используются фигурные, а вместо одного выражения указываются ключ и значение через двоеточие:
Как и в списочных включениях, можно добавить условие для фильтрации:
При работе с существующим словарём обычно используется метод items(), который возвращает пары (ключ, значение):
Давайте вернёмся к нашей первой задаче и решим её с помощью словарного включения:
Одна строка вместо четырёх! Код читается как естественное предложение: «Создай словарь, где для каждого предмета балл увеличен на 5, но только если исходный балл больше 75».
А теперь перейдём ко второму примеру, с ценами на фрукты. Для создания словаря из двух списков удобно использовать функцию zip(), которая «склеивает» два списка в пары:
Функция zip() берёт первый элемент из первого списка и первый элемент из второго списка, создавая пару (‘яблоки’, 100), затем вторые элементы (‘бананы’, 80) и так далее. Словарное включение превращает эти пары в словарь.
Сравните с традиционным подходом – вместо четырёх строк с работой с индексами у нас одна понятная строка. В использовании словарные включения аналогичны списочным: можно применять функции и методы к элементам, использовать условия, тернарные операторы и создавать вложенные словарные включения.
Списочные включения с множествами
Давайте снова представим задачу: у вас есть список оценок учеников класса, и нужно найти все уникальные оценки, которые встречаются в списке, причём интересуют только оценки выше тройки. Традиционный подход с циклом выглядит так:
Четыре строки кода для простой операции. Мы создаём пустое множество, перебираем все оценки, проверяем условие и добавляем элементы методом add(). Как и с пустыми списками и словарями, это работает, но требует больше кода и времени.
Уже догадываетесь к чему мы идём?
Да, механизм списочных включений может так же применяться и к множествам. И, по аналогии со словарными включениями, они называются множественными включениями (set comprehensions).
Множественные включения по синтаксису почти идентичны списочным, но вместо квадратных скобок используются фигурные:
Главное не спутать их со словарными: в словарных включениях есть двоеточие между ключом и значением ({ключ: значение}), а в множественных – только одно выражение ({выражение}).
Давайте решим первую задачу с помощью множественного включения:
Как вы уже могли понять, работают множественные включения аналогично списочным и словарным. Для примера давайте рассмотрим несколько ситуаций их применения.
Найдём все делители числа 24:
Извлечём все уникальные цифры из строки:
Найдём все возможные остатки от деления чисел от 1 до 20 на 7:
Итоги
Мы научились создавать новые структуры данных, применяя выражения и фильтры к существующим последовательностям, что делает программы компактнее и понятнее. Включения для списков, словарей и множеств позволяют заменить громоздкие циклы одной строкой кода, который работает быстрее и содержит меньше возможностей для ошибок.
На ЕГЭ по информатике эти навыки дают реальное преимущество: вместо пяти-семи строк с циклами вы пишете одну, экономя время на написание и проверку кода. Компактная запись позволяет быстрее находить ошибки и легче держать всю логику решения в голове.
Особенно полезными списочные включения окажутся для нас при решении 9 заданий ЕГЭ по информатике программным способом. В следующей статье мы разберём несколько примеров на каждый тип этого задания и покажем, как применять изученные в этой статье техники для быстрого и надёжного получения ответов.