Уроки курса
1 Введение в Python и философия дзен
30 мин
2 Переменные и динамическая типизация
30 мин
3 Базовые типы данных: числа, строки и булевы значения
30 мин
4 Изменяемые и неизменяемые объекты (Mutable vs Immutable)
30 мин
5 Форматирование строк и f-строки
30 мин
6 Углубленная работа со списками
30 мин
7 Кортежи и их особенности
30 мин
8 Словари под капотом
30 мин
9 Множества и математические операции
30 мин
10 Генераторы списков (List Comprehensions)
30 мин
11 Генераторы словарей и множеств
30 мин
12 Встроенные функции для коллекций
30 мин
13 Условные операторы и логические выражения
30 мин
14 Циклы while и управление потоком
30 мин
15 Итерация с циклом for
30 мин
16 Конструкции for...else и while...else
30 мин
17 Функции enumerate и zip
30 мин
18 Создание собственных функций (def)
30 мин
19 Позиционные и именованные аргументы
30 мин
20 Проблема изменяемых аргументов по умолчанию
30 мин
21 Произвольное число аргументов (*args и **kwargs)
30 мин
22 Область видимости переменных (LEGB)
30 мин
23 Анонимные функции (lambda)
30 мин
24 Функции высшего порядка
30 мин
25 Замыкания (Closures)
30 мин
26 Введение в объектно-ориентированное программирование
30 мин
27 Атрибуты классов и экземпляров
30 мин
28 Магический метод __init__
30 мин
29 Методы экземпляра
30 мин
30 Инкапсуляция и сокрытие данных
30 мин
31 Декоратор @property
30 мин
32 Наследование классов
30 мин
33 Переопределение методов и функция super()
30 мин
34 Полиморфизм в Python
30 мин
35 Магические методы строк (__str__ и __repr__)
30 мин
36 Обработка исключений (try-except)
30 мин
37 Блоки else и finally
30 мин
38 Генерация собственных исключений (raise)
30 мин
39 Открытие и чтение файлов
30 мин
40 Запись данных в файлы
30 мин
41 Контекстные менеджеры (with)
30 мин
42 Работа с форматом JSON
30 мин
43 Модули и импорты
30 мин
44 Полезные модули стандартной библиотеки
30 мин
45 Модуль datetime
30 мин
46 Модуль collections
30 мин
47 Виртуальные окружения (venv)
30 мин
48 Установка сторонних пакетов через pip
30 мин
49 Организация структуры Python-проекта
30 мин
50 Финальный проект: создание приложения
30 мин

Генераторы списков (List Comprehensions)

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

Прогресс урока: 0%

Добро пожаловать на новый уровень владения Python: Генераторы списков

На уровне Intermediate мы перестаем просто писать код — мы начинаем инженерить решения. Как вы уже знаете из философии Дзена Python (The Zen of Python), «Красивое лучше, чем уродливое» и «Простое лучше, чем сложное». Генераторы списков (List Comprehensions) — это воплощение этой философии в чистом виде. Это инструмент, который позволяет создавать списки лаконично, элегантно и, что самое главное, с высокой производительностью. В отличие от других языков программирования, где для фильтрации или трансформации массивов данных требуется громоздкий процедурный код с объявлением пустых массивов и ручным управлением счетчиками циклов, Python предлагает декларативный подход. Вы описываете что вы хотите получить, а не как шаг за шагом это построить. Генераторы списков заменяют традиционные циклы for в комбинации с вызовом метода .append(). Но почему это так важно? Дело не только в сокращении количества строк кода (хотя это делает код более читаемым и компактным). Дело в том, как интерпретатор CPython обрабатывает эти инструкции под капотом. Когда вы используете цикл for с append(), Python вынужден на каждой итерации искать метод append в пространстве имен объекта списка, вызывать эту функцию, передавать ей аргумент и обрабатывать возвращаемое значение. Это создает значительные накладные расходы на вызовы функций. Генератор списков, напротив, выполняется на уровне байт-кода CPython с использованием специализированных, высокооптимизированных инструкций (таких как LIST_APPEND). Это означает, что генераторы списков работают значительно быстрее, особенно на больших объемах данных. В этом уроке мы пройдем путь от базового синтаксиса до сложных вложенных структур, разберем типичные ошибки новичков, научимся работать с условиями, а также изучим генераторы словарей, множеств и ленивые вычисления. Мы применим методики Microlearning для постепенного усложнения материала и Active Recall для жесткого закрепления каждой микротемы.

