💻 Примеры для повторения

⚡ Минимальный пример

with open("data.txt", "r", encoding="utf-8") as f:
    lines = (line.strip() for line in f if line.strip())
    print(sum(1 for _ in lines))

Читаем файл и считаем непустые строки генераторным выражением.

ТемаЧто помнить
open()режимы r/w/a/x/b/+; всегда с with и encoding="utf-8"
Чтениеread() — вся строка целиком, readline() — одна строка, readlines() — список строк
iter()/next()next(it) продвигает итератор; next(it, default) не бросает StopIteration
Генераторленивый, экономит память, не поддерживает [i] и len()
itertoolschain склеивает, count/cycle бесконечны, product/permutations — комбинаторика
Топ-3 ошибки: забыли encoding="utf-8" при open() · переиспользовали уже исчерпанный итератор · перепутали генератор со списком и попытались его проиндексировать.

Этот урок не добавляет новой теории — он проверяет, как соединяются четыре темы: файлы (урок 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, которая делает то же самое, но для более сложной логики.