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

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


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

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


Существует два типа исключений: «явные» создаются при помощи вызова 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 и бизнес-логику.

Комментарии (133)


  1. evgenyk
    26.03.2019 13:25
    +7

    Что-то я не понял, в чем проблема с исключениями.


    1. j8kin
      26.03.2019 13:29

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

      И это действительно так. В критическом ПО, например, они запрещены к использованию как и goto на уровне стандартов на кодирование.


      1. evgenyk
        26.03.2019 13:31
        +1

        Все время возвращать ошибки — это адский ад.


        1. amarao
          26.03.2019 13:45
          -1

          Зависит от языка. В Rust, например, result — это основополагающая часть всего IO, и вы всегда возвращаете результат (Result), либо вы пишите чистую функцию, в которой не может быть ошибки (например, вы всегда можете сравнить на равенство два int'а и вернуть True/False без result, но не можете этого сделать с float, потому что он partialOrder, т.е. есть числа, которые сравнивать нельзя (что больше — NaN или +Inf?).


          1. evgenyk
            26.03.2019 13:46
            +2

            Я думал, мы про Python. Был неправ?
            Расшифрую: у разных языков могут быть разные подходы.


            1. amarao
              26.03.2019 13:55
              -1

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


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


              1. evgenyk
                26.03.2019 17:11

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


                1. amarao
                  26.03.2019 17:13

                  Статья про то, что при разработке большого ПО с требованиями по надёжности, надо, как правило, обрабатывать ошибки, не отдавая их «наверх», потому что «наверху» всего знать не могут.


                  1. evgenyk
                    26.03.2019 17:18

                    Мой подход другой. Наверх отдаются ошибки, с которыми неясно, что делать внизу. И это очень круто и удобно. Логику, с которй ясно, что делать, обрабатываем на месте. Неясно, отдаем наверх, разработчик решит, в зависимости от его ситуации.


                    1. wtpltd
                      26.03.2019 17:35

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


                      1. evgenyk
                        26.03.2019 18:23

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

                        try:
                            # some code
                        except:
                            pass
                        

                        Но это не очень хороший код.
                        А в в других случаях программа упадет.
                        Добавил: пардон, кстати, полностью с Вами согласен. Невнимательно прочитал коммент.


                        1. wtpltd
                          26.03.2019 18:36
                          +3

                          В статье автор предлагает

                          try:
                              return Success(first / second)
                          except ZeroDivisionError as exc:
                              return Failure(exc)
                          

                          В этом случае исключение перекладывается на разработчиков. Программа не падает, но какие гарантии, что разраб не забил на корректную обработку Failure(exc)? В этом случае и возможна ситуация, которую я описал.


                          1. evgenyk
                            26.03.2019 18:40

                            Я там дописал в комменте, я полностью с Вами согласен.


                          1. PsyHaSTe
                            27.03.2019 12:23

                            А каким образом он может «забить» на обработку Failure(exc)?


                            1. wtpltd
                              27.03.2019 12:55
                              +1

                              Да просто вообще не обрабатывать возвращаемое значение.


                              1. PsyHaSTe
                                27.03.2019 15:15

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


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

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


                                1. wtpltd
                                  27.03.2019 15:32

                                  А как такой пример?

                                  emailNewReport('20190101', '20190131', 'test@mail.com')
                                  

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


                                  1. PsyHaSTe
                                    27.03.2019 15:40

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


                                    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. 0xd34df00d
                                    28.03.2019 16:05
                                    +1

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


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


                                1. VolCh
                                  28.03.2019 11:38

                                  Не согласен с «совершенно сознательно». if написал, а else: «потом напишу, и вообще надо не забыть у продакта спросить, а что делать если ноль пришёл»


                                  1. PsyHaSTe
                                    28.03.2019 11:49

                                    Написал try {… } catch {} и такой «спрошу у продакта потом». В чем разница?


                                    1. rraderio
                                      28.03.2019 18:10

                                      А зачем писать catch?


                                      1. PsyHaSTe
                                        28.03.2019 18:11

                                        Чтобы потом не забыть поправить, конечно же.

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


                    1. amarao
                      26.03.2019 17:36

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


                      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'ы могут быть в любом месте от любой функции).


                      1. vintage
                        26.03.2019 18:10
                        +3

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

                        RangeError: Maximum call stack size exceeded


                        1. amarao
                          27.03.2019 14:42

                          Это не ошибка функции, это ошибка вызывающего.


                          1. vintage
                            27.03.2019 16:47
                            +1

                            Не важно. Это эксепшен, который может вылететь где угодно. И его надо уметь обрабатывать.


                            1. amarao
                              27.03.2019 17:03

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


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


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


                              try:
                                 foo()
                              except RecursionError:
                                handle_recursion_error()

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


                              1. vintage
                                27.03.2019 18:37

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

                                Много чего.


                              1. 0xd34df00d
                                28.03.2019 18:48

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


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


                            1. PsyHaSTe
                              27.03.2019 18:08

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


                              1. vintage
                                27.03.2019 18:41

                                Что делать с эксепшнов во время обработки эксепшна?

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


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

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


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

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


                                1. PsyHaSTe
                                  27.03.2019 21:41

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

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


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


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


                                  1. vintage
                                    27.03.2019 22:32

                                    Для этого существуют проверяемые исключения.


                                    1. PsyHaSTe
                                      27.03.2019 23:17

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


                                      1. vintage
                                        28.03.2019 08:07

                                        Проверяемые исключения это и есть что-то вроде резалтов, но более неудобное.

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


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

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


                                        1. PsyHaSTe
                                          28.03.2019 11:52

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

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

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

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

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

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


                                          1. vintage
                                            28.03.2019 16:08

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

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


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

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


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

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


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

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


                                            1. PsyHaSTe
                                              28.03.2019 16:21

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

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

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

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

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

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


                                              1. vintage
                                                28.03.2019 17:36

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

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


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

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


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

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


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

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


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

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


                                                1. PsyHaSTe
                                                  28.03.2019 18:10

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

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

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

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

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

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

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

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


                                                  1. vintage
                                                    28.03.2019 19:32

                                                    Потому что он явно должен это написать.

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


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

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


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

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


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

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


                                                    1. PsyHaSTe
                                                      28.03.2019 20:19

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

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


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

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


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

                                                      нет


                                                      1. vintage
                                                        28.03.2019 21:08

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

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


                                                        1. PsyHaSTe
                                                          28.03.2019 22:47

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


                                                          1. vintage
                                                            28.03.2019 23:05

                                                            Я бы посмотрел, как вы будете объяснять монады пользователям экселя.


                                                            1. PsyHaSTe
                                                              28.03.2019 23:31

                                                              Питон создан для пользователей экселя? Что-то новое.

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


                                                              1. vintage
                                                                29.03.2019 05:29

                                                                На питоне можно написать эксель.

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


                                        1. 0xd34df00d
                                          28.03.2019 18:49

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

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


                                          1. vintage
                                            28.03.2019 19:33

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


                                            1. 0xd34df00d
                                              28.03.2019 19:37
                                              +1

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


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


                                  1. evgenyk
                                    27.03.2019 22:52

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


                            1. 0xd34df00d
                              28.03.2019 18:47

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


                              1. vintage
                                28.03.2019 19:40

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

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


                                1. evgenyk
                                  28.03.2019 19:43

                                  +1. Ловить нужно все, чтобы записать в логи. Ну или в простейших случаях Python упадет и все напишет сам.


                                1. 0xd34df00d
                                  28.03.2019 19:44

                                  Я как раз про это и говорю: в библиотечном чистом коде этого делать не надо (если, конечно, это не цель библиотеки — работа с такими исключениями).

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

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


                            1. ivanrt
                              29.03.2019 09:35

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


                      1. evgenyk
                        26.03.2019 18:24

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

                        Это как? Слова я понимаю, но представить как это сделать в Python не могу. Возможно я что-то не знаю?


                        1. amarao
                          27.03.2019 14:43

                          Как забыл, и как заставить вышестоящего по коду обработать ошибку?


                      1. kzhyg
                        26.03.2019 23:40

                        И тут на сцену выходят проверяемые исключения :D


                        1. woodhead
                          27.03.2019 06:32

                          Кстати, да. Вроде как сейчас все уже поняли, что исключения должны быть непроверяемые. Об этом чётко написано в книге Боба Мартина «Чистый код». Но непонятно, как в этом случае узнать другому программисту, что некий участок кода может выдать исключение, если это исключение явно не описывается в сигнатуре функции?


                          1. kzhyg
                            27.03.2019 14:42

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


                  1. Hardcoin
                    26.03.2019 19:14
                    +2

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


                    1. amarao
                      27.03.2019 14:45
                      -1

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

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

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


                    1. BlessMaster
                      27.03.2019 16:46

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


                      1. Hardcoin
                        28.03.2019 00:47

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


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


        1. Sly_tom_cat
          26.03.2019 22:28

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


    1. fukkit
      26.03.2019 22:17

      Их не умеют, не хотят, лень.


    1. PsyHaSTe
      27.03.2019 12:21

      Вот тут более подробно можно почитать:

      www.lighterra.com/papers/exceptionsharmful

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

      Но очень рекомендую ознакомиться со статьей. Кстати, название статьи слегка намекает на сходство с goto.


  1. MaxKom
    26.03.2019 13:40
    +1

    Can I divide by zero in your code?


    1. agarus
      26.03.2019 21:47

      Это отсылка к чему-то?


      1. MaxKom
        27.03.2019 10:33

        Это отсылка к чему-то?

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

        ====

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

        Что-то вроде
        try
        {
        a = Int(полученные данные)
        }
        catch (er)
        {
        a = 0;
        }
        


  1. Hardcoin
    26.03.2019 13:44
    +9

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

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


    1. evgenyk
      26.03.2019 13:50
      +1

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


    1. werevolff
      27.03.2019 08:45
      +1

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


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


  1. amarao
    26.03.2019 13:54
    +2

    Я постоянно это повторяю, и повторю снова: 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. zvulon
      27.03.2019 01:18

      а как на счет StopIteration? Warning?
      на питоне нормально для control path применять,
      только нужно создавать marked Exceptions а не тыкать везде raise Exception


    1. 0xd34df00d
      28.03.2019 19:33

      На практике — флипнется бит в памяти и когда-нибудь оно случится. Или while True закончится.

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


  1. MikiRobot
    26.03.2019 14:03
    +4

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


    1. fireSparrow
      26.03.2019 14:36

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


      1. dreesh
        26.03.2019 16:23

        Для Haskell нужно скилов больше чем для python) Иначе он будет работать не в 2 раза медленнее чем c++ а в 2 раза медленнее python…


        1. fireSparrow
          26.03.2019 16:36
          +1

          Ну, раз уж мы говорим о ПО с очень высокими требованиями к отказоустойчивости и предсказуемости, то очевидно, что это предполагает определённый уровень программистов.
          Собственно, именно это я и хотел донести своим комментарием — есть ПО для интернет-магазинов и ПО для атомных реакторов.
          В одном случае допустимо выдать страничку «что-то пошло не так, попробуйте позже», но разработка должна быть быстрой, код ясным, а программисты — легкозаменяемыми. И здесь питон идеален.
          А во втором случае можно долго и тщательно писать и отлаживать код, а программисты могут быть штучным товаром. И нет никакого смысла тащить практики, относящиеся ко второму случаю, в язык, предназначенный для первого случая.


          1. evgenyk
            26.03.2019 17:15

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


            1. fireSparrow
              26.03.2019 17:25

              Вот да, тесты и обработка исключений — это всё хорошо вписывается в то, чем является питон.
              Я против только тех подходов, которые в питоне не нужны. А если для конкретной задачи они необходимы — то для этой задачи изначально не стоило брать питон.


        1. 0xd34df00d
          28.03.2019 19:35

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


    1. zvulon
      27.03.2019 01:08

      ага, такое чувство, что style guides и python way не читай,
      давайте питон в го в жабу или в спп превратим (или хаскель)


  1. shrimpsizemoose
    26.03.2019 14:08
    +4

    Я вот из всех докладов и статей Соболева не могу понять, зачем он продолжает писать на питоне и пытается «кормить лошадь углём и запрягать в паровоз».


  1. tgz
    26.03.2019 14:11
    +1

    Ощущение что чуваки увидели rust и поняли что до этого занимались какой-то фигней :)


  1. kraglik
    26.03.2019 14:13

    Так Result, Success, Failure — это же Either из Haskell. И это давно есть во всяких PyMonad и прочих подобных.


    1. caffeinum
      26.03.2019 16:03

      В Swift есть Optionals, это встроенная в язык монада. Очень-очень удобно!

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

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


      1. CrazyOpossum
        27.03.2019 00:26

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

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


      1. Norrius
        27.03.2019 12:55

        В 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 (>>=), только немного по-другому записанный.


  1. evgeny_pro
    26.03.2019 14:18
    +1

    Об этом было давно сказано www.joelonsoftware.com/2003/10/13/13


  1. TyVik
    26.03.2019 14:20
    +1

    Серъёзная заявка на победу. В смысле очень большие изменения в языке. Выглядит как попытка затащить монадки в язык, который к этому не приспособлен.

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


    1. spyphy
      27.03.2019 11:47

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


  1. vintage
    26.03.2019 14:32

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


    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)
    
       # ...


  1. Andrey_Dolg
    26.03.2019 14:39
    +1

    Автор имейте совесть. Вы хоть представляете сколько людей за валерьянкой пошло.


  1. Akon32
    26.03.2019 16:00

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


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


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


  1. MaximChistov
    26.03.2019 16:07

    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

    Аналогичный вопрос про любые похожие ситцуации в коде, не связанном с арифметикой


    1. CrazyOpossum
      27.03.2019 00:29

      Если я правильно понял — завернуть всё в pipeline и везде где мы получили эти переменные j, l, n, k, z получать их unwrap'ом. Тогда в самих переменных будут честные int'ы, но если хотя бы одна завалится, то pipeline тормознёт вычисления.


      1. MaximChistov
        27.03.2019 10:59

        >Тогда в самих переменных будут честные int'ы, но если хотя бы одна завалится, то pipeline тормознёт вычисления.
        Это вы только что описали принцип работы исключений :)


        1. CrazyOpossum
          28.03.2019 15:02

          Что насчёт такого?

          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() и добавить подробное описание.


          1. evgenyk
            28.03.2019 15:23

            Можно в одном блоке try except ловить несколько типов исключений.


            1. CrazyOpossum
              28.03.2019 17:50

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


              1. MaximChistov
                28.03.2019 18:28

                >в каком конкретно вызове произошло исключение
                Разве в питоне нет стектрейса?


              1. evgenyk
                28.03.2019 18:58

                Ну не знаю. Когда я вызываю пишу свой код и использую вызов функции из сторонней библиотеки, при получении исключения, мне в порядке убывания важности:
                1) Важно то, что я не получил результат. Группируем все ексепшены и в одном месте делаем то, что нужно при неполучении результата. Отлично!
                2) Кроме неполучения результата, мои действия могут отличаться в зависимости от того, какое это исключение. Группируем ексепшены по вариантам моих действий и обрабатываем так как нужно в каждом случае. Опять отлично!


  1. dreesh
    26.03.2019 16:09

    Ммм монада maybe или Either


  1. el777
    26.03.2019 16:36

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


    1. sergey-gornostaev
      27.03.2019 14:34

      Можно далеко не отходить даже, есть Lisp компилирующийся в байткод Python — Hy.


  1. maslyaev
    26.03.2019 20:23
    -3

    Обычная дилемма: или даёшь блёклый заголовок, и тогда народ тупо проходит мимо, или провоцируешь публику и получаешь порцию яда в комментах.

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


  1. GeloXIII
    26.03.2019 20:40
    +4

    всегда хорошо когда-что либо делаешь пользоваться зравым смыслом

    притягивать за уши монады в 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. dimonoid
    26.03.2019 21:07
    +2

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


  1. Vindicar
    26.03.2019 21:42
    +2

    >Исключения считаются антипаттерном
    >Останов итерации for реализован на основе исключений
    Да ладно?


  1. epishman
    26.03.2019 21:52
    +1

    Исключения выпиливают, goto запрещают, мьютексы не рекомендуют, ООП устарело, циклы это для олдфагов… мама, роди меня обратно…


  1. setevoy4
    26.03.2019 23:07

    Тот случай, когда жалеешь, что ты на Хабре только читатель с r/o, и не можешь поставить минус за такой заголовок.
    Это самый натуральный кликбейт.


  1. zvulon
    27.03.2019 01:13

    Никто исключение из питона не выпиливает,
    более того в питоне нормально использовать исключения для Control Flow
    (что не рекоммендовано в большинстве языков типа Cpp, Java, C#)
    посмотрите на Warning, UserWarning, DeprecationWarning
    или тем более на StopIteration exceptions.

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


  1. jorgen
    27.03.2019 08:57

    Похоже, скоро фразы со словом «антипаттерн» внутри — станут антипаттернами.


  1. igormich88
    27.03.2019 11:32

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


    1. PsyHaSTe
      27.03.2019 12:32

      let res = do {
         let a <- canFail1()
         let b <- canFail2()
         let result <- canFail3(a,b)
         result
      }
      return result.ok() // Just(result) or None


      1. rraderio
        27.03.2019 15:53

        А если canFail3 принимает Int и String а не Result?


        1. PsyHaSTe
          27.03.2019 16:02

          a и b в данном случае это и есть int и String.

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

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


      1. igormich88
        27.03.2019 17:49

        У вас если canFail1 вернуло ошибку canFail2 всё равно выполнится.


        1. PsyHaSTe
          27.03.2019 18:13

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


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


          1. igormich88
            28.03.2019 09:59

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


            1. PsyHaSTe
              28.03.2019 11:57

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


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


              1. igormich88
                28.03.2019 12:14

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


                1. PsyHaSTe
                  28.03.2019 12:17

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

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

                  Теперь я пишу тесты. Но тесты всегда хуже проверок типов.


  1. anjensan
    27.03.2019 13:33

    Срочно переходите на Go!!!
    Вам понравится.


  1. Megadeth77
    27.03.2019 21:57

    Скоро питон превратят в яву и колесо повернется на следующий круг.


  1. Meklon
    28.03.2019 11:41

    Эмм… Я ненастоящий сварщик, но что неправильного в таком примере кода? Вроде я все вылавливаю. И в логах потом все хорошо видно. У меня куча сетевых запросов разных и очень часто я какую-то ересь получаю от сервера. Мне предпочтительнее передать дальше 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
    


    1. immaculate
      28.03.2019 12:29

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


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


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


      1. Meklon
        28.03.2019 13:21

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

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

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


  1. pipewalker
    28.03.2019 13:16

    Выглядит как попытка переизобрести колесо. То ли монады, то ли условия/рестарты из Common Lisp (и оно же на русском)