company_banner

Исключения в Python теперь считаются анти-паттерном

    Что такое исключения? Из названия понятно — они возникают, когда в программе происходит исключительная ситуация. Вы спросите, почему исключения — анти-паттерн, и как они вообще относятся к типизации? Я попробовал разобраться, и теперь хочу обсудить это с вами, хабражители.

    Проблемы исключений


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

    Исключения трудно заметить


    Существует два типа исключений: «явные» создаются при помощи вызова raise прямо в коде, который вы читаете; «скрытые» запрятаны в используемых функциях, классах, методах.

    Проблема в том, что «скрытые» исключения и правда трудно заметить. Покажу на примере чистой функции:

    def divide(first: float, second: float) -> float:
        return first / second
    

    Функция просто делит одно число на другое, возвращая float. Типы проверены и можно запустить что-то такое:  

    result = divide(1, 0)
    print('x / y = ', result)
    

    Заметили? На самом деле до print исполнение программы никогда не дойдет, потому что деление 1 на 0 – невозможная операция, она вызовет ZeroDivisionError. Да, такой код безопасен с точки зрения типов, но его все равно нельзя использовать.

    Чтобы заметить потенциальную проблему даже в таком максимально простом и читаемом коде, нужен опыт. Все что угодно в Python может перестать работать с разными типами исключений: деление, вызовы функций, int, str, генераторы, итераторы в циклах, доступ к атрибутам или ключам. Даже сам raise something() может привести к сбою. Причем, я даже не упоминаю операции ввода и вывода. А проверенные исключения перестанут поддерживаться в ближайшем будущем.

    Восстановление нормального поведения на месте невозможно


    Но именно на такой случай у нас же есть исключения. Давайте просто обработаем ZeroDivisionError, и код станет безопасным с точки зрения типов.

    def divide(first: float, second: float) -> float:
        try:
            return first / second
        except ZeroDivisionError:
            return 0.0
    

    Теперь всё в порядке. Но почему мы возвращаем 0? Почему не 1 или None?  Конечно, в большинстве случаев, получить None почти так же плохо (если даже не хуже), как исключение, но все же нужно опираться на бизнес-логику и варианты использования функции.

    Что именно мы делим? Произвольные числа, какие-то конкретные единицы или деньги? Не каждый вариант легко предусмотреть и восстановить. Может получиться, что при последующем использовании одной функции обнаружится, что потребуется другая логика восстановления.

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

    Нет серебряной пули, которая бы справилась с ZeroDivisionError раз и навсегда. И это мы ещё не говорим о возможности сложного ввода-вывода с  повторными запросами и таймаутами.

    Может быть, вообще не обрабатывать исключения именно там, где они возникают? Может быть, просто кинуть его в процесс исполнения кода — кто-нибудь потом разберется. И тогда мы вынуждены вернуться к сегодняшнему положению дел.

    Процесс выполнения неясен


    Хорошо, давайте понадеемся, что кто-то другой поймает исключение и, возможно, справится с ним. Например, система может запросить у пользователя изменить введенное значение, потому что нельзя делить на 0. И функция divide явно не должна отвечать за восстановление после ошибки.

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

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

    Предположим, в приложении есть два независимых потока: обычный, выполняющийся сверху вниз, и поток исключений, который выполняется как ему вздумается. Как прочитать и осознать такой код?

    Только с включенным отладчиком в режиме «ловить все исключения».


    Исключения, как пресловутое goto, рвут структуру программы.

    Исключения не исключительны


    Посмотрим на другой пример: обычный код доступа к удаленному HTTP API:

    import requests
    
    def fetch_user_profile(user_id: int) -> 'UserProfile':
        """Fetches UserProfile dict from foreign API."""
        response = requests.get('/api/users/{0}'.format(user_id))
        response.raise_for_status()
        return response.json()
    

    В этом примере буквально все может пойти не так. Вот неполный список возможных ошибок:

    • Сеть может быть недоступна, и запрос вообще не будет выполняться.
    • Может не работать сервер.
    • Сервер может быть слишком занят, наступит таймаут.
    • Сервер может потребовать аутентификацию.
    • У API может не быть такого URL.
    • Может быть передан несуществующий пользователь.
    • Может быть недостаточно прав.
    • Сервер может упасть из-за внутренней ошибки при обработке вашего запроса
    • Сервер может вернуть невалидный или поврежденный ответ.
    • Сервер может вернуть невалидный JSON, который не удастся распарсить.

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

    Как себя обезопасить?


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

    • Везде написать except Exception: pass. Тупиковый путь. Не делайте так.
    • Возвращать None. Тоже зло. В итоге либо придется почти каждую строку начинать с if something is not None: и вся логика потеряется за мусором очищающих проверок, либо все время страдать от TypeError. Не самый приятный выбор.
    • Писать классы для особых случаев использования. Например, базовый класс User с подклассами для ошибок типа UserNotFound и MissingUser. Такой подход вполне можно использовать в некоторых конкретных ситуациях, таких как AnonymousUser в Django, но обернуть все возможные ошибки в классы нереально. Потребуется слишком много работы, и доменная модель станет невообразимо сложной.
    • Использовать контейнеры, чтобы обернуть полученное значение переменной или ошибки в обертку и дальше работать уже со значением  контейнера. Вот почему мы создали проект @dry-python/return. Чтобы функции возвращали что-то осмысленное, типизированное и безопасное.

    Вернемся к примеру с делением, который при возникновении ошибки возвращает 0. Можем ли мы явно указать, что выполнение функции не прошло успешно, не возвращая конкретное числовое значение?

    from returns.result import Result, Success, Failure
    
    def divide(first: float, second: float) -> Result[float, ZeroDivisionError]:
        try:
            return Success(first / second)
        except ZeroDivisionError as exc:
            return Failure(exc)
    

    Заключим значения в одну из двух оберток: Success или Failure. Данные классы наследуются от базового класса Result. Типы упакованных значений можно указать в аннотации возвращаемой функцией, например, Result[float, ZeroDivisionError] возвращает либо Success[float], либо Failure[ZeroDivisionError].

    Что это нам дает? Больше исключения не исключительные, а представляют собой ожидаемые проблемы. Также оборачивание исключения в Failure решает вторую проблему: сложность определения потенциальных исключений.

    1 + divide(1, 0)
    # => mypy error: Unsupported operand types for + ("int" and "Result[float, ZeroDivisionError]")
    

    Теперь их легко заметить. Если видите в коде Result, значит функция может выдать исключение. И вы даже заранее знаете его тип.

    Более того, библиотека полностью типизирована и совместима с PEP561. То есть mypy предупредит вас, если вы попытаетесь вернуть что-то, что не соответствует объявленному типу.

    from returns.result import Result, Success, Failure
    
    def divide(first: float, second: float) -> Result[float, ZeroDivisionError]:
        try:
            return Success('Done')
            # => error: incompatible type "str"; expected "float"
        except ZeroDivisionError as exc:
            return Failure(0)
            # => error: incompatible type "int"; expected "ZeroDivisionError"
    

    Как работать с контейнерами?


    Есть два метода:

    • map для функций, которые возвращают обычные значения;
    • bind для функций, которые возвращают другие контейнеры.

    Success(4).bind(lambda number: Success(number / 2))
    # => Success(2)
    
    Success(4).map(lambda number: number + 1)
    # => Success(5)
    

    Прелесть в том, что такой код защитит вас от неудачных сценариев, поскольку .bind и .map не выполнятся для контейнеров c Failure:

    Failure(4).bind(lambda number: Success(number / 2))
    # => Failure(4)
    
    Failure(4).map(lambda number: number / 2)
    # => Failure(4)
    

    Теперь можно просто сконцентрироваться на правильном процессе выполнения и быть уверенным, что неверное состояние не сломает программу в неожиданном месте. И всегда есть возможность определить неверное состояние, исправить его, и вернуться обратно к задуманному пути процесса.

    Failure(4).rescue(lambda number: Success(number + 1))
    # => Success(5)
    
    Failure(4).fix(lambda number: number / 2)
    # => Success(2)
    

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

    Но как развернуть значения из контейнеров?


    Действительно, если вы работаете с функциями, которые ничего не знают про контейнеры, вам нужны именно сами значения. Тогда можно использовать методы .unwrap() или .value_or():

    Success(1).unwrap()
    # => 1
    
    Success(0).value_or(None)
    # => 0
    
    Failure(0).value_or(None)
    # => None
    
    Failure(1).unwrap()
    # => Raises UnwrapFailedError()
    

    Подождите, мы должны были избавиться от исключений, а теперь выясняется, что все вызовы .unwrap() могут привести к еще одному исключению?

    Как не думать об UnwrapFailedErrors?


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

    from returns.result import Result, Success, Failure
    
    class CreateAccountAndUser(object):
        """Creates new Account-User pair."""
    
        # TODO: we need to create a pipeline of these methods somehow...
    
        def _validate_user(
            self, username: str, email: str,
        ) -> Result['UserSchema', str]:
            """Returns an UserSchema for valid input, otherwise a Failure."""
    
        def _create_account(
            self, user_schema: 'UserSchema',
        ) -> Result['Account', str]:
            """Creates an Account for valid UserSchema's. Or returns a Failure."""
    
        def _create_user(
            self, account: 'Account',
        ) -> Result['User', str]:
            """Create an User instance. If user already exists returns Failure."""
    

    Во-первых, можно вообще не разворачивать значения в собственной бизнес-логике:

    class CreateAccountAndUser(object):
        """Creates new Account-User pair."""
    
        def __call__(self, username: str, email: str) -> Result['User', str]:
            """Can return a Success(user) or Failure(str_reason)."""
            return self._validate_user(
                username, email,
            ).bind(
                self._create_account,
            ).bind(
                self._create_user,
            )
    
       # ...
    

    Все сработает без каких-либо проблем, не вызовутся никакие исключения, потому что не используется .unwrap(). Но легко ли читать такой код? Нет. А какая есть альтернатива? @pipeline:

    from result.functions import pipeline
    
    class CreateAccountAndUser(object):
        """Creates new Account-User pair."""
    
        @pipeline
        def __call__(self, username: str, email: str) -> Result['User', str]:
            """Can return a Success(user) or Failure(str_reason)."""
            user_schema = self._validate_user(username, email).unwrap()
            account = self._create_account(user_schema).unwrap()
            return self._create_user(account)
    
       # ...
    

    Теперь данный код отлично читается. Вот как .unwrap() и @pipeline работают вместе: всякий раз, когда какой-либо метод .unwrap() завершается неудачей и Failure[str], декоратор @pipeline ловит её и возвращает Failure[str] в качестве результирующего значения. Вот так я предлагаю удалить все исключения из кода и сделать его действительно безопасным и типизированным.

    Оборачиваем все вместе


    Хорошо, теперь применим новые инструменты к примеру с запросом к HTTP API. Помните, что каждая строка может вызвать исключение? И нет никакого способа заставить их вернуть контейнер с Result. Но можно использовать декоратор @safe, чтобы обернуть небезопасные функции и сделать их безопасными. Ниже два варианта кода, которые делают одно и то же:

    from returns.functions import safe
    
    @safe
    def divide(first: float, second: float) -> float:
         return first / second
    
    
    # is the same as:
    
    def divide(first: float, second: float) -> Result[float, ZeroDivisionError]:
        try:
            return Success(first / second)
        except ZeroDivisionError as exc:
            return Failure(exc)
    

    Первый, с @safe, проще и лучше читается.

    Последнее, что нужно сделать в примере с запросом к API – добавить декоратор @safe. В итоге получится такой код:

    import requests
    from returns.functions import pipeline, safe
    from returns.result import Result
    
    class FetchUserProfile(object):
        """Single responsibility callable object that fetches user profile."""
    
        #: You can later use dependency injection to replace `requests`
        #: with any other http library (or even a custom service).
        _http = requests
    
        @pipeline
        def __call__(self, user_id: int) -> Result['UserProfile', Exception]:
            """Fetches UserProfile dict from foreign API."""
            response = self._make_request(user_id).unwrap()
            return self._parse_json(response)
    
        @safe
        def _make_request(self, user_id: int) -> requests.Response:
            response = self._http.get('/api/users/{0}'.format(user_id))
            response.raise_for_status()
            return response
    
        @safe
        def _parse_json(self, response: requests.Response) -> 'UserProfile':
            return response.json()
    

    Подведем итог, как избавиться от исключений и обезопасить код:

    • Использовать обертку @safe для всех методов, которые могут вызвать исключение. Она изменит тип возвращаемого значения функции на Result[OldReturnType, Exception].
    • Использовать Result как контейнер, чтобы перенести значения и ошибки в простую абстракцию.
    • Использовать .unwrap(), чтобы развернуть значение из контейнера.
    • Использовать @pipeline, чтобы последовательности вызовов .unwrap легче читались.

    Соблюдая эти правила мы можем сделать ровно то же самое — только безопасно и хорошо читаемо. Решены все проблемы, которые были с исключениями:

    • «Исключения трудно заметить». Теперь они обернуты в типизированный контейнер Result, что делает их совершенно прозрачными.
    • «Восстановление нормального поведения на месте невозможно». Теперь можно смело делегировать процесс восстановления вызывающей стороне. На такой случай есть .fix() и .rescue().
    • «Последовательность исполнения неясна». Теперь они едины с обычным бизнес-потоком. От начала и до конца.
    • «Исключения не являются исключительными». Мы знаем! И мы ожидаем, что что-то пойдет не так и готовы ко всему.

    Варианты использования и ограничения


    Очевидно, не получится использовать такой подход во всем вашем коде. Будет слишком безопасно для большинства повседневных ситуаций и несовместимо с другими библиотеками или фреймворками. Но важнейшие части вашей бизнес-логики вы должны писать именно так, как я показал, чтобы обеспечить правильность работы вашей системы и облегчить поддержку в будущем.

    Тема заставляет задуматься или даже кажется холиварной? Приходите на Moscow Python Conf++ 5 апреля, обсудим! Кроме меня там будет Артём Малышев – основатель проекта dry-python и core-разработчик Django Channels. Он расскажет еще больше интересного про dry-python и бизнес-логику.
    Конференции Олега Бунина (Онтико)
    469.43
    Конференции Олега Бунина
    Share post

    Similar posts

    Comments 148

      +39
      Что-то я не понял, в чем проблема с исключениями.
        +5
        Исключения, как пресловутое goto, рвут структуру программы.

        И это действительно так. В критическом ПО, например, они запрещены к использованию как и goto на уровне стандартов на кодирование.
          +13
          Все время возвращать ошибки — это адский ад.
            +6
            Зависит от языка. В Rust, например, result — это основополагающая часть всего IO, и вы всегда возвращаете результат (Result), либо вы пишите чистую функцию, в которой не может быть ошибки (например, вы всегда можете сравнить на равенство два int'а и вернуть True/False без result, но не можете этого сделать с float, потому что он partialOrder, т.е. есть числа, которые сравнивать нельзя (что больше — NaN или +Inf?).
              +9
              Я думал, мы про Python. Был неправ?
              Расшифрую: у разных языков могут быть разные подходы.
                –1

                Так в слайдах и показывают, как реализовать Result на питоне.


                Только смысл, если аннотация типов опциональная и любой дятел тебе может вернуть вместо Err() что попало, включая None.

                  +5
                  На мой вкус это очень хорошая фишка, исключения, в случае непредвиденных обстоятельств прыгнуть на верхний уровень цепочки вызовов. Иначе бы пришлось передавать результаты через всю цепочку вызовов.
                  Часто очень удобно, когда вызываешь какие-то библиотеки неизвестно, что и где пойдет не так, шансы что что-то пойдет не так очень велики. Тогда очень удобно поймать все исключения и напечатать информацию в лог например.
                  Для простых скриптов, вообще удобно ничего не делать в смысле обработки, а в случае исключения просто прочитать информацию об исключении и стек вызовов, когда скрипт завершится аварийно.
                  В противоположном случае, без исключений пришлось бы передавать результат через всю цепочку вызовов.
                  Или я что-то не так понимаю?
                    0
                    Статья про то, что при разработке большого ПО с требованиями по надёжности, надо, как правило, обрабатывать ошибки, не отдавая их «наверх», потому что «наверху» всего знать не могут.
                      +4
                      Мой подход другой. Наверх отдаются ошибки, с которыми неясно, что делать внизу. И это очень круто и удобно. Логику, с которй ясно, что делать, обрабатываем на месте. Неясно, отдаем наверх, разработчик решит, в зависимости от его ситуации.
                        +9
                        Так именно для этого исключения и существуют и именно так применяются.
                        А иначе может сложиться ситуация, когда программа не падает, при этом работает неправильно, но об этом никто не знает.
                          0
                          Программа не падает и работает с исключениями неправильно в одном случае, когда разработчик сделал
                          try:
                              # some code
                          except:
                              pass
                          

                          Но это не очень хороший код.
                          А в в других случаях программа упадет.
                          Добавил: пардон, кстати, полностью с Вами согласен. Невнимательно прочитал коммент.
                            +4
                            В статье автор предлагает
                            try:
                                return Success(first / second)
                            except ZeroDivisionError as exc:
                                return Failure(exc)
                            

                            В этом случае исключение перекладывается на разработчиков. Программа не падает, но какие гарантии, что разраб не забил на корректную обработку Failure(exc)? В этом случае и возможна ситуация, которую я описал.
                              0
                              Я там дописал в комменте, я полностью с Вами согласен.
                                0
                                А каким образом он может «забить» на обработку Failure(exc)?
                                  +1
                                  Да просто вообще не обрабатывать возвращаемое значение.
                                    0

                                    Нет, ну вот пример


                                    div = divide(10, 20);
                                    // что дальше?

                                    Просто я вижу тут два сценария:
                                    Первый, автор просто делает unwrap() и соглашается с паникой если значения нет.
                                    Второй, автор делает if div.Ok() ... тогда он обработал ситуацию. Заставить писать Else это конечно не заставит, но это не "забыл обработать", а совершенно сознательно проигнорировал.

                                      0
                                      А как такой пример?
                                      emailNewReport('20190101', '20190131', 'test@mail.com')
                                      

                                      Есть действия, которые в общем случае не возвращают какой-то результат. Они просто должны выполниться. Например, увеличить цены в базе на 10% — тут возвращать-то нечего. Если, например, база неедоступна, падаем и ловим исключение где это уместно. Или падаем совсем.
                                      А если вместо исключения получили результат, который проигнорировали — начинаем торговать в минус.
                                        0

                                        Ну тут должен помогать компилятор(интерпретатор?)


                                        error: unused `std::result::Result` that must be used
                                          --> src/main.rs:10:5
                                           |
                                        10 |     emailNewReport();
                                           |     ^^^^^^^^^^^^^^^^^
                                           |
                                          = note: this `Result` may be an `Err` variant, which should be handled

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


                                        Основная сила в том что по записи foo = bar() можно судить, может ли в этом месте произойти ошибка, и если да, то какая именно.

                                          +1

                                          Одним простым Result гарантировать в этом случае что-то действительно сложно.


                                          Но функциональщики (откуда всё это Result идёт) такие задачи решают по-другому. Во-первых, нет никаких функций, делающих действия. Есть только функции, возвращающие описания этих действий, и композиция этих функций. Во-вторых, в таком случае действия, потенциально могущие закончиться неудачей, можно представлять особым образом, с соответствующими требованиями к композиции.

                                          0
                                          Не согласен с «совершенно сознательно». if написал, а else: «потом напишу, и вообще надо не забыть у продакта спросить, а что делать если ноль пришёл»
                                            0
                                            Написал try {… } catch {} и такой «спрошу у продакта потом». В чем разница?
                                              0
                                              А зачем писать catch?
                                                0
                                                Чтобы потом не забыть поправить, конечно же.

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

                                Ошибки в обоих случаях отдаются наверх. Есть два паттерна передачи ошибки:


                                raise/except
                                result(Ok|Err)


                                Оба их них позволяют вернуть результат и сообщить об ошибке, оба из них избегают анти-паттерна magic value (-1 как ошибка). Но!


                                Если мы возвращаем результат, а ошибку raise'им, то может оказаться, что вышестоящий код забыл обработать эту ошибку. Мог, но не обработал. Это анти-паттерн, т.к. мы вынуждены использовать обработчик верхнего уровня (который не знает контекста). Важно, что при отладке может оказаться так, что exception'а ни разу не будет, а в редких случаях в продакшене будет.


                                Альтернатива: мы возвращаем Result, который обрабатывающий код обязан "unwrap". Если он его не unwrap, у него фейлится всё (в т.ч. код без ошибок), т.к. Result нельзя использовать напрямую (но можно вернуть!). Человек, который вынужден развернуть Result явно отвечает на вопрос, что делать с Err.


                                … точнее, такое происходит в Rust, который не скопилируется, если нет ответа. В Python можно просто проигнорировать, увы. Чтобы не дать проигнорировать, эта библиотека делает так, что "проигноировать" нельзя, надо ответить что делать:
                                а) Падать (явно)
                                б) Заменить результат на None
                                в) Вернуть err вверх по стеку.


                                Ключевая разница с raise/except в том, что "явное лучше неявного". Вызывая функцию мы не знаем, будет у нас raise или нет. Вызывая что-то с Result в качестве возвращаемого значения мы точно знаем, что "тут может быть ошибка" (а есть функции, в которых ошибки быть не может, например, def x(): return 42). Анти-паттерн в exception'ах в том, что они неявные. Глядя на функцию мы не можем предсказать, какие exception'ы она вызовет. Хуже, часто даже автор функции не знает этого (т.к. exception'ы могут быть в любом месте от любой функции).

                                  +4
                                  а есть функции, в которых ошибки быть не может, например, def x(): return 42

                                  RangeError: Maximum call stack size exceeded

                                    +1
                                    Это не ошибка функции, это ошибка вызывающего.
                                      +1
                                      Не важно. Это эксепшен, который может вылететь где угодно. И его надо уметь обрабатывать.
                                        0

                                        Кстати, отличный пример в моём споре с людьми про то, что чистые функции могут иметь side effects, т.е. лямбда-исчисление — очень грубая апроксимация работы компьютеров.


                                        В принципе, я согласен, что это "неожиданная ошибка". Но, вопрос: а что программа может сделать в такой ситуации?


                                        Вот у вас в коде:


                                        try:
                                           foo()
                                        except RecursionError:
                                          handle_recursion_error()

                                        Оно же сфейлится на вызове handle_recursion_error. Более того, если мы на пару уровней вверх прокинем, то там будет то же самое, потому что 2-3 вызова по стеку мы во время обработки ошибки всё равно сделаем, а на третьем нас будет ждать RE.

                                          0
                                          а что программа может сделать в такой ситуации?

                                          Много чего.
                                            0

                                            А в этой ситуации ничего в чистом коде делать не надо. Этим должен заниматься нечистый код.


                                            На самом деле это даже более общий паттерн. Тут на с. 155 и далее хорошо описана мотивация.

                                            +1
                                            Что делать с эксепшнов во время обработки эксепшна? Что делать, если процессор перегрелся во время очередного вызова функции? Что делать с космическими лучами, меняющими значение оперативной памяти?
                                              0
                                              Что делать с эксепшнов во время обработки эксепшна?

                                              Ловить уровнем выше.


                                              Что делать, если процессор перегрелся во время очередного вызова функции?

                                              Он сам знает что делать — троттлиться.


                                              Что делать с космическими лучами, меняющими значение оперативной памяти?

                                              Дублировать и экранировать.

                                                0
                                                Ловить уровнем выше.

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


                                                Конкретный пример у меня: был код, который вызывал некий код, все эксепшны которого наследовались от RpcException. Ну я ничтоже сумнящеся написал catch (RpcException ex). И все работало нормально, пока через полгодика другой коллега не обновил либу. И тоже все работало хорошо, но через какое-то время начало падать. После инвестигейта, занявшего какое-то время, стало понятно, что в новой версии добавился RpcUnknownException, который не наследуется от того, и который никак не обрабатывается. Вешать глобальный хэндлер "лови любые необработанные эксепшоны" мы не хотели, т.к. это маскировало бы фатальные ошибки, которые должны вести к крашу приложения (поведение "паника").


                                                Я бы очень хотел в таком месте получить ошибку компиляции, а не молчаливое "ну что ж, прокину тогда эксепшон выше".

                                                  0
                                                  Для этого существуют проверяемые исключения.
                                                    0
                                                    Проверяемые исключения это и есть что-то вроде резалтов, но более неудобное. Собственно, поэтому в джаве они особой славы не снискали.
                                                      0
                                                      Проверяемые исключения это и есть что-то вроде резалтов, но более неудобное.

                                                      Скорее наоборот.


                                                      Собственно, поэтому в джаве они особой славы не снискали.

                                                      Там бы и резалты не снискали. А объясняется всё просто: проблема с появлением неизвестного исключения в новой версии зависимости встречается настолько редко, и решается настолько просто, что оно для многих не стоит лишней писанины с перечислением всех исключений. Думаете ручная раскрутка стека в виде резалтов снискала бы популярность?

                                                        0
                                                        Скорее наоборот.

                                                        Я поработал и с тем, и с другим. Возможность `var foo = Bar()` и не гадать, может ли тут быть какая-то ошибка или нет бесценна.

                                                        Там бы и резалты не снискали.

                                                        Согласен. Потому что без нормальных АДТ и паттерн матчинга делать нечего.
                                                        Думаете ручная раскрутка стека в виде резалтов снискала бы популярность?

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

                                                        Вопросы нового исключения очень даже актуальны для любого разрабатываемого софта. Если конечно экосистема состоит из кучи либ с мажорной версией за десяток это менее актуально. Только вот кроме либ есть еще прикладной софт, и новый эксепшон может добавить парень в соседней команде, а не только либописатель.
                                                          –1
                                                          Возможность var foo = Bar() и не гадать, может ли тут быть какая-то ошибка или нет бесценна.

                                                          Проверяемые исключения позволяют вам не гадать.


                                                          В резалтах нет никакой раскрутки стека

                                                          Именно. Поэтому раскручивать приходится вручную.


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

                                                          Это необходимость в 90% случаев. Если вы, конечно не поклонник огромных функций и тривиальных приложений.


                                                          Только вот мы явно видим, что может пойти не так.

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

                                                            +1
                                                            Проверяемые исключения позволяют вам не гадать.

                                                            Это необходимость в 90% случаев. Если вы, конечно не поклонник огромных функций и тривиальных приложений.

                                                            И как понять, человек в этом месте забыл проверить ошибку или решил её прокинуть наверх?

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

                                                            Есть ошибки а есть панкики. В случае паники упасть часто наилучший способ, потому что делать какие-то действия в невалидном стейте опасно.

                                                            Еще одна проблема эксепшнов — плохая композиция. Например, если я хочу сделать 10 параллельных запросов, а потом сагрегировать результаты, мне надо надеяться, что автор веб-фреймворка предусмотрел возможность этого, и какой-нибудь `WhenAll` позволяет получить результаты и ошибки. А если нет, то упс.
                                                              –1
                                                              И как понять, человек в этом месте забыл проверить ошибку или решил её прокинуть наверх?

                                                              А как проверить подумал человек, когда писал код, или механически написал то, что от него потребовал компилятор?


                                                              Есть ошибки а есть панкики.

                                                              Есть лишь исключительные ситуации. А появление паник — следствие протекающей абстракции "ошибок".


                                                              В случае паники упасть часто наилучший способ, потому что делать какие-то действия в невалидном стейте опасно.

                                                              А давайте это вызывающий код будет решать опасно ему дальше работать или нет?


                                                              я хочу сделать 10 параллельных запросов, а потом сагрегировать результаты

                                                              Какое это имеет отношение к теме исключений?


                                                              мне надо надеяться, что автор веб-фреймворка предусмотрел возможность этого

                                                              И какое отношение имеет веб фреймворк ко многозадачности?

                                                                +1
                                                                А как проверить подумал человек, когда писал код, или механически написал то, что от него потребовал компилятор?

                                                                Потому что он явно должен это написать. Например, unwrap(), чтобы сказать «хрен с ней с ошибкой, дай значение». И это будет видно на ревью.

                                                                Есть лишь исключительные ситуации. А появление паник — следствие протекающей абстракции «ошибок».

                                                                Нет, есть «все плохо, но мы знаем что с этим делать» и «все плохо и мы не знаем, что делать». Это прицнипиально разные вещи. В тот же дуднете поэтому в стандартной либе есть Exception и ApplicationException, который является подмножеством первых.

                                                                А давайте это вызывающий код будет решать опасно ему дальше работать или нет?

                                                                Нет, не давайте.

                                                                Какое это имеет отношение к теме исключений?

                                                                Такое, что мне интересно, как исключения работают в таком случае. По-моему опыту, не очень хорошо.
                                                                  –1
                                                                  Потому что он явно должен это написать.

                                                                  Мою ремарку про механическое написание вы опять проигнорировали.


                                                                  Например, unwrap(), чтобы сказать «хрен с ней с ошибкой, дай значение». И это будет видно на ревью.

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


                                                                  Нет, есть «все плохо, но мы знаем что с этим делать» и «все плохо и мы не знаем, что делать».

                                                                  Это не вызываемому коду решать.


                                                                  Такое, что мне интересно, как исключения работают в таком случае.

                                                                  Отлично работают.

                                                                    0
                                                                    Мою ремарку про механическое написание вы опять проигнорировали.

                                                                    С тем же успехом можно механически писать throw new Exception() в рандомных участках кода.


                                                                    Это не вызываемому коду решать.

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


                                                                    Отлично работают.

                                                                    нет

                                                                      –1
                                                                      с которым сделать ничего нельзя

                                                                      Можно. Я выше приводил пример.

                                                                        0

                                                                        Пример нерелевантен. Зачем предпочитать менее удобный инструмент без монадической обработки и явного хэндлинга более удобному мне так и не стало понятно.

                                                                          0
                                                                          Я бы посмотрел, как вы будете объяснять монады пользователям экселя.
                                                                            0
                                                                            Питон создан для пользователей экселя? Что-то новое.

                                                                            А вообще объяснить и им тоже несложно. «Если ошибки нет, то выполнится вот это, иначе будет ошибка».
                                                                              –1
                                                                              На питоне можно написать эксель.

                                                                              Ага, они очень обрадуются, когда на ввод неверной формулы эксель аварийно завершится и похерит всю их работу.
                                                          0
                                                          А объясняется всё просто: проблема с появлением неизвестного исключения в новой версии зависимости встречается настолько редко, и решается настолько просто, что оно для многих не стоит лишней писанины с перечислением всех исключений.

                                                          Для правильно сделанных резалтов все варианты перечислять и не надо.

                                                            0

                                                            Как и для правильно сделанных проверяемых исключений.

                                                              +1

                                                              Да. Если к проверяемым исключениям добавить аппликативный и монадический интерфейс, нафигачить всяких там partition, isLeft, isRight и тому подобного, то получится Either.


                                                              Или можно просто сразу взять Either.

                                                      0
                                                      В Питоне вы бы сразу все поняли бы по логам. С первого падения.
                                                  0

                                                  Поэтому эти наши любимые чистые функциональщики разделяют синхронные экзепшоны и асинхронные экзепшоны. Синхронные экзепшоны — это ошибки в вашем коде, и они отличные кандидаты на то, чтобы выражаться в виде Either. Асинхронные экзепшоны — это когда стек кончился, или память кончилась, или процессор отключился, или наш тред решили прибить, и пора закругляться. Их не надо ловить (и в хаскеле в чистом коде их и не получится поймать, например).

                                                    0
                                                    Их не надо ловить

                                                    А давайте разработчик прикладного решения, сам решит надо их ловить или нет, а не разработчик какой-то библиотечки где-то в глубине зависимостей? Вы когда-нибудь делали настраиваемую пользователем логику? Систему подключаемых плагинов? Да хотя бы многозадачный веб-сервер?

                                                      0
                                                      +1. Ловить нужно все, чтобы записать в логи. Ну или в простейших случаях Python упадет и все напишет сам.
                                                        0
                                                        Я как раз про это и говорю: в библиотечном чистом коде этого делать не надо (если, конечно, это не цель библиотеки — работа с такими исключениями).

                                                        Делал всё из этого. Веб-сервера даже на хаскеле.

                                                        А, понял. Я зря так сформулировал — их не надо ловить в чистом коде с бизнес-логикой, условно говоря. Они будут отлавливаться где-то высоко, в effectful-коде.
                                                      0

                                                      Занятно. Можно ещё попробовать обработать ситуацию когда ваше приложение убил OOM-киллер.
                                                      Напоминает дискуссию про предложенные новые исключения в c++.

                                                  0
                                                  то может оказаться, что вышестоящий код забыл обработать эту ошибку

                                                  Это как? Слова я понимаю, но представить как это сделать в Python не могу. Возможно я что-то не знаю?
                                                    0
                                                    Как забыл, и как заставить вышестоящего по коду обработать ошибку?
                                                    0
                                                    И тут на сцену выходят проверяемые исключения :D
                                                      0
                                                      Кстати, да. Вроде как сейчас все уже поняли, что исключения должны быть непроверяемые. Об этом чётко написано в книге Боба Мартина «Чистый код». Но непонятно, как в этом случае узнать другому программисту, что некий участок кода может выдать исключение, если это исключение явно не описывается в сигнатуре функции?
                                                        0

                                                        Мартин — чокнутый идеалист, возводящий в абсолют простые практики, понятные неподготовленной аудитории. Процентов на 60 написанного в книге стоит смотреть с (не)здоровой долей скепсиса.
                                                        Отказываясь от проверяемых исключений, вы отказываетесь от автоматический проверки корректности обработки ошибок при обновлении библиотек, например, и никакие @throws вам не помогут.

                                                  +4

                                                  Так в статье именно передача наверх. Да, без исключений, но наверх. Так в чем польза передать наверх сверх result вместо того, что бы передать наверх через исключение? Если у нас опять вложенных функций по дороге, не так-то уж это удобно.

                                                    –2
                                                    Есть паттерн, когда вы передаёте ошибку без обработки: If Err: return Err.

                                                    А разница в том, что exception рейзится где попало, а result — он всегда на выходе функции и надо явно ответить «что с этой ошибкой делать».

                                                    Фактически, исключения, это такой Result, для которого поведение по умолчанию (если ничего не сказано — передать дальше). Проблема в том, что это умолчание неявное и оно строго нарушает дзен питона «явное лучше неявного».
                                                      –1

                                                      На один уровень наверх, а не "кто его знает куда" наверх.
                                                      С необходимостью на этом уровне тоже принять решение что делать с ошибкой.

                                                        +2

                                                        Это если на один уровень вверх она ловится. Это совсем не обязательно. Недавно писал модуль, который разбирает эксель. Бросал исключения, если выяснялось, что файл ошибочен. Зачем мне ловить исключение на уровень выше? С ним там нечего делать.


                                                        Решение принимает самый верхний уровень модуля — файл разобран быть не может, отказ. По стеку это 2-3 уровня вверх. Передавать статусы на эти два уровня было бы неудобно, не вижу ни одной причины.

                                            0
                                            Вы это Go-шникам попробуйте объяснить :)

                                          0
                                          Их не умеют, не хотят, лень.
                                            0
                                            Вот тут более подробно можно почитать:

                                            www.lighterra.com/papers/exceptionsharmful

                                            Вкратце: неявность (можно легко отстрелить ногу, забыв покрыть где-нибудь какой-то случай), плохо подходит к method-chain вызовам методов (потому что эксепшны хорошо работают со statement, но плохо с expression), как следствие не очень дружит с многопотоком.

                                            Но очень рекомендую ознакомиться со статьей. Кстати, название статьи слегка намекает на сходство с goto.
                                            +4
                                            Can I divide by zero in your code?
                                              0
                                              Это отсылка к чему-то?
                                                –1
                                                Это отсылка к чему-то?

                                                Несколько слоёв.

                                                ====

                                                По теме статьи: чем в принципе плоха практика проверки типа полученных откуда-то из вне (например, от пользователя) данных через try catch c приведением типов?

                                                Что-то вроде
                                                try
                                                {
                                                a = Int(полученные данные)
                                                }
                                                catch (er)
                                                {
                                                a = 0;
                                                }
                                                
                                              +64
                                              Исключения в Python теперь считаются анти-паттерном

                                              Можно ссылку на официальную позицию? А то какой-то кликбейт. Если они считаются антипаттерном по мнению Никиты Соболева — никаких проблем, но стоит это явно указать.

                                                +5
                                                Да, название пугающее. Провоцирует все бросить, забиться в угол и плакать.
                                                  +4

                                                  Можно ещё форматирование строк через % назвать антипаттерном. А то я предпочитаю format. Или тернарные операции назовём антипаттерном. Они маскируют условия под операции присвоения. А ещё можно использование lambda объявить антипаттерном. Чисто поржать.


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

                                                  +17

                                                  Я постоянно это повторяю, и повторю снова: Exception'ы идеально подходят для bad path.


                                                  Три варианта работы программы:


                                                  • happy path (хорошо на входе, хорошо на выходе)
                                                  • sad path (плохо, но мы знаем, что с этим делать)
                                                  • bad path (плохо и мы не знаем, что с этим делать)

                                                  Уничтожение bad path (перевод его в sad path) — это процесс "maturing code", перевода его в продакшен. Однако, важно, bad path всегда остаётся.


                                                  Хотите пример?


                                                  a=0
                                                  b=1
                                                  while True:
                                                    if a+1 == b:
                                                       happy()
                                                    else:
                                                      wtf()

                                                  Вопрос: Если это однопоточное приложение и happy() — чистая функция, wtf когда-либо вызовется? Согласно теории типов — нет. На практике — флипнется бит в памяти и когда-нибудь оно случится. Или while True закончится.


                                                  Вот на такие случаи и нужны exception'ы.

                                                    +1
                                                    а как на счет StopIteration? Warning?
                                                    на питоне нормально для control path применять,
                                                    только нужно создавать marked Exceptions а не тыкать везде raise Exception
                                                      0
                                                      На практике — флипнется бит в памяти и когда-нибудь оно случится. Или while True закончится.

                                                      На практике компилятор заметит, что не случится, и соптимизирует проверку. Правда, не в питоне.

                                                      +45
                                                      За такие заголовки надо бить. Если где то у вас в Go или другом языке есть сложившийся подход, не нужно его тянуть в питон. Подходы приведенные в Вашем примере не совместимы с существующими практиками, а решаемые проблемы выдуманы.
                                                        +6
                                                        +1.
                                                        Если действительно у человека есть потребность писать код, который должен работать как часы даже в случае ядерной войны, то для этого лучше выбрать язык со статической типизацией. Например, Rust или Haskell, которые уже упомянули в комментах.
                                                        В тех же областях, для которых питон является хорошим выбором, исключения являются вполне удобным и разумным подходом.
                                                          0
                                                          Для Haskell нужно скилов больше чем для python) Иначе он будет работать не в 2 раза медленнее чем c++ а в 2 раза медленнее python…
                                                            +2
                                                            Ну, раз уж мы говорим о ПО с очень высокими требованиями к отказоустойчивости и предсказуемости, то очевидно, что это предполагает определённый уровень программистов.
                                                            Собственно, именно это я и хотел донести своим комментарием — есть ПО для интернет-магазинов и ПО для атомных реакторов.
                                                            В одном случае допустимо выдать страничку «что-то пошло не так, попробуйте позже», но разработка должна быть быстрой, код ясным, а программисты — легкозаменяемыми. И здесь питон идеален.
                                                            А во втором случае можно долго и тщательно писать и отлаживать код, а программисты могут быть штучным товаром. И нет никакого смысла тащить практики, относящиеся ко второму случаю, в язык, предназначенный для первого случая.
                                                              –1
                                                              На самом деле одно не исключает другого в сегодняшнем питоновском подходе. Просто, нужна надежность, пишем тесты, отлаживаем, ловим все нужные исключения. И все.
                                                              С другой стороны, нужно быстро набросать скрипт — пишем без всякой обработки ошибок. В случае ошибки читаем, что выбрасывает питон и чиним. Быстро и хорошо.
                                                                +2
                                                                Вот да, тесты и обработка исключений — это всё хорошо вписывается в то, чем является питон.
                                                                Я против только тех подходов, которые в питоне не нужны. А если для конкретной задачи они необходимы — то для этой задачи изначально не стоило брать питон.
                                                              0

                                                              Нет, это не так. На хаскеле относительно легко писать производительный код, и совсем легко писать код, который будет быстрее питона (особенно питона, не лезущего в С). ghc творит чудеса, если специально не вставлять ему палки в колёса ради интереса.

                                                            +1
                                                            ага, такое чувство, что style guides и python way не читай,
                                                            давайте питон в го в жабу или в спп превратим (или хаскель)
                                                            +41
                                                            Я вот из всех докладов и статей Соболева не могу понять, зачем он продолжает писать на питоне и пытается «кормить лошадь углём и запрягать в паровоз».
                                                              +16
                                                              Ощущение что чуваки увидели rust и поняли что до этого занимались какой-то фигней :)
                                                                +9
                                                                Так Result, Success, Failure — это же Either из Haskell. И это давно есть во всяких PyMonad и прочих подобных.
                                                                  +1
                                                                  В Swift есть Optionals, это встроенная в язык монада. Очень-очень удобно!

                                                                  А директива safe напомнила async/await из JS, тоже такой способ скрытно работать с контейнерами.

                                                                  В Haskell есть какая-то похожая штука для неявной работы с контейнерами? inline-разворачивания, так сказать?
                                                                    0
                                                                    В Haskell есть какая-то похожая штука для неявной работы с контейнерами? inline-разворачивания, так сказать?

                                                                    Конечно — трансформеры монад. Делаем всякие разные грязные действия, возвращающие Either (Result в статье), если хотя бы одно вернёт Left (Failure), игнорируем все последующие и возращаем этот Left. При этом локальные присваивания игнорируют Either (Result) и всегда думают, что им вернули Right (Success). Если не вернули — смотри выше.
                                                                      0
                                                                      В Haskell есть какая-то похожая штука для неявной работы с контейнерами? inline-разворачивания, так сказать?


                                                                      Если я правильно здесь понимаю «разворачивание», это do-нотация:

                                                                      create :: Text -> Text -> Maybe User
                                                                      create username email = do
                                                                          user <- validate username email
                                                                          account <- createAccount user
                                                                          createUser account
                                                                      


                                                                      Если после какой-нибудь распаковки (<-) получится Nothing, в итоге будет Nothing, и оставшиеся функции выполняться не будут, иначе create вернёт последнее выражение. Это на самом деле всё тот же оператор bind (>>=), только немного по-другому записанный.
                                                                    +2
                                                                    Об этом было давно сказано www.joelonsoftware.com/2003/10/13/13
                                                                      +6
                                                                      Серъёзная заявка на победу. В смысле очень большие изменения в языке. Выглядит как попытка затащить монадки в язык, который к этому не приспособлен.

                                                                      Мне кажется, что если и продвигать такие изменения, то через PEP и изменения конструкций языка, а не через стороннюю библиотеку. Но что-то мне подсказывает, что наследники Гвидо такое не одобрят.
                                                                        0
                                                                        Так многие дополнительные фичи в питоне вынесены в отдельные модули (те же интерфейсы, абстрактные классы). Ну и ок, кому надо, тот юзает. Менять синтаксис языка из-за этого никто не собирается — большую часть населения он и так устраивает.
                                                                        +2

                                                                        Найди десять отличий:


                                                                        from result.functions import pipeline
                                                                        
                                                                        class CreateAccountAndUser(object):
                                                                            """Creates new Account-User pair."""
                                                                        
                                                                            @pipeline
                                                                            def __call__(self, username: str, email: str) -> Result['User', str]:
                                                                                """Can return a Success(user) or Failure(str_reason)."""
                                                                                user_schema = self._validate_user(username, email).unwrap()
                                                                                account = self._create_account(user_schema).unwrap()
                                                                                return self._create_user(account)
                                                                        
                                                                           # ...

                                                                        from result.functions import pipeline
                                                                        
                                                                        class CreateAccountAndUser(object):
                                                                            """Creates new Account-User pair."""
                                                                        
                                                                            def __call__(self, username: str, email: str) -> User:
                                                                                """Can return a User or throws an Error(str_reason)."""
                                                                                user_schema = self._validate_user(username, email)
                                                                                account = self._create_account(user_schema)
                                                                                return self._create_user(account)
                                                                        
                                                                           # ...
                                                                          +33
                                                                          Автор имейте совесть. Вы хоть представляете сколько людей за валерьянкой пошло.
                                                                            +12

                                                                            Один вопрос. Вы пробовали это использовать?


                                                                            Навскидку, вижу проблемы с тем, что 1) стандартная библиотека не поддерживает ваш подход; 2) декоратор @pipeline может сработать неправильно в каких-то непредусмотренных сложных случаях; 3) если использовать вашу библиотеку в той мере, в какой в идеале нужно (т.е. везде), поток управления будет очень напоминать поток при обработке исключений, только обработку исключений разработчики языка могут как-то оптимизировать, а в вашей библиотеке "лапша" управления так и останется.


                                                                            Очень похоже на scala.util.Try, только в человеческой обёртке.

                                                                              +3
                                                                              1 + divide(1, 0)
                                                                              # => mypy error: Unsupported operand types for + ("int" and "Result[float, ZeroDivisionError]")

                                                                              Ок, для одного вызова за раз это легко анвраппится, а как насчет чего-то такого?
                                                                              x = y(z(4, pi) * f(j, l, n)) + p(k) / d(z) + 42

                                                                              Аналогичный вопрос про любые похожие ситцуации в коде, не связанном с арифметикой
                                                                                0
                                                                                Если я правильно понял — завернуть всё в pipeline и везде где мы получили эти переменные j, l, n, k, z получать их unwrap'ом. Тогда в самих переменных будут честные int'ы, но если хотя бы одна завалится, то pipeline тормознёт вычисления.
                                                                                  +3
                                                                                  >Тогда в самих переменных будут честные int'ы, но если хотя бы одна завалится, то pipeline тормознёт вычисления.
                                                                                  Это вы только что описали принцип работы исключений :)
                                                                                    0
                                                                                    Что насчёт такого?
                                                                                    def foo(a, b, dictionary, host):
                                                                                        j = divide(a / b)
                                                                                        l = http.get(host).parseSmth()
                                                                                        k = dictionary[l]
                                                                                        m = divide(k / j)
                                                                                        ...
                                                                                    

                                                                                    Здесь у нас могут быть разные исключения — арифметика, сеть, отсутствует ключ. Либо мы вешаем на весь блок «except Exception», либо делаем серию except'ов на каждый тип исключения, либо вешаем персональный try на каждый опасный вызов. Второй и третий способ раздувает код и делает его нелинейным, первый я не приемлю, потому что слишком общий. Монада Result добавляет один декоратор за пределами тела функции и по .unwrap() в конец каждой строки.
                                                                                    Вторая проблема — два опасных divide в одной функции. Допустим мы поставили общий «except ZeroDivisionError», но теперь мы не знаем, какой из них бросил исключение. Монада Result позволяет перед вызовом unwrap() сделать rescue() и добавить подробное описание.
                                                                                      0
                                                                                      Можно в одном блоке try except ловить несколько типов исключений.
                                                                                        0
                                                                                        Это то что я назвал вторым способом. При этом мы теряем информацию, в каком конкретно вызове произошло исключение и должны поддерживать несколько обработчиков, что затрудняет чтение и добавляет сложности.
                                                                                          0
                                                                                          >в каком конкретно вызове произошло исключение
                                                                                          Разве в питоне нет стектрейса?
                                                                                            0
                                                                                            Ну не знаю. Когда я вызываю пишу свой код и использую вызов функции из сторонней библиотеки, при получении исключения, мне в порядке убывания важности:
                                                                                            1) Важно то, что я не получил результат. Группируем все ексепшены и в одном месте делаем то, что нужно при неполучении результата. Отлично!
                                                                                            2) Кроме неполучения результата, мои действия могут отличаться в зависимости от того, какое это исключение. Группируем ексепшены по вариантам моих действий и обрабатываем так как нужно в каждом случае. Опять отлично!
                                                                                  +2
                                                                                  Ммм монада Maybe или Either
                                                                                    +6

                                                                                    Возьми любой функциональный язык и не мучайся.
                                                                                    Любишь красивые теоретические конструкции — Haskell.
                                                                                    Практический код — Scala.
                                                                                    Хочется императивщины с полуручным управлением памятью — Rust.
                                                                                    Питон, он для другого. В нем вся эта история будет жуткими костылями.

                                                                                      +1
                                                                                      Можно далеко не отходить даже, есть Lisp компилирующийся в байткод Python — Hy.
                                                                                      –4
                                                                                      Обычная дилемма: или даёшь блёклый заголовок, и тогда народ тупо проходит мимо, или провоцируешь публику и получаешь порцию яда в комментах.

                                                                                      Что касается самой идеи, то она реально зачётная. И, что бы ни говорили, стопроцентно питонская. Сам недавно перепилил функцию, возвращающую булево на функцию, возвращающую кортеж в стиле (результат, почему_нет). Декоратор «safe» — просто чудо. Хотел бы или нет я это видеть в стандартной библиотеке — не уверен, но как приёмчик, который в случае чего применить в своё удовольствие — очень достойно. Спасибо.
                                                                                        +6
                                                                                        всегда хорошо когда-что либо делаешь пользоваться зравым смыслом

                                                                                        притягивать за уши монады в Python которые только усложняют, в статье не видно чтобы они решили что-либо

                                                                                        на много понятней сразу словить исключения при которых можно восстановиться при помощи обычных try except сразу после вызова критичной функции чем прятать то же самое в fix/rescue в виде кучи isinstance(state, someError)

                                                                                        def fix(state: Exception) -> Result[int, Exception]:
                                                                                            if isinstance(state, ZeroDivisionError):
                                                                                                return Success(0)
                                                                                           ....
                                                                                            return Failure(state)
                                                                                          +1

                                                                                          Зачем Javа из Python делать? Если вам типов не хватает, то не надо Python коверкать, он не для того создавался. Python — для быстрых расчетов на скорую руку, а для продакшена используйте Java.

                                                                                            +3
                                                                                            >Исключения считаются антипаттерном
                                                                                            >Останов итерации for реализован на основе исключений
                                                                                            Да ладно?
                                                                                              +3
                                                                                              Исключения выпиливают, goto запрещают, мьютексы не рекомендуют, ООП устарело, циклы это для олдфагов… мама, роди меня обратно…
                                                                                                +5
                                                                                                Тот случай, когда жалеешь, что ты на Хабре только читатель с r/o, и не можешь поставить минус за такой заголовок.
                                                                                                Это самый натуральный кликбейт.
                                                                                                  0
                                                                                                  Никто исключение из питона не выпиливает,
                                                                                                  более того в питоне нормально использовать исключения для Control Flow
                                                                                                  (что не рекоммендовано в большинстве языков типа Cpp, Java, C#)
                                                                                                  посмотрите на Warning, UserWarning, DeprecationWarning
                                                                                                  или тем более на StopIteration exceptions.

                                                                                                  по началу мне тоже не хватало switch, pattern matching и прочего.
                                                                                                  но у питона свой путь, и тащить сюда вещи из других языков не надо.
                                                                                                  Как на счет фигурных скобок?
                                                                                                    0

                                                                                                    Скажите пожалуйста, как на C++/C#/Java поступают с исключениями? Если их не рекомендуют для Control Flow, что используют вместо этого? Я сам просто в основном Python знаю...

                                                                                                      0
                                                                                                      Насколько я ничего не знаю, на C++ не рекомендуют исключения для Control Flow в первую оченедь из-за:
                                                                                                      1) их реализации, очень медленные.
                                                                                                      2) Проблем с освобождением памяти в случае исключений.
                                                                                                      На Python обе причины не существуют для исключений.
                                                                                                        0
                                                                                                        1) их реализации, очень медленные.
                                                                                                        Нет, с++ «zero cost exceptions», в некоторых тестах с++ код с исключениями работает быстрее чем «возврат ошибки», если их использовать как исключения.
                                                                                                    +2
                                                                                                    Похоже, скоро фразы со словом «антипаттерн» внутри — станут антипаттернами.
                                                                                                      0

                                                                                                      Примеры хорошо выглядит пока вычисления последовательные. А если у меня такой код
                                                                                                      try:
                                                                                                      a = canFail1()
                                                                                                      b = canFail2()
                                                                                                      return canFail3(a, b)
                                                                                                      except SomeError:
                                                                                                      return None

                                                                                                        0
                                                                                                        let res = do {
                                                                                                           let a <- canFail1()
                                                                                                           let b <- canFail2()
                                                                                                           let result <- canFail3(a,b)
                                                                                                           result
                                                                                                        }
                                                                                                        return result.ok() // Just(result) or None
                                                                                                          0
                                                                                                          А если canFail3 принимает Int и String а не Result?
                                                                                                            +2
                                                                                                            a и b в данном случае это и есть int и String.

                                                                                                            Если дописать бойлерплейта, то примерно так.

                                                                                                            С do-монадой или try-блоками выглядит сильно лучше, но идеологически все ровно то же самое. Тут можно поиграться с условиями (true/false).
                                                                                                            –1
                                                                                                            У вас если canFail1 вернуло ошибку canFail2 всё равно выполнится.
                                                                                                              0

                                                                                                              Почему это? Все развернется в цепочку >>=, которая остановится на первой ошибке. Только let'ы зря написал, между языками если часто переключаться можно случайно запутаться без поддержки иде.


                                                                                                              res = do 
                                                                                                                 a <- canFail1()
                                                                                                                 b <- canFail2()
                                                                                                                 result <- canFail3(a,b)
                                                                                                                 return result
                                                                                                                0

                                                                                                                Я видимо плохо знаю питон и не совсем понимаю что делает оператор <-

                                                                                                                  0

                                                                                                                  На питоне это будет примерно


                                                                                                                  res=canFail1()
                                                                                                                    .flatMap(lambda a: 
                                                                                                                      canFail2().map(lambda b:
                                                                                                                        canFail3(a,b)
                                                                                                                      )
                                                                                                                    )
                                                                                                                    0

                                                                                                                    Да но этот код выглядит хуже чем код с try… catch

                                                                                                                      0
                                                                                                                      Согласен. Потому что нет языковой поддержки. С поддержкой получится как в примере 1, который вполне читаем и удобен. Особенно учитывая, что можно комбинировать по всякому. Явное >> неявного, рассчитывать что кто-то выше по стеку поймает и сделает что нужно часто вредно. Только на прошлой неделе я словил баг, связанный с этим. У меня есть реббит, и некоторые исключения являются бизнес-ошибками, из-за чего нужно переложить сообщение в deadletter, либо ошибкой нижнего слоя, тогда нужно сделать NackWithRequeue и попробовать позже еще раз. И у меня одно исключение не обрабатывалось, и не попадало ни туда, ни туда.

                                                                                                                      Было бы приятнее получить от компилятор ошибку «ты забыл вот эту ошибку обработать», когда она добавилась.

                                                                                                                      Теперь я пишу тесты. Но тесты всегда хуже проверок типов.
                                                                                                          –1
                                                                                                          Срочно переходите на Go!!!
                                                                                                          Вам понравится.
                                                                                                            0
                                                                                                            Скоро питон превратят в яву и колесо повернется на следующий круг.
                                                                                                              +1
                                                                                                              Эмм… Я ненастоящий сварщик, но что неправильного в таком примере кода? Вроде я все вылавливаю. И в логах потом все хорошо видно. У меня куча сетевых запросов разных и очень часто я какую-то ересь получаю от сервера. Мне предпочтительнее передать дальше None в любой непонятной ситуации, если данные собраны не были.

                                                                                                              def scan_nmap(hostname, host_ip):
                                                                                                                  logger.debug('Timestamp: {} Hostname: {} Port: N/A Action: Nmap_scan Message: Nmap scan started'.format(datetime.datetime.now(), hostname))
                                                                                                                  nm = nmap.PortScanner()
                                                                                                                  try:
                                                                                                                      nm.scan(hosts=str(host_ip))
                                                                                                              
                                                                                                                  except Exception as error:
                                                                                                                      logger.error('Timestamp: {} Hostname: {} Port: N/A Action: Nmap_scan Message: {}'.format(datetime.datetime.now(), hostname, error))
                                                                                                                      list_http_ports = None
                                                                                                                      str_nmap_port_scan_result = ''
                                                                                                                      return list_http_ports, str_nmap_port_scan_result
                                                                                                              
                                                                                                                0

                                                                                                                По сути статьи мне сказать нечего… Мне кажется, каждый инструмент имеет область применения. Сложно придумать правила на все случаи жизни. Копья по поводу исключений ломаются десятилетиями...


                                                                                                                Конкретно по поводу вашего кода могу сказать, что читается очень тяжело. Эти громоздкие строки логирования и названия переменных… Я считаю, что писать тип переменной в ее названии (list_, str_) — антипаттерн. Названия становятся громоздкими и менее читаемыми. А от отсутствия проверки типов это все равно не спасет.


                                                                                                                И еще, вместо logger.error лучше использовать logger.exception — он сразу напечатает исключение и traceback.

                                                                                                                  0
                                                                                                                  Хм. Спасибо. Попробую учесть. С логгированием в плане громоздкости у меня вариантов нет. Мне надо точно знать где и что у меня произошло в процессе.

                                                                                                                  Насчет str_, list_ — как-то всегда казалось удобным. Попробую отрефакторить аккуратно.

                                                                                                                  А как logger.exception работает? Мне надо не просто ошибку вывалить, а показать, что «При попытке получить сертификат шифрования с 2443 порта сервера example.com произошла ошибка: 'Сервер вернул полтора арбуза вместо сертификата' „
                                                                                                                  0
                                                                                                                  +1 Передача None или пустых данных при ошибке это практически негласный стандарт.

                                                                                                                  По поводу кода, я бы сказал что объявлять переменные внутри try/except это немного опасно неопределённостью. Но если их тут-же возвращать, то их и объявлять не надо.

                                                                                                                  почему бы сразу не написать
                                                                                                                  return None, ""
                                                                                                                  
                                                                                                                    0
                                                                                                                    Черт, логично, спасибо.
                                                                                                                  0
                                                                                                                  Выглядит как попытка переизобрести колесо. То ли монады, то ли условия/рестарты из Common Lisp (и оно же на русском)
                                                                                                                    +4
                                                                                                                    Исключения, как пресловутое goto, рвут структуру программы
                                                                                                                    Вы так и до yield, if/else, __call__ дойдете, исключения не рвут, а передают управление назад «родителю».

                                                                                                                    Но важнейшие части вашей бизнес-логики вы должны писать именно так, как я показал, чтобы обеспечить правильность работы вашей системы и облегчить поддержку в будущем
                                                                                                                    Ключевое отличие которое дает ваш «фреймворк» — это заглушить/игнорировать исключение если оно не обработано, в отличие от стандартного поведения — передать исключение наверх, если оно не обработано.
                                                                                                                    И вот как раз этот подход (игнорирования исключений) ведет к не правильной работе системы.

                                                                                                                    Исключения трудно заметить
                                                                                                                    И действительно, если вы глушите исключения, то вы их не заметите, и не узнаете, что ваша система работает не правильно.
                                                                                                                      –3
                                                                                                                      придется почти каждую строку начинать с if something is not None


                                                                                                                      Так и не начинайте. Пишите как все
                                                                                                                      if errors:
                                                                                                                          do_something()
                                                                                                                      
                                                                                                                      или
                                                                                                                      not errors and happy()
                                                                                                                      
                                                                                                                      или
                                                                                                                      errors and logging.error("This")
                                                                                                                      
                                                                                                                        +1
                                                                                                                        Такие проверки посчитают ошибкой не только None, но и False, 0, "", (), [], {}…
                                                                                                                          –2
                                                                                                                          И это прекрасная (и легко читаемая) проверка на наличие данных. Хотите типизировать — используйте docstring.
                                                                                                                            0
                                                                                                                            Это ужасная проверка в ситуации, когда надо различать ситуации пустых данных и произошедшей ошибки.

                                                                                                                            В обсуждаемом примере с делением divide(0,1)=0 и это нормально, а divide(1,0)=None и должно быть обработано как исключительная ситуация. Какой такой docstring вам здесь поможет?
                                                                                                                              –2
                                                                                                                              И только если вы работаете с целыми числами, вам нужно писать проверку на ноль а не None.

                                                                                                                              Если вы работаете со структурами или строками, то извините, 0 вам не вернётся никогда.
                                                                                                                                0

                                                                                                                                Так все-таки, что должен вернуть вызов divide(1,0)?

                                                                                                                                  0
                                                                                                                                  inf :D
                                                                                                                                    0
                                                                                                                                    Ошибку тайпчекера.
                                                                                                                                      0
                                                                                                                                      Это вариант для операции деления. А функция divide по условию должна как-то обработать все возникшие ошибки и вернуть результат.

                                                                                                                        Only users with full accounts can post comments. Log in, please.