python
# Традиционный подход (Procedural approach)
squares = []
for x in range(10):
    squares.append(x**2)

# Pythonic подход (List Comprehension)
squares_pythonic = [x**2 for x in range(10)]

Анатомия базового генератора списков

Давайте разберем синтаксис генератора списков на мельчайшие детали, чтобы понимать каждый символ. Базовая структура выглядит так: [выражение for элемент in итерируемый_объект]. Обратите внимание на квадратные скобки: они не только создают итоговый список, но и служат визуальным сигналом для интерпретатора и программиста о том, что результатом выполнения этой конструкции будет новый объект типа list.
1. Выражение (Expression): Это первая часть конструкции (в нашем примере x**2). Выражение определяет, что именно попадет в новый список. Это может быть сама переменная без изменений (просто x), математическая операция, вызов функции или даже сложный объект (например, словарь или кортеж). Выражение вычисляется один раз для каждого элемента.
2. Цикл (for элемент in итерируемый_объект): Это источник данных. итерируемый_объект может быть списком, строкой, кортежем, диапазоном (range), ключами словаря или любым другим объектом, поддерживающим протокол итерации. Переменная элемент (в примере x) временно принимает значение каждого объекта из источника на текущей итерации. Важно понимать правила области видимости (Scope). В Python 2 переменная цикла из list comprehension «протекала» в глобальную или локальную область видимости функции, что часто приводило к багам (затирая переменные с тем же именем). Однако, начиная с Python 3, генераторы списков имеют свою собственную, изолированную локальную область видимости. Переменная x существует только внутри этих квадратных скобок и будет уничтожена сборщиком мусора сразу после завершения генерации списка. Это делает генераторы списков безопасными и лишенными побочных эффектов (side-effects). Использование генераторов списков — это шаг к функциональному программированию, где функции и выражения не изменяют состояние программы неявно. Вы создаете новый список, оставляя исходный итерируемый объект в неизменном виде (Immutable approach), что крайне важно для надежности архитектуры.

Какую проблему решил Python 3 в отношении генераторов списков по сравнению с Python 2?

Напишите генератор списка, который создает список чисел от 0 до 4 (включительно), не модифицируя элементы. Используйте переменную i и функцию range.

Задание

Проектное задание: Рефакторинг кода. Переведите процедурный код в декларативный.

  • 1. Проанализируйте следующий код: 'names = ["alice", "bob", "charlie"]; upper_names = []; for n in names: upper_names.append(n.upper())'
  • 2. Создайте новую переменную upper_names_comp.
  • 3. Напишите генератор списка, применяющий метод .upper() к каждому элементу исходного списка.
  • 4. Убедитесь, что результат совпадает с исходным циклом.
10 баллов

Генераторы списков и фильтрация: Добавляем условие IF

