With python обработка ошибок

Под капотом Python. Тонкости популярных конструкций with и contextmanager

Привет, Хабр! Меня зовут Павел Корсаков, я python-разработчик, backend-developer в облачном провайдере beeline cloud.

Язык программирования Python задумывался своим автором Гвидо ван Россумом как ориентированный на повышение производительности разработчика, читаемости кода и его качества. Как следствие, в нем много синтаксического сахара, за который мы его любим. А к какому-то сахару мы так привыкаем, что используем и не задумываемся, что происходит под капотом в этот момент. В статье я хочу разобрать конструкции языка with и contextmanager, рассказать, как они устроены, какие задачи решают и как развивались от истоков Python до наших дней.

В этой статье оглянемся в прошлое языка, ответим на вопросы, как написать менеджер контекста, как создать функцию генератор для декоратора contextmanager. Опытные разработчики могут узнать что-то новенькое или дополнить статью ценными комментариями.

Как это было

Если исходить из того, что делает with и contextmanager, то можно найти что-то общее с try … except, и это неспроста, так как именно эта конструкция лежит в основе.

Сама конструкция появилась задолго до Python и использовалась в языках, оказавших на него влияние таких, как Ada, C++, Java. Они имеют свои механизмы обработки исключений, похожие на Python.

with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure main is x, y, z : Integer; begin Put("X = "); Get(x); Put("Y = "); Get(y); z := x / y; Put_Line(Integer'Image(x) & " / " & Integer'Image(y) & " Цифирь давай, цифирь!"); when Constraint_Error => Put_Line("Я такое не ем!"); when others => Put_Line("Что-то пошло не так!"); end main; 
double divide(int a, int b) < if (b) return a / b; throw "Division by zero!"; >int main() < int x; int y<>; try < double z ; std::cout catch (const char* error_message) < std::cout std::cout
public static void main(String[] args) throws IOException < try < BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt")); String firstString = reader.readLine(); System.out.println(firstString); >catch (ArithmeticException e) < System.out.println("Ошибка! Файл не найден!"); >> 

На сайте https://docs.python.org самое первое упоминание, которое удалось найти в официальной документации, — 1.4 от 25 октября 1996 года.

Оно говорит, что существует две формы выражения try:

Эти формы нельзя смешивать, но они могут быть вложены друг в друга. Первый вариант — обработчик исключений, а второй нужен для гарантированного выполнения раздела finally.

С небольшими изменениями есть и сейчас:

def foo(): try: a = 5 / 0 except Exception as e: print(e)

Что здесь происходит? Когда интерпретатор доходит до строчки try, он запоминает текущий контекст программы.

Что же такое контекст

Для лучшего понимания заглянем в теоретическую часть. Контекст задачи — это минимальный набор данных, которые необходимо сохранить, чтобы была возможность прервать выполнение задачи, а после продолжить с того же места. Концепция контекста приобретает значение в случае прерываемых задач, когда после прерывания процессор сохраняет контекст и продолжает выполнять процедуру обслуживания прерываний. Термин часто встречается в асинхронном программировании в Python.

Сохранив контекст, интерпретатор может продолжить выполнение кода с того же места. После этого интерпретатор выполняет операторы, вложенные внутрь заголовка try. Дальше есть несколько вариантов развития событий:

  1. Если исключение не генерируется, то интерпретатор запустит операторы после блока except.
  2. Если генерируется исключение и оно указано в except, происходит откат до сохраненного контекста и выполняются операторы под except.
  3. Если генерируется исключение и оно не указано в except, тогда исключение распространяется до самого последнего введенного оператора try, который дает совпадения с исключением (try могут быть вложенными).
  4. Если найти оператор try не удается и поиск достигает верхнего уровня процесса, то интерпретатор Python уничтожит программу и выведет стандартное сообщение об ошибке.

Другой вариант это finally.

def foo(): try: a = 5 / 0 finally: print('finally 1') print('finally 2')

Здесь так же: когда интерпретатор доходит до строчки try, он запоминает текущий контекст программы. Выполняет операторы, вложенные внутрь заголовка try. Если исключение не генерируется, то интерпретатор запустит операторы в блоке finally и продолжит выполнение кода. Если генерируется исключение — интерпретатор запустит операторы в блоке finally и, так как ошибка не отработана, программа завершится. В выводе будет «finally 1» и сообщение об ошибке. Без вывода «finally 2». В этой конструкции finally выполняется всегда. Таким образом, в нашем случае, контекст — это набор данных, которые сохраняются программой перед выполнением try.

10 лет спустя: встреча с Python 2.5

Так жили 10 лет, пока 19 сентября 2006 не появился Python 2.5. С этого момента стало возможным совместное использование else, except и finally. А строковые исключения были признаны устаревшими.

С тех пор ничего не поменялось. Разве что ошибки теперь обязательно наследуются от класса Exception. Актуальная документация предлагает такой код:

def divide(x, y): try: result = x / y except ZeroDivisionError: print("division by zero!") else: print("result is", result) finally: print("executing finally clause")

Если несколько ошибок обрабатываются одинаково, их можно завернуть в кортеж:

except (ZeroDivisionError, ValueError):

И в одном try может быть несколько except:

try: result = x / y except ZeroDivisionError: print("division by zero!") except Exception: print("unexpected error!")

try … except — штука отличная. В Python использование исключения не приводит к значительным накладным расходам. Так же в Python — с версии 3.11 реализованы исключения «Zero-cost». Теперь, если исключение не вызывается, накладные расходы не увеличиваются, также как в C++ и Java. Ссылка на документацию.

Поэтому try … except широко используется. Срабатывание исключения всё же увеличивает накладные расходы. Блок try / except чрезвычайно эффективен, если не создаются исключения. На самом деле перехват исключения обходится дорого. Ссылка на документацию.

Оператор with — альтернатива try . finally

В Python 2.5 впервые появился новый оператор, связанный с исключениями with. Он задумывался как альтернатива try . finally. Оператор with предназначен для работы с контекстными менеджерами.

Тут я сделаю небольшое отступление и определимся с понятиями. Контекстный менеджер, или как у Марка Луца, диспетчер контекста — это интерфейс к объектам Python для определения нового контекста запуска. Контекстные менеджеры позволяют объекту определять код, который выполняется в начале и конце блока кода. Это реализовано с использованием пары методов, которые позволяют определяемым пользователем классам определять контекст времени выполнения, который вводится перед выполнением тела инструкции и завершается после завершения инструкции. Ссылка на документацию.

Пользовательский класс через метод __enter__ и __exit__ переключает состояние контекста. Поэтому он и называется менеджер контекста. В примере ниже – это EXPRESSION.

  • Если стало понятнее, то отлично. Если нет, то разбираемся, что такое интерфейс.
  • Если у класса реализованы методы __enter__ и __exit__ ,то он удовлетворяет интерфейсу менеджера контекста (или реализует интерфейс менеджера контекста).
  • И совсем по-простому. Если у класса реализованы методы __enter__ и __exit__ , то он — менеджер контекста.
  • Если посмотреть, что о нем сказано в документации, — магия улетучивается в один момент. Этот код:
with EXPRESSION as TARGET: SUITE
manager = (EXPRESSION) enter = type(manager).__enter__ exit = type(manager).__exit__ value = enter(manager) hit_except = False try: TARGET = value SUITE except: hit_except = True if not exit(manager, *sys.exc_info()): raise finally: if not hit_except: exit(manager, None, None, None)

Поскольку здесь except поднимает ту же самую ошибку, которую получает, он не обязателен. Такое поведение будет и без except. А если мы хотим поменять ошибку на свою, он нам может пригодиться. Чтобы было легче понять магию, его можно упростить:

manager = (EXPRESSION) enter = type(manager).__enter__ exit = type(manager).__exit__ value = enter(manager) try: TARGET = value SUITE finally: exit(manager, *sys.exc_info())

Что происходит? У экземпляра класса вызываем метод enter. В конструкции try выполняем код, который находится под with. Как бы ни отработал код, всегда вызываем метод exit из finally. И райзим ошибку, если генерируется исключение. Всё, никакой магии. Но кода с with стало в пять раз меньше.

Эта концепция прижилась и уже 17 лет является стандартом. Чтобы with мог работать с нашим классом, нужно реализовать два метода enter и exit, для асинхронной версии async with — aenter и aexit.

Как дела с contextmanager

Функция contextmanager из библиотеки contextlib, как и with, впервые появилась в Python 2.5. Ссылка на документацию.

В 2006 году в ней было три метода. С тех пор она значительно разрослась, и все ее методы помогают работать с менеджером контекста.

Сontextmanager — это еще один синтаксический сахар для работы с контекстом. У него есть асинхронная версия, и она практически ничем не отличается.

Если посмотреть код, вы увидите краткий и понятный докстринг:

def contextmanager(func): """@contextmanager decorator. Typical usage: @contextmanager def some_generator(): try: yield finally: This makes this: with some_generator() as : equivalent to this: try: =  finally: """ @wraps(func) def helper(*args, **kwds): return _GeneratorContextManager(func, args, kwds) return helper

Таким образом, contextmanager — это функция декоратор, который параметром получает генератор. В нашем случае эта функция для возврата значения используется yield вместо return. И генератор возвращает только одно значение. Функция contextmanager возвращает объект класса GeneratorContextManager. У этого объекта реализованы методы enter и exit. И его можно использовать как менеджер контекста с with.

Рекомендую посмотреть код класса _GeneratorContextManager и классов, от которых он был наследован. Код сложноват для новичка в программировании, но содержит много комментариев, поэтому разобраться в нем все же возможно.

От себя добавлю, что собачка в декораторе — это тоже питоновский синтаксический сахар. И иногда удобнее декорировать функцию не там, где мы ее определяем, а там, где ее вызываем. И тогда докстринг можно немного поправить:

def contextmanager(func): """@contextmanager decorator. Typical usage: def some_generator(): try: yield finally: This makes this: with contextmanager(some_generator)() as : equivalent to this: try: =  finally: """

Подведем итоги

Почти всегда синтаксический сахар или какая-нибудь магия под капотом имеет обычный и прагматичный код, который используется для повышения производительности разработчика, читаемости и качества кода. Лишний раз загляните в документацию или исходный код, чтобы понять, что делает та или иная конструкция. Знание таких вещей может пригодиться на собеседовании и поможет в поиске багов в коде.

Разработчики, которые используют синтаксический сахар, но не задумываются или не знают, как он работает, будут проигрывать в гибкости написания кода. В одних случаях использование синтаксического сахара будет обоснованно, в других использование измененного под свои нужды кода может дать необходимую гибкость.

beeline cloud — secure cloud provider. Разрабатываем облачные решения, чтобы вы предоставляли клиентам лучшие сервисы.

Источник

Читайте также:  Dynamic classes in css
Оцените статью