Часть 1: Магия with: почему это не только про open()
Привет, Хабр!
Если вы пишете на Python хотя бы пару месяцев, вы наверняка сталкивались с конструкцией with open(...) as f:. Это как ритуал, который мы совершаем, чтобы открыть файл: удобно, понятно, и все говорят, что так надо. Мы интуитивно чувствуем, что это "правильный" способ, потому что он избавляет нас от головной боли с ручным закрытием файла через f.close().
Но задумывались ли вы, что на самом деле происходит за кулисами этого простого оператора? И что, если я скажу вам, что эта магия доступна не только для файлов? Что, если with — это один из самых мощных и недооцененных инструментов в арсенале Python-разработчика, способный сделать ваш код чище, надежнее и выразительнее?
Давайте честно: все мы хоть раз забывали закрыть ресурс. Будь то файл, соединение с базой данных или сетевой сокет. В лучшем случае это просто неаккуратно. В худшем — приводит к утечкам ресурсов, исчерпанию лимитов соединений и трудноуловимым багам, которые проявляются под нагрузкой. Классический подход с try...finally решает эту проблему, но, согласитесь, он довольно громоздкий:
# Старый, многословный способ
f = open('my_file.txt', 'w')
try:
f.write('Привет, мир!')
finally:
f.close()
А теперь наш элегантный with:
# Чисто, коротко и безопасно
with open('my_file.txt', 'w') as f:
f.write('Привет, мир!')
Код делает то же самое, но насколько же он лаконичнее! Главное его преимущество в том, что он гарантирует освобождение ресурса. Неважно, завершился ли блок кода успешно, или внутри него произошло исключение — Python позаботится о том, чтобы все было "прибрано".
И вот тут мы подходим к главному. Конструкция with ... as — это не какая-то специальная функция для работы с файлами. Это универсальный механизм, который называется "менеджер контекста". И сегодня мы разберемся, как он устроен, и научимся применять его для самых разных задач, далеко за пределами чтения и записи файлов.
Как это работает "под капотом"?
Вся магия держится на двух специальных методах, которые должен реализовать объект, чтобы считаться менеджером контекста. Представьте, что у нас есть невидимый дворецкий, который выполняет за нас всю грязную работу.
__enter__(self): Этот метод вызывается, когда мы "входим" в блокwith. Его задача — подготовить все необходимое. Для файлов это означает открыть сам файл. Если вы используете конструкцию... as my_var, то в переменнуюmy_varпопадет именно то, что вернет метод__enter__.-
__exit__(self, exc_type, exc_val, exc_tb): А это "уборщик". Метод вызывается, когда мы покидаем блокwith, причем всегда, независимо от причины.Если все прошло гладко, он просто завершает работу (например, закрывает файл).
Если внутри блока
withпроизошло исключение, то информация о нем (тип, значение и трассировка) передается в__exit__. Это дает нам возможность корректно обработать ошибку — например, откатить транзакцию в базе данных перед тем, как закрыть соединение.
Понимая эти два простых принципа, мы перестаем видеть в with просто синтаксический сахар. Мы видим мощный паттерн для управления любыми ресурсами, которые требуют четких действий по настройке и очистке.
Часть 2: Практические примеры: от баз данных до таймеров
Итак, мы знаем, что with — это про гарантированную "уборку". Давайте применим этот принцип к реальным задачам, с которыми сталкивается каждый разработчик.
Пример 1: Управление соединениями с базами данных
Это, пожалуй, вторая по популярности задача для with после файлов. Кто хоть раз писал бэкенд, знает: забыть закрыть соединение с БД — прямой путь к проблемам. Соединения — это ценный и ограниченный ресурс, и если их не освобождать, они очень быстро закончатся, и ваше приложение перестанет отвечать.
Проблема: Ручное управление соединениями и курсорами — это многословный и опасный код.
import sqlite3
# Как делать НЕ НАДО (легко забыть .close())
conn = sqlite3.connect('test.db')
cursor = conn.cursor()
cursor.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER, name TEXT)')
# ... тут может произойти ошибка, и .close() никогда не выполнится
conn.commit()
cursor.close()
conn.close()
Немного лучше с try...finally, но всё ещё громоздко.
Решение: Хорошие библиотеки для работы с базами данных (включая стандартный sqlite3) реализуют менеджер контекста для своих объектов соединений.
Код: Объект connection в sqlite3 при использовании в with делает две важные вещи:
Автоматически вызывает
commit()в конце, если блок выполнился без ошибок.Автоматически вызывает
rollback(), если внутри блока произошло исключение.В любом случае, соединение будет корректно закрыто.
import sqlite3
# Идеальный вариант
try:
# Соединение само является менеджером контекста!
with sqlite3.connect('test.db') as conn:
cursor = conn.cursor()
cursor.execute('INSERT INTO users VALUES (1, "Alice")')
# Если здесь произойдет ошибка, транзакция автоматически откатится
cursor.execute('INSERT INTO users VALUES (2, "Bob")')
# Как только мы вышли из блока with, транзакция закоммичена, а соединение закрыто.
print("Пользователи добавлены!")
except sqlite3.Error as e:
print(f"Произошла ошибка: {e}")
Чисто, безопасно и читаемо. Никаких шансов оставить "висящее" соединение.
Пример 2: Замер времени выполнения кода
Часто нужно понять, сколько времени выполняется тот или иной участок кода. Стандартный подход — засечь время до и после.
Проблема: Ручной замер времени загромождает код и смешивает логику программы с логикой профилирования.
import time
start_time = time.time()
# --- Начало блока, который мы измеряем ---
result = [x**2 for x in range(1000000)]
# --- Конец блока ---
end_time = time.time()
print(f"Блок выполнился за {end_time - start_time:.4f} секунд")
Решение: А что если "подготовка" — это засечка времени, а "очистка" — это расчёт и вывод дельты? Идеальная задача для менеджера контекста!
Код: Напишем простенький таймер. Мы пока не будем создавать полноценный класс, а воспользуемся удобным декоратором @contextmanager из стандартной библиотеки contextlib (подробнее о нем — в следующей части!).
from contextlib import contextmanager
import time
@contextmanager
def timer(label: str):
start_time = time.time()
try:
# yield передаёт управление внутрь блока with
yield
finally:
# этот код выполнится после выхода из блока with
end_time = time.time()
print(f"{label}: выполнено за {end_time - start_time:.4f} секунд")
# И как это использовать:
with timer("Генерация списка"):
result = [x**2 for x in range(1000000)]
with timer("Создание множества"):
my_set = {x for x in range(5000000)}
# Вывод:
# Генерация списка: выполнено за 0.0818 секунд
# Создание множества: выполнено за 0.2227 секунд
Смотрите, как изящно! Вся логика таймера инкапсулирована, а наш основной код остался чистым и сфокусированным на своей задаче.
Пример 3: Временное изменение состояния
Иногда нужно выполнить кусок кода с какими-то особыми настройками: например, с повышенной точностью для математических вычислений или временно перенаправить вывод в другой поток.
Проблема: Нужно не забыть вернуть всё как было, даже если что-то пошло не так.
Решение: Контекстный менеджер, который в __enter__ запоминает старое состояние и устанавливает новое, а в __exit__ — гарантированно восстанавливает старое.
Код: Классический пример — модуль decimal.
from decimal import Decimal, getcontext
# Исходная точность
print(f"Исходная точность: {getcontext().prec}") # по умолчанию 28
# Вычисляем с обычной точностью
print(Decimal(1) / Decimal(7))
# А теперь нам нужен блок с повышенной точностью
with localcontext() as ctx: # localcontext - это готовый менеджер контекста!
ctx.prec = 42
print(f"Временная точность: {getcontext().prec}")
print(Decimal(1) / Decimal(7))
# Точность снова стала прежней
print(f"Точность после блока: {getcontext().prec}")
# Вывод:
# Исходная точность: 28
# 0.1428571428571428571428571429
# Временная точность: 42
# 0.142857142857142857142857142857142857142857
# Точность после блока: 28
Библиотека decimal уже предоставляет нам готовый localcontext, но написать такой же для управления любыми другими настройками — дело техники, которую мы разберем в следующей части.
Пример 4: Потоки и блокировки (threading.Lock)
В многопоточном программировании with — это не просто удобство, а жизненная необходимость. Если один поток "захватит" блокировку (lock.acquire()) и упадёт с ошибкой до того, как "отпустит" её (lock.release()), то все остальные потоки, ждущие эту блокировку, зависнут навсегда. Это называется deadlock.
Решение: Объект threading.Lock тоже является менеджером контекста!
Код:
import threading
lock = threading.Lock()
some_shared_resource = []
def do_work():
# ...
# Это безопасный и правильный способ
with lock:
# В этот момент может работать только один поток
some_shared_resource.append(1)
print(f"Поток {threading.current_thread().name} поработал с ресурсом")
# lock.release() вызовется автоматически при выходе из блока,
# даже если тут случится исключение.
# ...
Конструкция with lock: — это короткая и безопасная запись для: lock.acquire() при входе и lock.release() при выходе. Всегда используйте её при работе с блокировками.
Пример 5: Подавление ожидаемых исключений
Что делать, если мы ожидаем, что какая-то операция может вызвать ошибку, и хотим её просто проигнорировать? Например, при удалении файла, которого может и не быть.
Проблема: try...except...pass выглядит немного громоздко и может скрыть реальную проблему, если мы случайно поймаем не то исключение.
import os
# Способ "в лоб"
try:
os.remove('some_non_existent_file.tmp')
except FileNotFoundError:
pass # Ожидаемая ошибка, всё в порядке
Решение: В contextlib есть изящный менеджер контекста suppress.
Код:
from contextlib import suppress
import os
# Элегантно и явно
with suppress(FileNotFoundError):
os.remove('some_non_existent_file.tmp')
print("Файл удален (или его и не было)")
print("Программа продолжает работу")
Этот код говорит сам за себя: "Выполни следующий блок и, пожалуйста, подави исключение FileNotFoundError, если оно возникнет". Это намного более читаемо и явно выражает ваше намерение.
Как видите, сфера применения менеджеров контекста огромна. Они помогают нам управлять соединениями, засекать время, менять настройки, писать безопасный многопоточный код и даже элегантно обрабатывать ошибки.
Ключевая идея — инкапсуляция логики "настройки" и "очистки" (setup / teardown). Если у вас в коде есть повторяющийся паттерн из try...finally, это верный знак: здесь можно и нужно написать свой менеджер контекста.
Часть 3: Создание собственных менеджеров контекста
Ну что ж, мы увидели, насколько мощным и удобным может быть оператор with. Мы управляли базами данных, замеряли время и даже работали с потоками. Но настоящая сила приходит тогда, когда вы понимаете, что не нужно ждать милости от разработчиков библиотек. Вы можете создавать собственные менеджеры контекста для решения ваших уникальных задач.
В Python есть два основных пути для этого: олдскульный, через класс, и элегантный, через декоратор. Давайте разберем оба.
1. Классовый подход: все по протоколу
Это фундаментальный способ. Он требует чуть больше кода, но зато дает полный контроль над процессом и отлично проясняет, как все устроено "под капотом". Чтобы превратить обычный класс в менеджер контекста, нам нужно реализовать всего два метода: __enter__ и __exit__.
Давайте вернемся к нашему примеру с таймером из прошлой части и реализуем его в виде полноценного класса.
__init__(self, ...): Обычный конструктор. Сюда мы можем передать любые параметры, которые понадобятся нашему менеджеру.__enter__(self): Код, который выполняется при входе в блокwith. Он должен подготовить среду и, опционально, вернуть объект, который будет доступен послеas.-
__exit__(self, exc_type, exc_value, traceback): Код, который гарантированно выполняется при выходе из блокаwith. Самое интересное здесь — его аргументы.Если блок
withзавершился без ошибок, все три аргумента будутNone.Если произошло исключение, они будут содержать информацию о нём (тип, объект и трейсбек). Это позволяет нам по-разному реагировать на успешное и аварийное завершение.
Важный нюанс: если
__exit__вернетTrue, то исключение будет подавлено и не "вылетит" за пределыwith. Если он вернетFalse(или ничего, что равносильноNone), исключение продолжит свой путь наверх.
Код:
import time
class Timer:
def __init__(self, label: str):
self.label = label
print(f"Таймер '{self.label}' запущен...")
def __enter__(self):
# При входе в блок with засекаем время
self.start_time = time.time()
# Этот return не обязателен, т.к. мы не используем `as`
# Но если бы мы хотели что-то передать, то вернули бы это здесь
return self
def __exit__(self, exc_type, exc_value, traceback):
# При выходе из блока вычисляем разницу
end_time = time.time()
print(f"Блок '{self.label}' выполнен за {end_time - self.start_time:.4f} секунд")
if exc_type is not None:
# Если было исключение, сообщим об этом
print(f"Блок завершился с ошибкой: {exc_type.__name__}")
# Возвращаем False (или ничего), чтобы не подавлять исключение
return False
# Используем наш класс
with Timer("Обработка данных"):
# какой-то долгий процесс
_ = [x**2 for x in range(1000000)]
print("-" * 20)
# А теперь с ошибкой
try:
with Timer("Провальная операция"):
_ = 100 / 0
except ZeroDivisionError:
print("Исключение было поймано снаружи")
Вывод:
Таймер 'Обработка данных' запущен...
Блок 'Обработка данных' выполнен за 0.0825 секунд
--------------------
Таймер 'Провальная операция' запущен...
Блок 'Провальная операция' выполнен за 0.0000 секунд
Блок завершился с ошибкой: ZeroDivisionError
Исключение было поймано снаружи
Этот подход идеален, когда вам нужно управлять сложным состоянием. Объект self позволяет легко хранить данные (как self.start_time) между вызовами __enter__ и __exit__.
2. Функциональный подход с contextlib
Давайте по-честному: писать целый класс для простого таймера — это немного чересчур. Python-сообщество ценит лаконичность, и для таких случаев в стандартной библиотеке contextlib есть волшебный декоратор — @contextmanager.
Он позволяет превратить в менеджер контекста обычную функцию-генератор (то есть функцию с yield).
Как это работает?
Все, что в функции находится до инструкции yield, работает как __enter__.
Сама инструкция yield передает управление в блок with. То, что вы передадите в yield (если передадите), попадет в переменную после as.
Все, что находится после yield, работает как __exit__.
Ключевой момент: чтобы код после yield гарантированно выполнился (ведь в блоке with может быть исключение!), его нужно обернуть в try...finally.
Код:
Давайте перепишем наш Timer с помощью @contextmanager. Почувствуйте разницу!
from contextlib import contextmanager
import time
@contextmanager
def timer(label: str):
print(f"Таймер '{label}' запущен...")
start_time = time.time()
try:
# Передаем управление в with
yield
finally:
# Этот блок выполнится всегда!
end_time = time.time()
print(f"Блок '{label}' выполнен за {end_time - start_time:.4f} секунд")
# Используем точно так же!
with timer("Обработка данных (через декоратор)"):
_ = [x**2 for x in range(1000000)]
Вывод:
Таймер 'Обработка данных (через декоратор)' запущен...
Блок 'Обработка данных (через декоратор)' выполнен за 0.0841 секунд
Насколько же чище и короче!
Когда что использовать?
Используйте
@contextmanagerв 90% случаев. Он идеально подходит для простых и средних задач, где нужно просто выполнить что-то до и что-то после. Это более "питоничный" и читаемый способ.Используйте класс, когда ваш менеджер контекста имеет сложную внутреннюю логику и состояние, которое нужно хранить и изменять. Например, менеджер транзакций, который может отслеживать уровень вложенности.
Теперь вы не просто используете with, а понимаете его суть и умеете создавать свои собственные, идеально подходящие под ваши задачи, контекстные менеджеры. Это открывает дверь к написанию более надежного, чистого и декларативного кода.
Часть 4: Продвинутые техники и заключение
Мы прошли большой путь: от простого with open() до создания собственных менеджеров контекста двумя разными способами. Теперь, когда вы уверенно владеете основами, давайте рассмотрим пару сценариев, которые сделают ваш код еще более элегантным.
Вложенные менеджеры контекста: прощай, "пирамида ужаса"
Что если нам нужно работать с несколькими ресурсами одновременно? Например, читать данные из одного файла и сразу же записывать их в другой. По старинке, можно было бы вложить один блок with в другой:
# Некрасиво и громоздко: "Пирамида ужаса" (Pyramid of Doom)
with open('source.txt', 'r') as source_file:
with open('destination.txt', 'w') as dest_file:
# Уровень вложенности растет...
for line in source_file:
dest_file.write(line.upper())
Работает? Да. Но если добавить третий или четвертый ресурс, код станет совершенно нечитаемым из-за большого количества отступов.
К счастью, Python предлагает гораздо более изящное решение. Можно перечислить несколько менеджеров контекста в одной строке with, разделив их запятыми.
# Чисто, плоско и читаемо
with open('source.txt', 'r') as source_file, open('destination.txt', 'w') as dest_file:
for line in source_file:
dest_file.write(line.upper())
print("Копирование и преобразование завершено!")
Результат тот же, но код стал плоским и гораздо более приятным для глаз. Это работает для любых менеджеров контекста, не только для файлов. Вы можете в одной строке открыть файл, подключиться к базе данных и запустить таймер. Python гарантирует, что все методы __exit__ будут вызваны корректно, даже если ошибка произойдет при инициализации одного из последующих менеджеров.
Асинхронные менеджеры контекста (async with)
Если вы работаете с современным асинхронным кодом на asyncio, то для вас есть отличные новости. Концепция менеджеров контекста была элегантно перенесена и в этот мир.
Проблема: Обычный with является блокирующей операцией. Методы __enter__ и __exit__ — это обычные синхронные функции. Если вам нужно, например, установить сетевое соединение в асинхронном приложении, использование обычного with заблокирует весь цикл событий (event loop), сведя на нет все преимущества асинхронности.
Решение: Для асинхронных операций существует конструкция async with. Она работает с объектами, которые реализуют не __enter__/__exit__, а их асинхронные аналоги:
__aenter__(self): асинхронный метод для "входа".__aexit__(self, exc_type, exc_val, exc_tb): асинхронный метод для "выхода".
Самый частый пример — работа с HTTP-запросами с помощью популярной библиотеки aiohttp.
import asyncio
import aiohttp
async def main():
# aiohttp.ClientSession — это асинхронный менеджер контекста.
# Он управляет пулом соединений.
async with aiohttp.ClientSession() as session:
# Внутри этого блока мы можем делать запросы
async with session.get('https://api.github.com/events') as response:
print(f"Статус ответа: {response.status}")
data = await response.json()
print(f"Получено {len(data)} событий.")
# Соединение будет корректно возвращено в пул
# после выхода из блока async with.
# Запускаем асинхронную функцию
if __name__ == "__main__":
asyncio.run(main())
Использование async with гарантирует, что такие операции, как установка соединения и его закрытие, будут происходить асинхронно, не блокируя другие задачи в вашем приложении. Если вы пишете на asyncio, async with — ваш лучший друг для управления асинхронными ресурсами.
Заключение: думайте о контексте!
Мы начали с простого примера с файлом, а закончили асинхронными операциями. Надеюсь, теперь для вас очевидно, что with — это не просто синтаксический сахар для try...finally. Это мощный паттерн проектирования, который помогает писать более надежный, читаемый и декларативный код.
Давайте подведем итог:
Это про управление ресурсами:
withгарантирует, что ресурсы (файлы, сокеты, соединения с БД, блокировки) будут корректно освобождены.Это про управление состоянием:
withпозволяет временно изменить настройки или среду, с гарантированным возвратом к исходному состоянию.Это чисто и явно: Код внутри блока
withчетко отделен и сфокусирован на своей задаче. Логика "подготовки" и "уборки" инкапсулирована в менеджере контекста.Это расширяемо: Вы можете и должны писать собственные менеджеры контекста для инкапсуляции повторяющихся паттернов
setup/teardownв вашем коде. Для простых случаев используйте@contextmanager, для сложных — классы.
В следующий раз, когда вы поймаете себя на написании очередного try...finally, остановитесь на секунду и спросите себя: "А не является ли это задачей для менеджера контекста?". С большой долей вероятности, ответ будет "да".
Не ограничивайте себя файлами — начните думать о контексте
Домашнее задание для закрепления материала
Задача 1: Простой генератор HTML-тегов
Напишите менеджер контекста html_tag, который можно использовать для обертывания текста в HTML-теги. Он должен принимать имя тега в качестве аргумента.
Пример использования:
print("Начало документа")
with html_tag("h1"):
print("Это заголовок")
with html_tag("p"):
print("А это параграф текста.")
print("Конец документа")
Ожидаемый вывод:
Начало документа
<h1>
Это заголовок
</h1>
<p>
А это параграф текста.
</p>
Конец документа
Подсказка: Это идеальный кандидат для реализации через декоратор @contextmanager.
Задача 2: Безопасная смена директории
Создайте менеджер контекста in_dir, который временно меняет текущую рабочую директорию на указанную, а после выхода из блока with гарантированно возвращает ее обратно, даже если внутри блока произошла ошибка.
Пример использования:
import os
# Создадим тестовые директории
os.makedirs("dir1/dir2", exist_ok=True)
print(f"Изначальная директория: {os.getcwd()}")
try:
with in_dir("dir1"):
print(f"Внутри with: {os.getcwd()}")
with in_dir("dir2"):
print(f"Внутри вложенного with: {os.getcwd()}")
# Симулируем ошибку
raise ValueError("Что-то пошло не так")
except ValueError as e:
print(f"Поймали ошибку: {e}")
print(f"Финальная директория: {os.getcwd()}")
Ожидаемый вывод:
Должно быть видно, что несмотря на ValueError, финальная директория совпадает с изначальной. Путь может отличаться в вашей системе.
Изначальная директория: /path/to/project
Внутри with: /path/to/project/dir1
Внутри вложенного with: /path/to/project/dir1/dir2
Поймали ошибку: Что-то пошло не так
Финальная директория: /path/to/project
Подсказка: Используйте модуль os для получения текущей директории (os.getcwd) и ее смены (os.chdir). Не забудьте про try...finally, если используете @contextmanager.
Задача 3: Менеджер транзакций для словаря
Напишите менеджер контекста dict_transaction, который работает с обычным словарем. Если код внутри блока with завершается успешно, все изменения в словаре должны сохраниться. Если же внутри блока происходит исключение, все изменения, сделанные в словаре, должны быть отменены (словарь должен вернуться к состоянию, которое было до входа в блок with).
Пример использования:
data = {"a": 1, "b": 2}
print(f"До: {data}")
try:
with dict_transaction(data) as t_data:
t_data["c"] = 3 # Это изменение должно откатиться
t_data["a"] = 100 # И это тоже
raise KeyError("Произошла ошибка транзакции!")
except KeyError:
print("Произошла ошибка, транзакция отменена.")
print(f"После ошибки: {data}")
print("-" * 20)
print(f"До: {data}")
with dict_transaction(data) as t_data:
t_data["d"] = 4 # Это изменение сохранится
t_data["b"] = 20 # И это
print(f"После успеха: {data}")
Ожидаемый вывод:
До: {'a': 1, 'b': 2}
Произошла ошибка, транзакция отменена.
После ошибки: {'a': 1, 'b': 2}
--------------------
До: {'a': 1, 'b': 2}
После успеха: {'a': 1, 'b': 20, 'd': 4}
Подсказка: Проще всего реализовать через класс. В __enter__ нужно сделать неглубокую копию словаря (.copy()), которую и будет изменять пользователь. В __exit__ в случае успеха нужно обновить оригинальный словарь (.update()), а в случае ошибки — просто ничего не делать.
Задача 4: Подавление нескольких исключений
Стандартный contextlib.suppress может подавлять только один тип исключений (или несколько, если передать их как кортеж). Напишите свой собственный менеджер контекста Suppressor, который принимает на вход произвольное количество типов исключений и подавляет любое из них, если оно возникнет внутри блока with. Любые другие исключения должны "пролетать" дальше.
Пример использования:
with Suppressor(TypeError, ValueError):
print("Попытка сложить строку и число...")
x = "2" + 2 # Вызовет TypeError, будет подавлено
print("Эта строка не выполнится")
print("Продолжаем работу после первого блока.")
try:
with Suppressor(TypeError, ValueError):
print("Деление на ноль...")
y = 1 / 0 # Вызовет ZeroDivisionError, не будет подавлено
except ZeroDivisionError:
print("Исключение ZeroDivisionError было поймано, как и ожидалось.")
Ожидаемый вывод:
Попытка сложить строку и число...
Продолжаем работу после первого блока.
Деление на ноль...
Исключение ZeroDivisionError было поймано, как и ожидалось.
Подсказка: Эту задачу нужно решать через класс. Используйте *args в __init__ для приема произвольного числа аргументов. Вся логика будет в методе __exit__, где нужно проверить, принадлежит ли тип возникшего исключения (exc_type) к тем, что были переданы в конструктор.
Задача 5 (со звездочкой): "Перенаправление" вывода
Напишите менеджер контекста redirect_stdout, который временно перенаправляет весь стандартный вывод (stdout, то, что выводит print) в указанный файл. После выхода из блока with стандартный вывод должен быть восстановлен в исходное состояние, даже если произошла ошибка.
Пример использования:
import sys
print("Эта строка выводится в консоль.")
with open("output.txt", "w", encoding='utf-8') as f:
with redirect_stdout(f):
print("А эта строка должна попасть в файл.")
print("И эта тоже.")
# raise Exception("Проверка восстановления stdout при ошибке") # Раскомментируйте для теста
print("Эта строка снова выводится в консоль.")
# Проверим содержимое файла
with open("output.txt", "r", encoding='utf-8') as f:
print("\nСодержимое файла output.txt:")
print(f.read())
Ожидаемый вывод в консоли:
Эта строка выводится в консоль.
Эта строка снова выводится в консоль.
Содержимое файла output.txt:
А эта строка должна попасть в файл.
И эта тоже.
Подсказка: Для решения понадобится модуль sys. Оригинальный sys.stdout нужно сохранить во временную переменную, а на его место подставить объект файла. В блоке finally (или в __exit__) нужно вернуть sys.stdout его исходное значение.
Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.
Уверен, у вас все получится. Вперед, к практике!
Mr_Boshi
Довольно информативно и полезно, хорошая фича питона. Спасибо за статью.