Создание списков — это отлично, но реальная мощь генераторов списков раскрывается, когда нам нужно отфильтровать данные. Довольно часто возникает задача: пройти по существующей коллекции элементов, отобрать только те, которые удовлетворяют определенному условию, и поместить их в новый список. В классическом процедурном программировании для этого требуется цикл for, внутри которого находится блок if, а внутри него — вызов метода append(). Это создает структуру кода с тремя уровнями вложенности, что ухудшает читаемость. Генераторы списков позволяют сделать это в одну плоскую строку. Синтаксис расширяется и теперь выглядит так: [выражение for элемент in итерируемый_объект if условие].
Важно понимать порядок выполнения: интерпретатор сначала берет элемент из коллекции, затем проверяет условие (блок if), и только если условие возвращает True (или любое истинное значение в контексте логического выражения), выполняется выражение, и результат добавляется в новый список. Блок if здесь играет роль своеобразного охранника (guard clause) или фильтра. Если условие ложно, элемент просто игнорируется и отбрасывается. Это функциональный эквивалент встроенной функции filter() в сочетании с lambda, но, по общепринятому мнению в сообществе Python (и согласно PEP 8), использование генератора списков для фильтрации считается более читабельным и 'питоническим', чем связка filter() и lambda. Кроме того, с точки зрения производительности, генератор списков с условием часто превосходит filter() из-за отсутствия накладных расходов на вызов анонимной функции для каждого элемента. Это особенно заметно на больших массивах данных, с которыми вы часто будете сталкиваться в Data Science или при обработке логов серверов.

python
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Фильтрация четных чисел: классический способ
even_numbers = []
for num in numbers:
    if num % 2 == 0:
        even_numbers.append(num)

# Фильтрация четных чисел: List Comprehension
even_numbers_comp = [num for num in numbers if num % 2 == 0]
print(even_numbers_comp)  # Вывод: [2, 4, 6, 8, 10]

Глубокое погружение: Фильтрация сложных структур данных

На уровне Intermediate мы редко работаем с простыми списками чисел. В реальных проектах, веб-разработке (например, при парсинге JSON-ответов от API) или анализе данных, вы будете иметь дело со списками словарей, кортежами или объектами классов. Генераторы списков идеально подходят для извлечения (экстракции) и фильтрации данных из таких сложных структур. Давайте рассмотрим ситуацию: вы получили из базы данных список словарей, каждый из которых представляет пользователя. Вам нужно получить список имен (строки) только тех пользователей, чей статус 'активен'.
Используя генератор списков, вы комбинируете извлечение значения по ключу (в блоке 'выражение') и проверку другого ключа (в блоке 'условие'). Это позволяет превратить многословный код в элегантный конвейер трансформации данных. Однако здесь кроется потенциальная ловушка для новичков: вы должны быть уверены, что ключи, по которым вы обращаетесь к словарю, действительно существуют, иначе вы получите ошибку KeyError. Чтобы избежать этого, профессиональные разработчики используют метод словаря .get(key, default) прямо внутри генератора списков. Это делает ваш код отказоустойчивым. Например: [user.get('name', 'Unknown') for user in users if user.get('is_active', False)]. В этом выражении мы безопасно извлекаем имя (или подставляем 'Unknown'), только если значение по ключу 'is_active' является истинным. Такой подход не только краток, но и демонстрирует глубокое понимание безопасной работы с данными, что ожидается от разработчика уровня Senior.

Что произойдет, если в блоке IF генератора списков условие для конкретного элемента окажется ложным (False)?

Напишите генератор списка, который отбирает только положительные числа из списка `nums`. Используйте переменную `n`.

Практика: Продвинутая фильтрация списков (Архивная задача 1.1)

