{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Стратегии контроля ошибок\n", "\n", "Как уже упоминалось, возникновение исключений в `python` не всегда является следствием логических ошибок программы. Нередко механизм исключений и их обработки используется для упрощения программы и повышения её надежности. \n", "\n", "## LBYL vs EAFP \n", "В этой связи часто рассматривают два кардинально разных подхода к контролю ошибок.\n", "\n", "- Первый из них --- [LBYL](https://docs.python.org/3/glossary.html#term-LBYL), который расшифровывается \"**L**ook **B**efore **Y**ou **L**eap\" (осмотрись, прежде чем прыгать), который предписывает перед попыткой выполнить какую-то операцию убедиться, что её выполнению ничто не помешает. При таком подходе в коде обычно встречается большое количество проверок `if`.\n", "\n", "- Второй из них --- [EAFP](https://docs.python.org/3/glossary.html#term-EAFP), который расшифровывается \"It's **E**asier to **A**sk **F**orgivness than **P**ermission\" (проще просить прощения, чем получить разрешение), который предписывает попытаться выполнить операцию в блоке `try` и обработать возможные исключения в блоках `except`.\n", "\n", "В `python` в большинстве ситуаций второй подход является предпочтительным. Разберем основные недостатки первого подхода.\n", "\n", "**Во-первых,** проверочный код может ухудшить читабельность программы и затруднить понимании основной её части, выполняющейся в отсутствии ошибок. \n", "\n", "В качестве примера, предположим, что мы хотим написать функцию безопасного вычисления функции $f(x) = \\sqrt{(x-1)(x-2)(x-3)}$, которая определена на множестве $[1, 2]\\cup[3,\\infty]$." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "from math import sqrt\n", "\n", "def f(x):\n", " return sqrt((x-1)*(x-2)*(x-3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вызов этой функции с аргументом вне области определения приведет к возникновению исключения `ValueError`." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "ename": "ValueError", "evalue": "math domain error", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_5124/3994200981.py\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mf\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m2.5\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_5124/2484835386.py\u001b[0m in \u001b[0;36mf\u001b[1;34m(x)\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mf\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 4\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0msqrt\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m-\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m*\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m-\u001b[0m\u001b[1;36m2\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m*\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m-\u001b[0m\u001b[1;36m3\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mValueError\u001b[0m: math domain error" ] } ], "source": [ "f(2.5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Реализуем функцию, которая будет возвращать $f(x)$, если $x$ из области определения и `None` иначе.\n", "\n", "````{panels}\n", "**LBYL**\n", "^^^\n", "```{code} python\n", "def safe_f(x):\n", " if x < 0:\n", " return None\n", " if 2 < x < 3:\n", " return None\n", " return sqrt((x-1)*(x-2)*(x-3))\n", "```\n", "При чтении такой реализации в стиле **LBYL** первое, что бросается в глаза, --- проверки на значение аргумента $x$, в то время как основной код оказывается скрытым где-то в конце функции. \n", "\n", "---\n", "**EAFP**\n", "^^^\n", "```{code} python\n", "def safe_f(x):\n", " try:\n", " return sqrt((x-1)*(x-2)*(x-3))\n", " except ValueError:\n", " return None\n", "```\n", "При подходе **EAFP** основной код располагается наверху функции, а исключительные ситуации располагаются ниже.\n", "````\n", "\n", "**Во-вторых,** проверочный код может дублировать действия, которые выполняются при совершении самой операции.\n", "\n", "В качестве примера рассмотрим метод [get](https://docs.python.org/3/library/stdtypes.html#dict.get) словарей и его возможный реализации. Напомним, что `get` возвращает значение по ключу, если такой ключ находится, и возвращает `None` иначе. \n", "\n", "````{panels}\n", "**LBYL**\n", "^^^\n", "```{code} python\n", "def get(d, k):\n", " if k in d:\n", " return d[k]\n", " else:\n", " return None\n", "```\n", "Заметим, что выполнение обеих инструкций `k in d` и `d[k]` приводит к проверке наличия ключа в словаре.\n", "---\n", "**EAFP**\n", "^^^\n", "```{code} python\n", "def get(d, k):\n", " try:\n", " return d[k]\n", " except KeyError:\n", " return None\n", "```\n", "Проверка на наличие ключа происходит только в операции `d[k]`.\n", "````\n", "\n", "Реализация в рамках подхода **LBYL** приводит к двухкратной проверке на наличие ключа в словаре, в то время как подход **EAFP** позволяет этого избежать.\n", "\n", "**В-третьих,** между проверкой на безопасность операции и началом выполнения операции многое может измениться. \n", "\n", "Такая ситуация может случиться в многопоточном приложении. Тут актуален и предыдущий пример со словарем: при подходе **LBYL** возможна ситуация, когда сразу после проверки на наличие ключа в словаре инструкцией `k in d`, другой поток удалит этот ключ, что приведет к возникновению исключения `KeyError`. Второй подход лишен такого недостатка, так как такой проверки вовсе не производится.\n", "\n", "Так же такая ситуация может возникнуть в приложения, работающих сетью. В таких приложениях принципиально нельзя убедиться, что операцию по сети удаться успешно завершить, т.к. не всё находится под контролем программы: сеть может в любой момент упасть. В таких ситуациях подход **LBYL** почти не заменим." ] } ], "metadata": { "interpreter": { "hash": "cd49b4596bae9c980ff74fdf93e8fe80e447435ae307c062fad6c4f9ef2eb47f" }, "kernelspec": { "display_name": "Python 3.8.10 ('venv': venv)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }