Как стать автором
Обновить

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

Время на прочтение9 мин
Количество просмотров58K
Всего голосов 116: ↑71 и ↓45+26
Комментарии149

Комментарии 149

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

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

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


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

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

Но это не очень хороший код.
А в в других случаях программа упадет.
Добавил: пардон, кстати, полностью с Вами согласен. Невнимательно прочитал коммент.
НЛО прилетело и опубликовало эту надпись здесь
Я там дописал в комменте, я полностью с Вами согласен.
А каким образом он может «забить» на обработку Failure(exc)?
НЛО прилетело и опубликовало эту надпись здесь

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


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

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

НЛО прилетело и опубликовало эту надпись здесь

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


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() можно судить, может ли в этом месте произойти ошибка, и если да, то какая именно.

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

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

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


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

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

RangeError: Maximum call stack size exceeded

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

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


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


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


try:
   foo()
except RecursionError:
  handle_recursion_error()

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

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

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


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

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


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

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

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

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


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


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

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

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


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

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

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

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

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

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

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

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

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


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

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


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

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


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

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

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

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

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

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

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

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

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


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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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

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

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


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

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


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

нет

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

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

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

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

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

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

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

НЛО прилетело и опубликовало эту надпись здесь
В Питоне вы бы сразу все поняли бы по логам. С первого падения.
НЛО прилетело и опубликовало эту надпись здесь
Их не надо ловить

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

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

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

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

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

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

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

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

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

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

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


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

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

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

www.lighterra.com/papers/exceptionsharmful

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

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

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

====

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

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

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

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

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


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

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

а как на счет StopIteration? Warning?
на питоне нормально для control path применять,
только нужно создавать marked Exceptions а не тыкать везде raise Exception
НЛО прилетело и опубликовало эту надпись здесь
За такие заголовки надо бить. Если где то у вас в Go или другом языке есть сложившийся подход, не нужно его тянуть в питон. Подходы приведенные в Вашем примере не совместимы с существующими практиками, а решаемые проблемы выдуманы.
+1.
Если действительно у человека есть потребность писать код, который должен работать как часы даже в случае ядерной войны, то для этого лучше выбрать язык со статической типизацией. Например, Rust или Haskell, которые уже упомянули в комментах.
В тех же областях, для которых питон является хорошим выбором, исключения являются вполне удобным и разумным подходом.
Для Haskell нужно скилов больше чем для python) Иначе он будет работать не в 2 раза медленнее чем c++ а в 2 раза медленнее python…
Ну, раз уж мы говорим о ПО с очень высокими требованиями к отказоустойчивости и предсказуемости, то очевидно, что это предполагает определённый уровень программистов.
Собственно, именно это я и хотел донести своим комментарием — есть ПО для интернет-магазинов и ПО для атомных реакторов.
В одном случае допустимо выдать страничку «что-то пошло не так, попробуйте позже», но разработка должна быть быстрой, код ясным, а программисты — легкозаменяемыми. И здесь питон идеален.
А во втором случае можно долго и тщательно писать и отлаживать код, а программисты могут быть штучным товаром. И нет никакого смысла тащить практики, относящиеся ко второму случаю, в язык, предназначенный для первого случая.
На самом деле одно не исключает другого в сегодняшнем питоновском подходе. Просто, нужна надежность, пишем тесты, отлаживаем, ловим все нужные исключения. И все.
С другой стороны, нужно быстро набросать скрипт — пишем без всякой обработки ошибок. В случае ошибки читаем, что выбрасывает питон и чиним. Быстро и хорошо.
Вот да, тесты и обработка исключений — это всё хорошо вписывается в то, чем является питон.
Я против только тех подходов, которые в питоне не нужны. А если для конкретной задачи они необходимы — то для этой задачи изначально не стоило брать питон.
НЛО прилетело и опубликовало эту надпись здесь
ага, такое чувство, что style guides и python way не читай,
давайте питон в го в жабу или в спп превратим (или хаскель)
Я вот из всех докладов и статей Соболева не могу понять, зачем он продолжает писать на питоне и пытается «кормить лошадь углём и запрягать в паровоз».
Ощущение что чуваки увидели rust и поняли что до этого занимались какой-то фигней :)
Так Result, Success, Failure — это же Either из Haskell. И это давно есть во всяких PyMonad и прочих подобных.
В Swift есть Optionals, это встроенная в язык монада. Очень-очень удобно!

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

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

Конечно — трансформеры монад. Делаем всякие разные грязные действия, возвращающие Either (Result в статье), если хотя бы одно вернёт Left (Failure), игнорируем все последующие и возращаем этот Left. При этом локальные присваивания игнорируют Either (Result) и всегда думают, что им вернули Right (Success). Если не вернули — смотри выше.
В 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 (>>=), только немного по-другому записанный.
Серъёзная заявка на победу. В смысле очень большие изменения в языке. Выглядит как попытка затащить монадки в язык, который к этому не приспособлен.

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

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


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


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

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

Аналогичный вопрос про любые похожие ситцуации в коде, не связанном с арифметикой
Если я правильно понял — завернуть всё в pipeline и везде где мы получили эти переменные j, l, n, k, z получать их unwrap'ом. Тогда в самих переменных будут честные int'ы, но если хотя бы одна завалится, то pipeline тормознёт вычисления.
>Тогда в самих переменных будут честные int'ы, но если хотя бы одна завалится, то pipeline тормознёт вычисления.
Это вы только что описали принцип работы исключений :)
Что насчёт такого?
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() и добавить подробное описание.
Можно в одном блоке try except ловить несколько типов исключений.
Это то что я назвал вторым способом. При этом мы теряем информацию, в каком конкретно вызове произошло исключение и должны поддерживать несколько обработчиков, что затрудняет чтение и добавляет сложности.
>в каком конкретно вызове произошло исключение
Разве в питоне нет стектрейса?
Ну не знаю. Когда я вызываю пишу свой код и использую вызов функции из сторонней библиотеки, при получении исключения, мне в порядке убывания важности:
1) Важно то, что я не получил результат. Группируем все ексепшены и в одном месте делаем то, что нужно при неполучении результата. Отлично!
2) Кроме неполучения результата, мои действия могут отличаться в зависимости от того, какое это исключение. Группируем ексепшены по вариантам моих действий и обрабатываем так как нужно в каждом случае. Опять отлично!

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

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

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

притягивать за уши монады в 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)

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

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

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

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

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

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

А если canFail3 принимает Int и String а не Result?
a и b в данном случае это и есть int и String.

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

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

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


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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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


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

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

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

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

inf :D
НЛО прилетело и опубликовало эту надпись здесь
Это вариант для операции деления. А функция divide по условию должна как-то обработать все возникшие ошибки и вернуть результат.
Заголовок кликбейтный.
В статьях с такими заголовками ценность информации как правило равна нулю или около того.

Каждый мечтает стать собственным Геростратом, назвав антипаттерном то, что люди используют нормально годами.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий