Ключевые вопросы параграфа
- Что такое списочные выражения и зачем они нужны?
- Чем генераторы отличаются от списков?
- Как добавить условия и вложенные циклы в списочные выражения?
- Что такое изменяемые и неизменяемые типы в Python?
- Как Python хранит переменные в памяти и почему важно понимать модель хранения?
Что такое списочные выражения и зачем они нужны
В Python, помимо стандартных конструкций — цикла for и встроенных функций вроде append()
или range()
, — есть более лаконичный способ создавать списки. Это списочные выражения (англ. list comprehensions).
Напомним: в предыдущих параграфах вы уже научились перебирать элементы в цикле
for
, добавлять значения в список с помощью метода .append()
, использоватьrange()
для генерации чисел и преобразовывать строки в числа с помощьюint()
. Теперь всё это можно объединить в одну строку.
Пример: ввод чисел и создание списка
Допустим, нужно получить список из пяти целых чисел, введённых пользователем.
Обычный способ:
1numbers = []
2for i in range(5):
3 numbers.append(int(input()))
4print(numbers)
Теперь та же программа, но с использованием списочного выражения:
1numbers = [int(input()) for i in range(5)]
2print(numbers)
Здесь цикл for
встроен внутрь выражения — и на каждой итерации значение добавляется прямо в список. Такой подход делает код более читаемым и лаконичным, особенно когда нужно получить список преобразованных значений.
Чем генераторы отличаются от списков и в каких задачах полезнее
Списочные выражения позволяют быстро и лаконично создавать списки. Это удобно, если вам сразу нужен весь результат — например, список чисел, строк или объектов.
Но в Python есть ещё один способ перебора значений — генераторы
. Внешне они похожи на списочные выражения, но работают иначе: список создаёт все элементы сразу и хранит их в памяти, а генератор формирует значения по одному, «по запросу». Это экономит ресурсы и полезно при работе с большими объёмами данных.
Рассмотрим пример. Вы получаете от пользователя пять чисел и хотите оставить только те, которые больше среднего арифметического:
1numbers = [int(input()) for i in range(5)]
2avg = sum(numbers) // len(numbers)
3numbers = [element for element in numbers if element > avg]
4print(numbers)
Здесь работает та же логика цикла: мы проходим по всем элементам numbers, но добавляем в новый список только те, которые больше среднего. Такое выражение можно прочитать как: «добавь элемент в новый список, если он больше среднего».
Такой способ удобен, когда нужно получить итоговый список. Но, если вам нужно просто перебирать значения — например, передавать их по одному в цикл, — можно использовать генератор:
1numbers = (int(input()) for i in range(5))
Это выражение возвращает генератор-объект, который выдаёт значения по одному — не загружая в память весь список сразу.
Важно: не жертвуйте производительностью
На первый взгляд, можно упростить код, сразу подставив sum(numbers) // len(numbers)
в условие:
1numbers = [int(input()) for i in range(5)]
2numbers = [element for element in numbers if element > sum(numbers) // len(numbers)]
3print(numbers)
Но здесь кроется ошибка производительности: sum(numbers)
будет пересчитываться заново при каждой итерации
. Это значит, что для списка из 5 элементов функция sum()
вызовется 5 раз, а при 1000 элементах — 1000 раз.
Вместо этого лучше один раз вычислить среднее и сохранить в переменной, как показано в первом примере. Это не только быстрее, но и делает код проще для понимания.
Как добавить условия и вложенные циклы в списочные выражения
Списочные выражения позволяют не только фильтровать значения, но и включать вложенные циклы. Это особенно полезно при работе с матрицами, строками, словарями и другими составными структурами данных.
Ввод матрицы с клавиатуры
Допустим, нужно считать пять строк чисел, где каждое число в строке разделено пробелом:
1matrix = [[int(x) for x in input().split()] for i in range(5)]
2print(matrix)
Пример ввода:
1 2 3 4 5 2 3 4 5 6 3 4 5 6 7 4 5 6 7 8 5 6 7 8 9
Вывод программы:
[[1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8], [5, 6, 7, 8, 9]]
Такое выражение можно прочитать как:
«Сделай 5 итераций. На каждой итерации считай строку, разбей её на элементы, преврати каждый в число и добавь полученный список в матрицу».
Ошибка: копирование одной и той же строки
Попробуем создать двумерный список из нулей:
1zeros = [[0] * 5] * 5
2print(zeros)
3
4# Вывод программы:
5# [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
На первый взгляд, всё работает. Попробуем изменить первый элемент первого списка в списке.
1zeros[0][0] = 1
2print(zeros)
3
4# Вывод программы:
5# [[1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0]]
Результат удивляет — изменилось не только одно значение, а весь первый столбец.
Дело в том, что создаётся пять ссылок на один и тот же внутренний список.
Правильный способ: новый список на каждой итерации
Решение — использовать списочное выражение:
1zeros = [[0] * 5 for i in range(5)]
2print(zeros)
3
4# Вывод программы:
5# [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
Теперь каждый внутренний список создаётся заново, и изменения не распространяются:
1zeros[0][0] = 1
2print(zeros)
3
4# Вывод программы:
5# [[1, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
Преобразование строк в коды символов
Списочные выражения можно использовать не только с числами, но и с текстами:
1text = "Строка символов"
2codes = [ord(symbol) for symbol in text]
3print(codes)
Результат — список кодов каждого символа.
1# Вывод программы:
2# [1057, 1090, 1088, 1086, 1082, 1072, 32, 1089, 1080, 1084, 1074, 1086, 1083, 1086, 1074]
Фильтрация словаря
Списочные выражения работают и с парами ключ — значение
:
1countries = {
2 "Россия": ["русский"],
3 "Беларусь": ["белорусский", "русский"],
4 "Бельгия": ["немецкий", "французский", "нидерландский"],
5 "Вьетнам": ["вьетнамский"]
6}
7
8multiple_lang = [country for (country, lang) in countries.items() if len(lang) > 1]
9
10print(multiple_lang)
11
12# Вывод программы:
13# ['Беларусь', 'Бельгия']
Создание словаря из списка пар
С помощью словарного включения можно собрать dict
из списка:
1countries = {country: capital for country, capital in [
2 ("Россия", "Москва"),
3 ("Беларусь", "Минск"),
4 ("Сербия", "Белград")
5]}
6print(countries)
7
8# Вывод программы:
9# {'Россия': 'Москва', 'Беларусь': 'Минск', 'Сербия': 'Белград'}
Генераторы: альтернатива спискам
В Python генераторы позволяют создавать последовательности значений «на лету» — без хранения всех элементов в памяти. Они похожи на списочные выражения, но вместо квадратных скобок используют круглые:
1numbers = (int(input()) for i in range(5))
2print(numbers)
Этот код не выведет числа, а покажет объект генератора:
1# Вывод программы:
2# <generator object <genexpr> at 0x00000266CEA0CAC0>
Чтобы получить значения, нужно проитерироваться по генератору — например, с помощью for
или передав в list()
.
Генераторы и память
Главное преимущество генераторов — экономия памяти. Они не создают всю коллекцию сразу, а вычисляют значения по одному:
1numbers_iter = (i for i in range(10 ** 6)) # генератор
2print(f"Итератор занимает {getsizeof(numbers_iter)} байт.")
3numbers_list = list(range(10 ** 6)) # список
4print(f"Список занимает {getsizeof(numbers_list)} байт.")
Важно: переменная numbers_iter — это генератор, который реализует протокол итератора. Он сам по себе занимает очень мало места — в данном случае всего 112 байт, независимо от потенциального количества элементов.
1# Вывод программы:
2# Итератор занимает 112 байт.
3# Список занимает 8000056 байт
Генераторы и производительность
Генератор может быть быстрее
, особенно при работе с большим объёмом данных:
1from timeit import timeit
2
3print(round(timeit("s = '; '.join(str(x) for x in range(10 ** 7))", number=10), 3))
4
5print(round(timeit("s = '; '.join([str(x) for x in list(range(10 ** 7))])", number=10), 3))
Первая строка использует генератор, вторая — список. Разница ощутима:
1# Вывод программы (зависит от ресурсов системы):
2# 22.914
3# 25.576
Из результата видно, что итератор работает быстрее, чем список, при условии обработки большого количества элементов.
Что такое изменяемые и неизменяемые типы в Python
В Python все переменные ссылаются на объекты в памяти. При этом объекты делятся на изменяемые
и неизменяемые
. Это различие важно при работе с коллекциями, операциями присваивания и передачей аргументов в функции.
Чтобы понять, чем отличаются эти типы, рассмотрим, как Python работает с переменными на уровне хранения данных.
Как работает присваивание
Когда вы создаёте переменную, Python сохраняет объект в памяти и присваивает переменной ссылку
на этот объект. У каждого объекта есть идентификатор
— уникальный номер, который можно узнать с помощью функции id()
.
Посмотрим, что произойдёт при изменении значения переменной:
1x = 5
2print(id(x))
3x = 10
4print(id(x))
5
6# Вывод программы (значения будут меняться от запуска к запуску программы):
7# 2198530255152
8# 2198530255184
Идентификатор изменился. Это значит, что Python создал новый объект в памяти, а переменная x теперь ссылается на него.
Как Python хранит переменные в памяти и почему важно понимать модель хранения
Когда вы создаёте переменную в Python, в ней хранится не само значение, а лишь ссылка на область памяти, где это значение размещено. Это ключевая особенность языка, которая напрямую влияет на поведение кода, особенно при работе с коллекциями и изменяемыми объектами.
Переменные — это ссылки
Если вы присваиваете значение одной переменной в другую, обе переменные будут указывать на один и тот же объект. Убедимся в этом:
1x = 1
2y = x
3print(id(x))
4print(id(y))
5print(x is y)
6
7# Примерный вывод:
8# 2109716588848
9# 2109716588848
10# True
Оператор is
показывает, что x
и y
— один и тот же
объект в памяти. Они не просто равны по значению (==
), но и указывают на один идентификатор объекта (is
).
Равны, но не одинаковы
Однако равенство значений ещё не означает, что переменные ссылаются на один и тот же объект:
1x = [el ** 2 for el in range(5)]
2y = [el ** 2 for el in range(5)]
3print(x == y)
4print(x is y)
5
6# Результат:
7# True
8# False
Значения одинаковые, но x
и y
— разные списки в памяти, у них разные идентификаторы.
Изменяемые и неизменяемые типы
- Неизменяемые типы:
int
,float
,str
,tuple
,frozenset
. - Изменяемые типы:
list
,set
,dict
.
У неизменяемых объектов при попытке изменить значение создаётся новый объект с новым идентификатором
:
1x = 5
2print(id(x))
3x = 10
4print(id(x))
У изменяемых объектов — наоборот, можно изменить значение, не меняя идентификатор
:
1numbers = [1, 2, 3]
2print(f"{numbers}, id = {id(numbers)}")
3numbers += [4]
4print(f"{numbers}, id = {id(numbers)}")
5
6# Результат:
7# [1, 2, 3], id = 2095932585856
8# [1, 2, 3, 4], id = 2095932585856
Но если используется операция присваивания, даже изменяемый объект будет заменён на новый:
1numbers = [1, 2, 3]
2print(f"{numbers}, id = {id(numbers)}")
3numbers = numbers + [4]
4print(f"{numbers}, id = {id(numbers)}")
5
6# Результат:
7# [1, 2, 3], id = 1438332303232
8# [1, 2, 3, 4], id = 1438341470336
Осторожно: ссылки на один и тот же объект
Если две переменные указывают на один и тот же изменяемый объект, то изменение одной повлияет и на вторую:
1x = [1, 2, 3]
2y = x
3x[0] = 0
4print(x)
5print(y)
6
7# Результат:
8# [0, 2, 3]
9# [0, 2, 3]
Здесь x и y — один и тот же список в памяти. Мы изменили x, и это автоматически изменило y.
Поверхностная и глубокая копия
Чтобы создать отдельный объект, можно использовать срез:
1x = [1, 2, 3]
2y = x[:]
3x[0] = 0
4print(x)
5print(y)
6
7# Результат:
8# [0, 2, 3]
9# [1, 2, 3]
x и y теперь разные объекты — изменение одного не затрагивает другой.
Но если список вложенный, срез копирует только внешний список:
1numbers = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
2numbers_copy = numbers[:]
3print([numbers_copy[i] is numbers[i] for i in range(len(numbers))])
4
5# Результат:
6# [True, True, True]
Внутренние списки остались одними и теми же объектами.
Как создать настоящую копию вложенного списка
Чтобы скопировать всё содержимое, можно использовать списочное выражение:
1numbers_copy = [elem[:] for elem in numbers]
Проверим:
1print([numbers_copy[i] is numbers[i] for i in range(len(numbers))])
2
3# Результат:
4# [False, False, False]
Теперь это полноценная независимая копия.
Альтернатива — использовать функцию deepcopy из модуля copy:
1from copy import deepcopy
2numbers_copy = deepcopy(numbers)
Почему это важно
- При работе с коллекциями и функциями вы должны понимать, где копия, а где ссылка.
- Ошибки с мутацией общего объекта — одни из самых частых в коде на Python.
- Модель ссылок помогает писать предсказуемый и устойчивый код, особенно при передаче параметров и работе с вложенными структурами.
✅ У вас получилось разобраться со списочными выражениями?
Что дальше
Теперь вы умеете компактно создавать и фильтровать списки с помощью списочных выражений, добавлять условия и вложенные циклы, отличать списки от генераторов и понимать, как работает память в Python. Вы узнали, как устроена модель хранения переменных и чем опасны лишние ссылки на одни и те же изменяемые объекты.
В следующем параграфе мы сделаем ещё один шаг вперёд: познакомимся со встроенными возможностями Python для работы с коллекциями. Вы увидите, как с помощью функций из стандартной библиотеки itertools
, а также enumerate()
и zip()
можно перебирать, комбинировать, накапливать и повторять элементы — причём без лишнего кода и с минимальной нагрузкой на память.
А пока вы не ушли дальше — закрепите материал на практике:
- Отметьте, что урок прочитан, при помощи кнопки ниже.
- Пройдите мини-квиз, чтобы проверить, насколько хорошо вы усвоили тему.
- Перейдите к задачам этого параграфа и потренируйтесь.
- Перед этим загляните в короткий гайд о том, как работает система проверки.
Хотите обсудить, задать вопрос или не понимаете, почему код не работает? Мы всё предусмотрели — вступайте в сообщество Хендбука! Там студенты помогают друг другу разобраться.
Ключевые выводы параграфа
- Списочные выражения позволяют компактно создавать списки, объединяя цикл for и условие в одной строке.
- Внутри списочных выражений можно использовать условия и вложенные циклы, включая преобразование вложенных структур (например, матриц).
- Генераторы создаются с помощью круглых скобок и возвращают значения по одному — это экономит память при работе с большими объёмами данных.
- В Python переменные хранят ссылку на объект в памяти, а не само значение; идентификатор объекта можно получить с помощью
id()
.
Изменяемые типы (например,list
,dict
,set
) можно модифицировать без создания нового объекта. Неизменяемые типы (int, str, tuple) при изменении создаются заново. - При копировании коллекций важно различать поверхностную копию (
x[:]
) и глубокую копию (deepcopy()
), особенно при работе со вложенными структурами.