Пришло время применить теорию к реальной задаче из нашего архива упражнений для уровня Python Intermediate. Эта задача направлена на тренировку 'Pythonic way' — использования встроенных возможностей языка для краткости и скорости. В реальной работе, например, при разработке e-commerce платформы, вы постоянно будете фильтровать каталоги товаров по различным критериям: цене, наличию, рейтингу и так далее.
Условие задачи: У вас есть список словарей, представляющих товары. Каждый словарь содержит ключи 'name' (имя товара), 'price' (цена) и 'stock' (количество на складе). Используя list comprehension (генератор списков), вам нужно создать новый список, который будет содержать только названия товаров, которые стоят больше 500 рублей и которые есть в наличии (stock больше нуля).
Это классический пример, где генератор списков показывает свою невероятную мощь. Если бы мы писали это через for, нам бы понадобился цикл, сложное условие if с логическим оператором and, и метод append(), извлекающий значение по ключу 'name'. В генераторе списков мы просто указываем выражение p['name'] в самом начале, а в конце ставим составное условие: if p['price'] > 500 and p['stock'] > 0. Важно отметить, что Python использует так называемое 'ленивое вычисление логических выражений' (short-circuit evaluation). Если цена товара равна 400, условие p['price'] > 500 вернет False, и интерпретатор даже не будет проверять вторую часть p['stock'] > 0, экономя такты процессора. Это небольшая, но важная деталь, отличающая Intermediate разработчика от Junior.

python
products = [
    {"name": "Laptop", "price": 1200, "stock": 5},
    {"name": "Mouse", "price": 400, "stock": 10},
    {"name": "Monitor", "price": 800, "stock": 0},
    {"name": "Keyboard", "price": 600, "stock": 2}
]

# Решение с использованием List Comprehension
expensive_in_stock = [p['name'] for p in products if p['price'] > 500 and p['stock'] > 0]

print(expensive_in_stock) 
# Ожидаемый результат: ['Laptop', 'Keyboard']

Задание

Проанализируйте задачу 1.1 и модифицируйте её под новые требования.

  • 1. Возьмите массив products из предыдущего примера.
  • 2. Напишите генератор списков, который извлекает целые словари товаров (а не только имена).
  • 3. Условие фильтрации: цена меньше или равна 800, ИЛИ товар отсутствует на складе (stock == 0).
  • 4. Используйте логический оператор 'or' в блоке if.
10 баллов

Трансформация данных: Условное выражение (Ternary Operator) внутри генератора

До сих пор мы рассматривали блок if только в качестве фильтра в конце генератора списков: элемент либо добавлялся, либо полностью отбрасывался. Но что делать, если нам нужно сохранить ВСЕ элементы исходного массива, но при этом трансформировать некоторые из них на основе определенного условия? Например, мы хотим заменить все отрицательные числа в списке нулями, а положительные оставить как есть. Если мы поместим if в конец конструкции, мы просто потеряем отрицательные числа (отфильтруем их), а не заменим их. Здесь на помощь приходит тернарный оператор Python (Ternary Operator), который используется внутри блока выражения (в самом начале генератора списков).
Тернарный оператор в Python имеет синтаксис: значение_если_истина if условие else значение_если_ложь. В контексте генератора списков конструкция выглядит так: [значение_если_истина if условие else значение_если_ложь for элемент in итерируемый_объект]. Обратите самое пристальное внимание на позицию ключевых слов: полноценная if-else конструкция пишется ДО ключевого слова for! Это одно из самых частых мест для синтаксических ошибок у новичков. Если if стоит после for — это фильтрация. Если if-else стоит до for — это трансформация (условное присваивание). При использовании этой конструкции размер результирующего списка всегда будет в точности равен размеру исходного списка, меняются лишь сами значения. Эта механика крайне важна для операций нормализации данных или очистки датасетов (data cleaning) в Pandas, где мы не можем удалять строки случайным образом, чтобы не сломать индексы датафрейма.

python
raw_temperatures = [22, -4, 18, -10, 25, 30]

# Задача: Заменить все отрицательные температуры (ошибки датчика) на 0.

# НЕПРАВИЛЬНО (Фильтрация - длина списка изменится):
# [t for t in raw_temperatures if t >= 0]

# ПРАВИЛЬНО (Трансформация с if-else ДО for):
cleaned_temps = [t if t >= 0 else 0 for t in raw_temperatures]
print(cleaned_temps)  # Вывод: [22, 0, 18, 0, 25, 30]

Если вам нужно сохранить исходный размер списка, но заменить четные числа на строку 'Чет', а нечетные оставить числами, какую конструкцию вы используете?

Глубокое понимание области применения: Когда НЕ следует использовать List Comprehension

Несмотря на всю элегантность и мощность генераторов списков, истинное мастерство (Intermediate/Senior уровень) заключается в понимании того, когда этот инструмент становится контрпродуктивным. Главный критерий оценки кода в Python — это его читаемость для других разработчиков (PEP 8). Если ваш генератор списков начинает занимать более двух строк на экране или включает в себя сложные вложенные тернарные операторы, это верный знак того, что пора вернуться к классическому циклу for.
Давайте рассмотрим понятие «Вложенный тернарный оператор». Вы можете написать что-то вроде [x if x > 0 else 'zero' if x == 0 else 'negative' for x in numbers]. Синтаксически это абсолютно корректный Python-код. Однако, когнитивная нагрузка при чтении такой конструкции колоссальна. Разработчик, который будет поддерживать ваш код (а возможно, это будете вы сами через полгода), потратит значительное время на расшифровку этой логики. Согласно философии Дзена Python, 'Явное лучше, чем неявное' и 'Читаемость имеет значение'. В таких случаях лучше написать отдельную функцию, которая обрабатывает один элемент, и затем применить её внутри генератора списков: [process_number(x) for x in numbers].
Второе жесткое ограничение — это побочные эффекты (Side Effects). Никогда не используйте генераторы списков просто для запуска функций, которые изменяют состояние системы (например, запись в файл, отправка запроса в сеть, вывод на печать print()). Если выражение генератора не возвращает полезных данных, и вам не нужен результирующий список, использование генератора списков (часто с результатом в виде списка из элементов None) является антипаттерном. Вы создаете бессмысленный объект списка в памяти только ради того, чтобы выполнить цикл. Для побочных эффектов ВСЕГДА используйте стандартный цикл for. Это не только экономит память (O(1) против O(n)), но и четко передает ваши намерения другим программистам.

python
# АНТИПАТТЕРН: Использование генератора для побочных эффектов
urls = ['api/v1/users', 'api/v1/posts']
# Это плохой код. Он создаст список [None, None] в памяти, просто чтобы сделать запросы.
_ = [requests.get(url) for url in urls]

# ПРАВИЛЬНЫЙ ПОДХОД: Использование стандартного цикла for
for url in urls:
    requests.get(url)

# ХОРОШИЙ ПАТТЕРН: Использование генератора, если нужны результаты
responses = [requests.get(url) for url in urls]

Как называется результат функции (или выражение), которое изменяет состояние программы вне своей области видимости, делая генератор списков плохим выбором? (Два слова, по-русски)

Вложенные генераторы списков (Nested List Comprehensions)

До этого момента мы работали с одномерными (плоскими) коллекциями данных. Но в математике, Data Science и машинном обучении (в частности, при работе с NumPy) постоянно встречаются матрицы — списки, содержащие внутри себя другие списки. Python позволяет использовать несколько циклов for внутри одного генератора списков. Это называется вложенными генераторами. Синтаксис выглядит так: [выражение for внешний_элемент in внешняя_коллекция for внутренний_элемент in внутренний_цикл].
Самая большая сложность для новичков при освоении этой темы — понимание порядка следования циклов for. В отличие от вложенных списков, которые мы читаем изнутри наружу, циклы for в генераторе списков записываются в том же порядке, в котором они были бы написаны в виде обычных вложенных циклов с отступами (сверху вниз = слева направо). То есть первый for, который вы пишете, — это самый внешний цикл. Второй for — это цикл, вложенный в первый.
Классическая задача: 'выравнивание' (flattening) двумерной матрицы в одномерный список. У нас есть матрица matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]. Мы хотим получить [1, 2, 3, 4, 5, 6, 7, 8, 9]. Для этого нам нужно сначала пройтись по всем строкам матрицы (внешний цикл), а затем внутри каждой строки пройтись по её элементам (внутренний цикл). Выражение в генераторе: [элемент for строка in матрица for элемент in строка]. Интерпретатор CPython обрабатывает такие вложенные генераторы невероятно эффективно, выстраивая оптимизированный байт-код для двойного итерирования. Это значительно быстрее, чем использование itertools.chain или двойных циклов с append. Однако, если глубина вложенности превышает два цикла (например, трехмерный тензор), от генераторов списков стоит отказаться ради читаемости кода.

