Этот урок не добавляет новой теории — он проверяет, как соединяются четыре темы: файлы (урок 52), итераторы и генераторные выражения (урок 54) и модуль itertools. Примеры идут в том же порядке. Файлы создаются и читаются реальным кодом — весь вывод в комментариях получен запуском, а не подобран вручную.
Пример 1. Режимы open() и безопасное закрытие через with
with гарантирует, что файл закроется даже при ошибке внутри блока — в этом его смысл, а не просто более короткая запись.
with open("report.txt", "w", encoding="utf-8") as file:
file.write("первая строка\n")
print("Файл создан режимом 'w'")
with open("report.txt", "a", encoding="utf-8") as file:
file.write("вторая строка\n")
print("Дописали режимом 'a'")
try:
with open("report.txt", "x", encoding="utf-8") as file:
pass
except FileExistsError as error:
print("FileExistsError:", error)
# [Errno 17] File exists: 'report.txt'
with open("report.txt", "r", encoding="utf-8") as file:
print(repr(file.read()))
# 'первая строка\nвторая строка\n'
Что происходит: "w" создаёт файл заново (стирая старое содержимое, если оно было), "a" дописывает в конец, а "x" нужен, когда важно не перезаписать существующий файл случайно — Python сам откажется, если файл уже есть.
Пример 2. read(), readline(), readlines() — три разные формы результата
Все три метода читают один и тот же файл, но возвращают данные по-разному, и это частый источник путаницы (в топ-3 ошибок урока).
with open("report.txt", "r", encoding="utf-8") as file:
print("read():", repr(file.read()))
# read(): 'первая строка\nвторая строка\n' — вся строка целиком
with open("report.txt", "r", encoding="utf-8") as file:
print("readline():", repr(file.readline())) # 'первая строка\n'
print("readline() ещё раз:", repr(file.readline())) # 'вторая строка\n'
with open("report.txt", "r", encoding="utf-8") as file:
print("readlines():", file.readlines())
# ['первая строка\n', 'вторая строка\n'] — список строк
Как проверить себя: read() — одна большая строка, readline() — по одной строке за вызов, readlines() — сразу список. Перебор for line in file из примера 1 практикума делает то же самое, что readlines(), но не хранит все строки в памяти одновременно.
Пример 3. Запись: write() и writelines()
Симметричная пара к чтению: write() пишет одну строку текста, writelines() — сразу список строк. Ни тот, ни другой не добавляют перенос строки автоматически — \n нужно ставить самим.
lines = ["строка A\n", "строка B\n", "строка C\n"]
with open("batch.txt", "w", encoding="utf-8") as file:
file.writelines(lines)
with open("batch.txt", "r", encoding="utf-8") as file:
for line in file:
print(line.strip())
# строка A
# строка B
# строка C
Почему так: writelines() — это не «writeline с s», а «записать список строк одну за другой». Если в исходных строках нет \n, весь текст слипнется в одну строку файла.
Пример 4. iter(), next() и StopIteration
iter() превращает итерируемый объект (список) в итератор — объект, который отдаёт элементы по одному и помнит, где остановился.
values = iter([10, 20, 30])
print(next(values)) # 10
print(next(values)) # 20
print(next(values, "конец")) # 30
print(next(values, "конец")) # конец — элементы закончились, но исключения нет
values2 = iter([1, 2])
try:
next(values2)
next(values2)
next(values2) # элементов больше нет
except StopIteration:
print("StopIteration поймали вручную")
Что происходит: без второго аргумента next() на исчерпанном итераторе бросает StopIteration — именно этим исключением, «под капотом», управляется цикл for: он ловит StopIteration сам и просто завершается.
Пример 5. Итератор одноразовый — список нет
Ошибка 2 из раздела «Ошибки» этого урока: пройти итератор один раз и попытаться пройти ещё раз, ожидая тот же результат.
it = iter([1, 2, 3])
print(list(it)) # [1, 2, 3]
print(list(it)) # [] — уже нечего отдавать
numbers_list = [1, 2, 3]
print(list(numbers_list)) # [1, 2, 3]
print(list(numbers_list)) # [1, 2, 3] — список можно обходить сколько угодно раз
Почему так: список — это данные, а итератор — это процесс чтения данных с запомненной позицией. Как только позиция дошла до конца, у итератора не остаётся, что возвращать. Если нужно пройти данные снова — нужен новый итератор (iter(numbers_list)), а не тот же самый объект.
Пример 6. Генераторное выражение: ленивость, память, без индексации
Генераторное выражение (x ** 2 for x in numbers) выглядит как list comprehension в круглых скобках, но устроено принципиально иначе: оно не считает все значения сразу.
numbers = [1, 2, 3, 4, 5]
squares_gen = (x ** 2 for x in numbers)
squares_list = [x ** 2 for x in numbers]
print(type(squares_gen)) # <class 'generator'>
try:
squares_gen[0] # индексация недоступна
except TypeError as error:
print("TypeError:", error)
# 'generator' object is not subscriptable
print(list(squares_gen)) # [1, 4, 9, 16, 25] — значения вычисляются здесь
Разница в памяти становится заметна на больших объёмах — вот тот же приём на миллионе чисел:
import sys
big_gen = (x ** 2 for x in range(1, 1_000_001))
big_list = [x ** 2 for x in range(1, 1_000_001)]
print(sys.getsizeof(big_gen), "байт — генератор") # 208 байт — генератор
print(sys.getsizeof(big_list), "байт — список") # 8448728 байт — список
sys.getsizeof() выходит за рамки source этого урока, добавлен только для наглядной иллюстрации разницы в памяти. Сверьтесь с docs.python.org — sys.getsizeof().
Что важно: генератор хранит не готовые значения, а «рецепт», как их получить по одному. Для маленьких numbers из пяти элементов разница в памяти незаметна и даже не в пользу генератора — экономия проявляется именно на больших или потенциально бесконечных последовательностях.
Пример 7. any(), all(), sum() с генераторным выражением
Эти функции — самое частое место, где генераторное выражение пишут прямо внутри вызова, без отдельной переменной и без списка.
nums = [3, 7, 12, 5, 9]
print(any(n > 10 for n in nums)) # True
print(all(n > 0 for n in nums)) # True
print(sum(n for n in nums if n % 2 == 0)) # 12 — сумма только чётных
def probe(n):
print(" проверяю", n)
return n > 10
print(any(probe(n) for n in nums))
# проверяю 3
# проверяю 7
# проверяю 12
# True — до 5 и 9 проверка не дошла, результат уже известен
Почему так: any() обходит генератор по одному значению за раз и останавливается сразу после первого True — генератор для этого подходит даже лучше списка: не нужно заранее готовить весь список, чтобы проверить всего одно условие.
Пример 8. itertools.chain: пройти несколько последовательностей как одну
import itertools
fruits = ["apple", "banana"]
vegetables = ["carrot", "tomato"]
for item in itertools.chain(fruits, vegetables):
print(item)
# apple
# banana
# carrot
# tomato
Что происходит: chain() не создаёт новый список из копий элементов — он просто переключается со списка на список по очереди. Это тот же принцип ленивости, что и у генераторного выражения из примера 6.
Пример 9. itertools.count: бесконечный счётчик и как его безопасно ограничить
count() генерирует числа бесконечно — обойти его циклом for ... in count() без остановки означает зависшую программу. Ограничивают его через break или itertools.islice().
counter = itertools.count(start=10, step=5)
first_three = list(itertools.islice(counter, 3))
print(first_three) # [10, 15, 20]
itertools.islice() не входит в список функций source (там перечислены только count, cycle, chain, product, permutations), но без него безопасно продемонстрировать бесконечный count() нельзя. Сверьтесь с docs.python.org — itertools.islice().
Пример 10. itertools.product и itertools.permutations: комбинаторика без вложенных циклов
product() — все сочетания элементов из разных наборов (аналог вложенных for, как в примере 5 практикума 8). permutations() — все способы выбрать и упорядочить элементы одного набора.
sizes = ["S", "M"]
colors = ["red", "blue"]
print(list(itertools.product(sizes, colors)))
# [('S', 'red'), ('S', 'blue'), ('M', 'red'), ('M', 'blue')]
print(list(itertools.permutations([1, 2, 3], 2)))
# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
Разница: product берёт по одному элементу из каждого набора — порядок наборов важен, но набор с собой не смешивается. permutations берёт элементы из одного набора без повторов — (1, 2) и (2, 1) оба присутствуют как разные перестановки.
Пример 11. Собираем всё: файл + генераторное выражение (задание 3 урока)
Итоговая задача из раздела «Задания»: создать файл с числами, прочитать его генераторным выражением и посчитать сумму квадратов — без единого промежуточного списка.
with open("numbers.txt", "w", encoding="utf-8") as file:
for n in range(1, 11):
file.write(str(n) + "\n")
with open("numbers.txt", "r", encoding="utf-8") as file:
total = sum(int(line.strip()) ** 2 for line in file if line.strip())
print("Сумма квадратов:", total) # 385
Что дальше: та же ленивость, что у генераторного выражения, — основа для следующей темы курса, урока 56 «Генераторы»: там вместо выражения в скобках появится функция с yield, которая делает то же самое, но для более сложной логики.