python
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Задача: превратить матрицу в плоский список

# Через обычные циклы:
flat_list_loop = []
for row in matrix:          # Внешний цикл
    for num in row:         # Внутренний цикл
        flat_list_loop.append(num)

# Через List Comprehension (обратите внимание на порядок for!)
flat_list_comp = [num for row in matrix for num in row]

print(flat_list_comp)  # Вывод: [1, 2, 3, 4, 5, 6, 7, 8, 9]

Каков правильный порядок написания циклов for во вложенном генераторе списков, если вы переделываете классические вложенные циклы?

Задание

Создание матрицы с помощью вложенных генераторов

  • 1. Задача обратная 'выравниванию'. Создайте матрицу 3x3 (список списков), заполненную нулями.
  • 2. Внешний генератор должен создавать 3 строки (например, for _ in range(3)).
  • 3. Внутренний генератор (выступающий в роли ВЫРАЖЕНИЯ для внешнего) должен создавать список из трех нулей.
  • 4. Ожидаемый результат: [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
10 баллов

Генераторы словарей (Dictionary Comprehensions)

Познав магию генераторов списков, вы легко сможете перенести эти знания на другие структуры данных Python. Словари (Dictionaries) — основа архитектуры Python. Их создание и преобразование — рутинная задача для любого разработчика. В Python существует конструкция Dictionary Comprehension, которая работает абсолютно по тем же принципам, что и List Comprehension, но с небольшими синтаксическими отличиями. Вместо квадратных скобок [] используются фигурные скобки {}. Кроме того, в блоке выражения вам необходимо указать не одно значение, а пару ключ-значение, разделенную двоеточием: {ключ: значение for элемент in итерируемый_объект}.
Генераторы словарей невероятно полезны, когда вам нужно создать словарь из списка ключей и значений, или инвертировать существующий словарь (поменять ключи и значения местами). Часто источником данных для таких генераторов выступает другой словарь, к которому применяется метод .items(), возвращающий пары ключ-значение в виде кортежей. Также вы можете использовать встроенную функцию zip(), чтобы параллельно итерироваться по двум спискам (списку ключей и списку значений) и мгновенно собирать из них словарь. Помните о главном ограничении словарей: ключи должны быть неизменяемыми (Immutable) типами данных, такими как строки, числа или кортежи. Использование генераторов словарей не только сокращает код, но и избавляет от необходимости создавать пустой словарь и многократно вызывать метод .update() или прямое присваивание по ключу в цикле, что снова приводит к ускорению выполнения за счет оптимизации на уровне виртуальной машины CPython.

python
names = ['Alice', 'Bob', 'Charlie']
scores = [85, 92, 78]

# Создание словаря из двух списков с помощью zip и Dict Comprehension
students_scores = {name: score for name, score in zip(names, scores)}
print(students_scores)  # {'Alice': 85, 'Bob': 92, 'Charlie': 78}

# Инвертирование словаря (значения становятся ключами и наоборот)
# Внимание: значения исходного словаря должны быть уникальными и неизменяемыми!
inverted_scores = {score: name for name, score in students_scores.items()}
print(inverted_scores)  # {85: 'Alice', 92: 'Bob', 78: 'Charlie'}

Архивная практика 1.2: Продвинутая работа со словарями и множествами

Обратимся к реальному заданию из нашего архива 'Python Intermediate'. Это классическая алгоритмическая задача, которая часто встречается на собеседованиях. Задача: Дана строка текста. Необходимо создать словарь, где ключом будет символ, а значением — количество его повторений в этой строке. Игнорируйте пробелы и регистр букв (считайте 'A' и 'a' одним и тем же символом).
Эта задача тестирует ваше умение комбинировать методы строк, множества и генераторы словарей. Первым шагом мы нормализуем строку: приводим к нижнему регистру (.lower()) и удаляем пробелы (.replace(' ', '')). Затем нам нужно подсчитать символы. Если мы будем итерироваться по самой строке, мы будем выполнять подсчет для буквы 'l' в слове 'hello' дважды. Это неэффективно. Секретный прием — использовать множество (Set). Обернув нормализованную строку в set(), мы получаем уникальный набор символов строки (устраняем дубликаты). Теперь мы можем использовать генератор словаря! Ключом будет уникальный символ из множества, а значением — вызов метода .count(char) от нашей нормализованной строки. Синтаксис решения: {char: clean_text.count(char) for char in set(clean_text)}.
Стоит отметить (как и указано в архиве для уровня Senior), что для продакшена в модуле collections существует специальный класс Counter, который выполняет эту задачу быстрее (за один проход по строке O(N), в то время как метод с count и set имеет сложность близкую к O(N^2) в худшем случае из-за вызова count для каждого уникального символа). Однако, для тренировки логики создания и работы генераторов словарей, этот пример является одним из лучших педагогических инструментов. Вы учитесь комбинировать структуры данных в единый выразительный 'one-liner' (однострочник).

Почему в задаче подсчета символов рекомендуется итерироваться по set(clean_text), а не по самой строке clean_text?

Генераторы множеств (Set Comprehensions)

Поскольку мы затронули множества в предыдущем примере, давайте рассмотрим, как работают Set Comprehensions. Множество в Python — это структура данных, представляющая собой неупорядоченную коллекцию уникальных (не повторяющихся) хешируемых объектов. В математике это эквивалент понятия 'множество'. Генератор множеств позволяет создать такую коллекцию напрямую, минуя создание списка. Синтаксис генератора множеств практически идентичен генератору списков, но, как и в случае со словарями, он использует фигурные скобки {}. Отличие от генератора словарей в том, что внутри нет двоеточия, разделяющего ключ и значение; есть только одно выражение, как в генераторе списков: {выражение for элемент in итерируемый_объект}.
Главное практическое применение Set Comprehensions — это мгновенная фильтрация дубликатов с одновременной трансформацией данных. Предположим, вы получаете лог-файл с IP-адресами пользователей. Некоторые адреса повторяются много раз. Если вам нужно получить список уникальных подсетей (например, первые три октета IP-адреса), использование генератора списков вернет вам массив с дубликатами, и вам придется дополнительно оборачивать его в set(). Использование Set Comprehension делает это автоматически на лету: как только интерпретатор вычисляет выражение, он проверяет, есть ли такой элемент уже в создаваемом множестве. Если есть, он просто игнорируется, что делает процесс очень эффективным с точки зрения расхода памяти, так как дубликаты даже не добавляются во временные структуры.

python
words = ['apple', 'Banana', 'APPLE', 'cherry', 'banana']

# Задача: получить набор уникальных слов в нижнем регистре.

# Подход через List Comprehension + конвертация (выполняет лишнюю работу)
# list_comp = set([w.lower() for w in words]) 

# Подход через Set Comprehension (Чистый и быстрый код)
unique_words_set = {w.lower() for w in words}

print(unique_words_set)  # Результат: {'cherry', 'apple', 'banana'} (порядок не гарантирован)

Святой Грааль Оптимизации: Выражения-генераторы (Generator Expressions)

Мы подошли к одной из самых важных тем уровня Intermediate, которая напрямую касается управления ресурсами сервера и оптимизации. Допустим, мы читаем файл логов размером 5 гигабайт и хотим найти все строки с ошибками 'ERROR'. Если мы используем генератор списков: [line for line in file if 'ERROR' in line], интерпретатор Python прочитает ВЕСЬ файл, отфильтрует его и создаст новый гигантский список в оперативной памяти (RAM). Вполне вероятно, что ваша программа упадет с ошибкой MemoryError, если серверу не хватит памяти.
Здесь на сцену выходят выражения-генераторы (Generator Expressions). Они выглядят почти в точности как генераторы списков, но вместо квадратных скобок [] они используют круглые скобки (). Разница под капотом колоссальна. Выражение-генератор не создает список в памяти целиком. Вместо этого оно возвращает специальный объект генератора, который вычисляет элементы 'лениво' (lazy evaluation) — строго по одному за раз, только когда его об этом попросят (например, в цикле for или при вызове функции next()). Как только элемент обработан, он удаляется из памяти. Это означает, что потребление памяти выражением-генератором составляет константное значение O(1) и практически равно нулю, независимо от того, обрабатываете вы 10 элементов или 10 миллиардов. Единственный минус генератора — по нему можно пройтись только один раз; после того как он истощится (выбросит StopIteration), вам придется пересоздавать его заново, если данные нужны повторно. Поэтому правило Senior-разработчика гласит: если вам не нужны все данные в памяти одновременно (например, для сортировки или подсчета длины), и вы планируете просто поочередно обработать результаты — ВСЕГДА используйте выражения-генераторы () вместо генераторов списков [].

python
import sys

# Создаем список из миллиона квадратов чисел
list_comp = [x**2 for x in range(1000000)]

# Создаем генератор для миллиона квадратов чисел
gen_expr = (x**2 for x in range(1000000))

# Проверяем потребление оперативной памяти
print(f"List Comprehension size: {sys.getsizeof(list_comp)} bytes")  # ~8 МБ
print(f"Generator Expression size: {sys.getsizeof(gen_expr)} bytes") # ~104 байта!

# Получение данных из генератора
for num in gen_expr:
    print(num) # Напечатает первый квадрат, затем забудет его
    break # Прерываем, чтобы не забить консоль

В чем главное преимущество выражений-генераторов (созданных через круглые скобки) перед генераторами списков?

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

Резюме урока и советы из Code Review (Архивная ситуация)

Давайте подведем итоги нашего глубокого погружения в генераторы коллекций. Вы научились: создавать списки, фильтровать их с помощью if, трансформировать данные с помощью тернарного if-else ДО начала цикла, работать с матрицами через вложенные генераторы, использовать генераторы словарей {} с парами ключ:значение, применять множества для уникализации и, самое главное, оптимизировать потребление памяти с помощью выражений-генераторов (). Это огромный шаг вперед от базовых процедурных скриптов. Ваша архитектура стала более 'питонической'.
В нашем архиве есть раздел 'Code Review Simulation'. Представьте, что Junior-разработчик прислал вам такой код: default_users = [add_user_to_db(name) for name in new_names]. Как Senior, вы должны отклонить этот Pull Request. Почему? Потому что функция add_user_to_db явно совершает операцию ввода-вывода (I/O), вставляет запись в базу данных. Это классический пример 'побочного эффекта' (side effect). В генераторе списков не должно быть I/O операций или изменения глобального состояния. Если вызов к базе данных упадет на третьем имени, что произойдет с вашим списком? Он не достроится. Хуже того, интерпретатор загрузит в оперативную память список ответов от базы данных (возможно, просто массив из True или идентификаторов), который затем будет сразу выброшен сборщиком мусора, так как результат нигде не используется. Вы должны посоветовать Junior'у переписать этот кусок кода на классический цикл for. В Python 'явное лучше, чем неявное'. Используйте List Comprehensions только для чистой генерации и трансформации данных в памяти. Спасибо за внимание на этом масштабном и важном уроке!