Проблемы исключений
Трудно найти недостатки в том, с чем сталкиваешься каждый день. Привычка и зашоренность превращает баги в фичи, но давайте попробуем взглянуть на исключения непредвзято.
Исключения трудно заметить
Существует два типа исключений: «явные» создаются при помощи вызова
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)
MaxKom
26.03.2019 13:40+1Can I divide by zero in your code?
agarus
26.03.2019 21:47Это отсылка к чему-то?
MaxKom
27.03.2019 10:33Это отсылка к чему-то?
Несколько слоёв.
====
По теме статьи: чем в принципе плоха практика проверки типа полученных откуда-то из вне (например, от пользователя) данных через try catch c приведением типов?
Что-то вроде
try { a = Int(полученные данные) } catch (er) { a = 0; }
Hardcoin
26.03.2019 13:44+9Исключения в Python теперь считаются анти-паттерном
Можно ссылку на официальную позицию? А то какой-то кликбейт. Если они считаются антипаттерном по мнению Никиты Соболева — никаких проблем, но стоит это явно указать.
werevolff
27.03.2019 08:45+1Можно ещё форматирование строк через % назвать антипаттерном. А то я предпочитаю format. Или тернарные операции назовём антипаттерном. Они маскируют условия под операции присвоения. А ещё можно использование lambda объявить антипаттерном. Чисто поржать.
Долбаные модернисты, короче. Куда ни плюнь — везде находят антипаттерны. Лишь бы продвинуть свой продукт.
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'ы.
zvulon
27.03.2019 01:18а как на счет StopIteration? Warning?
на питоне нормально для control path применять,
только нужно создавать marked Exceptions а не тыкать везде raise Exception
0xd34df00d
28.03.2019 19:33На практике — флипнется бит в памяти и когда-нибудь оно случится. Или while True закончится.
На практике компилятор заметит, что не случится, и соптимизирует проверку. Правда, не в питоне.
MikiRobot
26.03.2019 14:03+4За такие заголовки надо бить. Если где то у вас в Go или другом языке есть сложившийся подход, не нужно его тянуть в питон. Подходы приведенные в Вашем примере не совместимы с существующими практиками, а решаемые проблемы выдуманы.
fireSparrow
26.03.2019 14:36+1.
Если действительно у человека есть потребность писать код, который должен работать как часы даже в случае ядерной войны, то для этого лучше выбрать язык со статической типизацией. Например, Rust или Haskell, которые уже упомянули в комментах.
В тех же областях, для которых питон является хорошим выбором, исключения являются вполне удобным и разумным подходом.dreesh
26.03.2019 16:23Для Haskell нужно скилов больше чем для python) Иначе он будет работать не в 2 раза медленнее чем c++ а в 2 раза медленнее python…
fireSparrow
26.03.2019 16:36+1Ну, раз уж мы говорим о ПО с очень высокими требованиями к отказоустойчивости и предсказуемости, то очевидно, что это предполагает определённый уровень программистов.
Собственно, именно это я и хотел донести своим комментарием — есть ПО для интернет-магазинов и ПО для атомных реакторов.
В одном случае допустимо выдать страничку «что-то пошло не так, попробуйте позже», но разработка должна быть быстрой, код ясным, а программисты — легкозаменяемыми. И здесь питон идеален.
А во втором случае можно долго и тщательно писать и отлаживать код, а программисты могут быть штучным товаром. И нет никакого смысла тащить практики, относящиеся ко второму случаю, в язык, предназначенный для первого случая.evgenyk
26.03.2019 17:15На самом деле одно не исключает другого в сегодняшнем питоновском подходе. Просто, нужна надежность, пишем тесты, отлаживаем, ловим все нужные исключения. И все.
С другой стороны, нужно быстро набросать скрипт — пишем без всякой обработки ошибок. В случае ошибки читаем, что выбрасывает питон и чиним. Быстро и хорошо.fireSparrow
26.03.2019 17:25Вот да, тесты и обработка исключений — это всё хорошо вписывается в то, чем является питон.
Я против только тех подходов, которые в питоне не нужны. А если для конкретной задачи они необходимы — то для этой задачи изначально не стоило брать питон.
0xd34df00d
28.03.2019 19:35Нет, это не так. На хаскеле относительно легко писать производительный код, и совсем легко писать код, который будет быстрее питона (особенно питона, не лезущего в С). ghc творит чудеса, если специально не вставлять ему палки в колёса ради интереса.
zvulon
27.03.2019 01:08ага, такое чувство, что style guides и python way не читай,
давайте питон в го в жабу или в спп превратим (или хаскель)
shrimpsizemoose
26.03.2019 14:08+4Я вот из всех докладов и статей Соболева не могу понять, зачем он продолжает писать на питоне и пытается «кормить лошадь углём и запрягать в паровоз».
tgz
26.03.2019 14:11+1Ощущение что чуваки увидели rust и поняли что до этого занимались какой-то фигней :)
kraglik
26.03.2019 14:13Так Result, Success, Failure — это же Either из Haskell. И это давно есть во всяких PyMonad и прочих подобных.
caffeinum
26.03.2019 16:03В Swift есть Optionals, это встроенная в язык монада. Очень-очень удобно!
А директива safe напомнила async/await из JS, тоже такой способ скрытно работать с контейнерами.
В Haskell есть какая-то похожая штука для неявной работы с контейнерами? inline-разворачивания, так сказать?CrazyOpossum
27.03.2019 00:26В Haskell есть какая-то похожая штука для неявной работы с контейнерами? inline-разворачивания, так сказать?
Конечно — трансформеры монад. Делаем всякие разные грязные действия, возвращающие Either (Result в статье), если хотя бы одно вернёт Left (Failure), игнорируем все последующие и возращаем этот Left. При этом локальные присваивания игнорируют Either (Result) и всегда думают, что им вернули Right (Success). Если не вернули — смотри выше.
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 (>>=
), только немного по-другому записанный.
TyVik
26.03.2019 14:20+1Серъёзная заявка на победу. В смысле очень большие изменения в языке. Выглядит как попытка затащить монадки в язык, который к этому не приспособлен.
Мне кажется, что если и продвигать такие изменения, то через PEP и изменения конструкций языка, а не через стороннюю библиотеку. Но что-то мне подсказывает, что наследники Гвидо такое не одобрят.spyphy
27.03.2019 11:47Так многие дополнительные фичи в питоне вынесены в отдельные модули (те же интерфейсы, абстрактные классы). Ну и ок, кому надо, тот юзает. Менять синтаксис языка из-за этого никто не собирается — большую часть населения он и так устраивает.
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) # ...
Andrey_Dolg
26.03.2019 14:39+1Автор имейте совесть. Вы хоть представляете сколько людей за валерьянкой пошло.
Akon32
26.03.2019 16:00Один вопрос. Вы пробовали это использовать?
Навскидку, вижу проблемы с тем, что 1) стандартная библиотека не поддерживает ваш подход; 2) декоратор
@pipeline
может сработать неправильно в каких-то непредусмотренных сложных случаях; 3) если использовать вашу библиотеку в той мере, в какой в идеале нужно (т.е. везде), поток управления будет очень напоминать поток при обработке исключений, только обработку исключений разработчики языка могут как-то оптимизировать, а в вашей библиотеке "лапша" управления так и останется.
Очень похоже на
scala.util.Try
, только в человеческой обёртке.
MaximChistov
26.03.2019 16:071 + 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
Аналогичный вопрос про любые похожие ситцуации в коде, не связанном с арифметикойCrazyOpossum
27.03.2019 00:29Если я правильно понял — завернуть всё в pipeline и везде где мы получили эти переменные j, l, n, k, z получать их unwrap'ом. Тогда в самих переменных будут честные int'ы, но если хотя бы одна завалится, то pipeline тормознёт вычисления.
MaximChistov
27.03.2019 10:59>Тогда в самих переменных будут честные int'ы, но если хотя бы одна завалится, то pipeline тормознёт вычисления.
Это вы только что описали принцип работы исключений :)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() и добавить подробное описание.evgenyk
28.03.2019 15:23Можно в одном блоке try except ловить несколько типов исключений.
CrazyOpossum
28.03.2019 17:50Это то что я назвал вторым способом. При этом мы теряем информацию, в каком конкретно вызове произошло исключение и должны поддерживать несколько обработчиков, что затрудняет чтение и добавляет сложности.
MaximChistov
28.03.2019 18:28>в каком конкретно вызове произошло исключение
Разве в питоне нет стектрейса?
evgenyk
28.03.2019 18:58Ну не знаю. Когда я вызываю пишу свой код и использую вызов функции из сторонней библиотеки, при получении исключения, мне в порядке убывания важности:
1) Важно то, что я не получил результат. Группируем все ексепшены и в одном месте делаем то, что нужно при неполучении результата. Отлично!
2) Кроме неполучения результата, мои действия могут отличаться в зависимости от того, какое это исключение. Группируем ексепшены по вариантам моих действий и обрабатываем так как нужно в каждом случае. Опять отлично!
el777
26.03.2019 16:36Возьми любой функциональный язык и не мучайся.
Любишь красивые теоретические конструкции — Haskell.
Практический код — Scala.
Хочется императивщины с полуручным управлением памятью — Rust.
Питон, он для другого. В нем вся эта история будет жуткими костылями.sergey-gornostaev
27.03.2019 14:34Можно далеко не отходить даже, есть Lisp компилирующийся в байткод Python — Hy.
maslyaev
26.03.2019 20:23-3Обычная дилемма: или даёшь блёклый заголовок, и тогда народ тупо проходит мимо, или провоцируешь публику и получаешь порцию яда в комментах.
Что касается самой идеи, то она реально зачётная. И, что бы ни говорили, стопроцентно питонская. Сам недавно перепилил функцию, возвращающую булево на функцию, возвращающую кортеж в стиле (результат, почему_нет). Декоратор «safe» — просто чудо. Хотел бы или нет я это видеть в стандартной библиотеке — не уверен, но как приёмчик, который в случае чего применить в своё удовольствие — очень достойно. Спасибо.
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)
dimonoid
26.03.2019 21:07+2Зачем Javа из Python делать? Если вам типов не хватает, то не надо Python коверкать, он не для того создавался. Python — для быстрых расчетов на скорую руку, а для продакшена используйте Java.
Vindicar
26.03.2019 21:42+2>Исключения считаются антипаттерном
>Останов итерации for реализован на основе исключений
Да ладно?
epishman
26.03.2019 21:52+1Исключения выпиливают, goto запрещают, мьютексы не рекомендуют, ООП устарело, циклы это для олдфагов… мама, роди меня обратно…
setevoy4
26.03.2019 23:07Тот случай, когда жалеешь, что ты на Хабре только читатель с r/o, и не можешь поставить минус за такой заголовок.
Это самый натуральный кликбейт.
zvulon
27.03.2019 01:13Никто исключение из питона не выпиливает,
более того в питоне нормально использовать исключения для Control Flow
(что не рекоммендовано в большинстве языков типа Cpp, Java, C#)
посмотрите на Warning, UserWarning, DeprecationWarning
или тем более на StopIteration exceptions.
по началу мне тоже не хватало switch, pattern matching и прочего.
но у питона свой путь, и тащить сюда вещи из других языков не надо.
Как на счет фигурных скобок?
igormich88
27.03.2019 11:32Примеры хорошо выглядит пока вычисления последовательные. А если у меня такой код
try:
a = canFail1()
b = canFail2()
return canFail3(a, b)
except SomeError:
return NonePsyHaSTe
27.03.2019 12:32let res = do { let a <- canFail1() let b <- canFail2() let result <- canFail3(a,b) result } return result.ok() // Just(result) or None
igormich88
27.03.2019 17:49У вас если canFail1 вернуло ошибку canFail2 всё равно выполнится.
PsyHaSTe
27.03.2019 18:13Почему это? Все развернется в цепочку
>>=
, которая остановится на первой ошибке. Только let'ы зря написал, между языками если часто переключаться можно случайно запутаться без поддержки иде.
res = do a <- canFail1() b <- canFail2() result <- canFail3(a,b) return result
igormich88
28.03.2019 09:59Я видимо плохо знаю питон и не совсем понимаю что делает оператор <-
PsyHaSTe
28.03.2019 11:57На питоне это будет примерно
res=canFail1() .flatMap(lambda a: canFail2().map(lambda b: canFail3(a,b) ) )
igormich88
28.03.2019 12:14Да но этот код выглядит хуже чем код с try… catch
PsyHaSTe
28.03.2019 12:17Согласен. Потому что нет языковой поддержки. С поддержкой получится как в примере 1, который вполне читаем и удобен. Особенно учитывая, что можно комбинировать по всякому. Явное >> неявного, рассчитывать что кто-то выше по стеку поймает и сделает что нужно часто вредно. Только на прошлой неделе я словил баг, связанный с этим. У меня есть реббит, и некоторые исключения являются бизнес-ошибками, из-за чего нужно переложить сообщение в deadletter, либо ошибкой нижнего слоя, тогда нужно сделать NackWithRequeue и попробовать позже еще раз. И у меня одно исключение не обрабатывалось, и не попадало ни туда, ни туда.
Было бы приятнее получить от компилятор ошибку «ты забыл вот эту ошибку обработать», когда она добавилась.
Теперь я пишу тесты. Но тесты всегда хуже проверок типов.
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
immaculate
28.03.2019 12:29По сути статьи мне сказать нечего… Мне кажется, каждый инструмент имеет область применения. Сложно придумать правила на все случаи жизни. Копья по поводу исключений ломаются десятилетиями...
Конкретно по поводу вашего кода могу сказать, что читается очень тяжело. Эти громоздкие строки логирования и названия переменных… Я считаю, что писать тип переменной в ее названии (
list_
,str_
) — антипаттерн. Названия становятся громоздкими и менее читаемыми. А от отсутствия проверки типов это все равно не спасет.
И еще, вместо
logger.error
лучше использоватьlogger.exception
— он сразу напечатает исключение и traceback.Meklon
28.03.2019 13:21Хм. Спасибо. Попробую учесть. С логгированием в плане громоздкости у меня вариантов нет. Мне надо точно знать где и что у меня произошло в процессе.
Насчет str_, list_ — как-то всегда казалось удобным. Попробую отрефакторить аккуратно.
А как logger.exception работает? Мне надо не просто ошибку вывалить, а показать, что «При попытке получить сертификат шифрования с 2443 порта сервера example.com произошла ошибка: 'Сервер вернул полтора арбуза вместо сертификата' „
pipewalker
28.03.2019 13:16Выглядит как попытка переизобрести колесо. То ли монады, то ли условия/рестарты из Common Lisp (и оно же на русском)
evgenyk
Что-то я не понял, в чем проблема с исключениями.
j8kin
И это действительно так. В критическом ПО, например, они запрещены к использованию как и goto на уровне стандартов на кодирование.
evgenyk
Все время возвращать ошибки — это адский ад.
amarao
Зависит от языка. В Rust, например, result — это основополагающая часть всего IO, и вы всегда возвращаете результат (Result), либо вы пишите чистую функцию, в которой не может быть ошибки (например, вы всегда можете сравнить на равенство два int'а и вернуть True/False без result, но не можете этого сделать с float, потому что он partialOrder, т.е. есть числа, которые сравнивать нельзя (что больше — NaN или +Inf?).
evgenyk
Я думал, мы про Python. Был неправ?
Расшифрую: у разных языков могут быть разные подходы.
amarao
Так в слайдах и показывают, как реализовать Result на питоне.
Только смысл, если аннотация типов опциональная и любой дятел тебе может вернуть вместо Err() что попало, включая None.
evgenyk
На мой вкус это очень хорошая фишка, исключения, в случае непредвиденных обстоятельств прыгнуть на верхний уровень цепочки вызовов. Иначе бы пришлось передавать результаты через всю цепочку вызовов.
Часто очень удобно, когда вызываешь какие-то библиотеки неизвестно, что и где пойдет не так, шансы что что-то пойдет не так очень велики. Тогда очень удобно поймать все исключения и напечатать информацию в лог например.
Для простых скриптов, вообще удобно ничего не делать в смысле обработки, а в случае исключения просто прочитать информацию об исключении и стек вызовов, когда скрипт завершится аварийно.
В противоположном случае, без исключений пришлось бы передавать результат через всю цепочку вызовов.
Или я что-то не так понимаю?
amarao
Статья про то, что при разработке большого ПО с требованиями по надёжности, надо, как правило, обрабатывать ошибки, не отдавая их «наверх», потому что «наверху» всего знать не могут.
evgenyk
Мой подход другой. Наверх отдаются ошибки, с которыми неясно, что делать внизу. И это очень круто и удобно. Логику, с которй ясно, что делать, обрабатываем на месте. Неясно, отдаем наверх, разработчик решит, в зависимости от его ситуации.
wtpltd
Так именно для этого исключения и существуют и именно так применяются.
А иначе может сложиться ситуация, когда программа не падает, при этом работает неправильно, но об этом никто не знает.
evgenyk
Программа не падает и работает с исключениями неправильно в одном случае, когда разработчик сделал
Но это не очень хороший код.
А в в других случаях программа упадет.
Добавил: пардон, кстати, полностью с Вами согласен. Невнимательно прочитал коммент.
wtpltd
В статье автор предлагает
В этом случае исключение перекладывается на разработчиков. Программа не падает, но какие гарантии, что разраб не забил на корректную обработку Failure(exc)? В этом случае и возможна ситуация, которую я описал.
evgenyk
Я там дописал в комменте, я полностью с Вами согласен.
PsyHaSTe
А каким образом он может «забить» на обработку Failure(exc)?
wtpltd
Да просто вообще не обрабатывать возвращаемое значение.
PsyHaSTe
Нет, ну вот пример
Просто я вижу тут два сценария:
Первый, автор просто делает unwrap() и соглашается с паникой если значения нет.
Второй, автор делает
if div.Ok() ...
тогда он обработал ситуацию. Заставить писать Else это конечно не заставит, но это не "забыл обработать", а совершенно сознательно проигнорировал.wtpltd
А как такой пример?
Есть действия, которые в общем случае не возвращают какой-то результат. Они просто должны выполниться. Например, увеличить цены в базе на 10% — тут возвращать-то нечего. Если, например, база неедоступна, падаем и ловим исключение где это уместно. Или падаем совсем.
А если вместо исключения получили результат, который проигнорировали — начинаем торговать в минус.
PsyHaSTe
Ну тут должен помогать компилятор(интерпретатор?)
Резалты как раз нужны для того, чтобы не забыть обработать ошибку, а не наоборот. Они направлены на увеличение безопасности и уверенности, что все кейзы покрыты, а не на уменьшение.
Основная сила в том что по записи
foo = bar()
можно судить, может ли в этом месте произойти ошибка, и если да, то какая именно.0xd34df00d
Одним простым
Result
гарантировать в этом случае что-то действительно сложно.Но функциональщики (откуда всё это
Result
идёт) такие задачи решают по-другому. Во-первых, нет никаких функций, делающих действия. Есть только функции, возвращающие описания этих действий, и композиция этих функций. Во-вторых, в таком случае действия, потенциально могущие закончиться неудачей, можно представлять особым образом, с соответствующими требованиями к композиции.VolCh
Не согласен с «совершенно сознательно». if написал, а else: «потом напишу, и вообще надо не забыть у продакта спросить, а что делать если ноль пришёл»
PsyHaSTe
Написал try {… } catch {} и такой «спрошу у продакта потом». В чем разница?
rraderio
А зачем писать catch?
PsyHaSTe
Чтобы потом не забыть поправить, конечно же.
Черт, видимо ни у кого кроме меня не бывает проблем, что человек не знал, что в некотором месте может возникнуть исключение и не предусмотрел никакой обработки. В таком случае пожалуй стоит действительно замолчать и ничего не писать. Порадуюсь просто за таких людей молча.
amarao
Ошибки в обоих случаях отдаются наверх. Есть два паттерна передачи ошибки:
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'ы могут быть в любом месте от любой функции).vintage
RangeError: Maximum call stack size exceeded
amarao
Это не ошибка функции, это ошибка вызывающего.
vintage
Не важно. Это эксепшен, который может вылететь где угодно. И его надо уметь обрабатывать.
amarao
Кстати, отличный пример в моём споре с людьми про то, что чистые функции могут иметь side effects, т.е. лямбда-исчисление — очень грубая апроксимация работы компьютеров.
В принципе, я согласен, что это "неожиданная ошибка". Но, вопрос: а что программа может сделать в такой ситуации?
Вот у вас в коде:
Оно же сфейлится на вызове
handle_recursion_error
. Более того, если мы на пару уровней вверх прокинем, то там будет то же самое, потому что 2-3 вызова по стеку мы во время обработки ошибки всё равно сделаем, а на третьем нас будет ждать RE.vintage
Много чего.
0xd34df00d
А в этой ситуации ничего в чистом коде делать не надо. Этим должен заниматься нечистый код.
На самом деле это даже более общий паттерн. Тут на с. 155 и далее хорошо описана мотивация.
PsyHaSTe
Что делать с эксепшнов во время обработки эксепшна? Что делать, если процессор перегрелся во время очередного вызова функции? Что делать с космическими лучами, меняющими значение оперативной памяти?
vintage
Ловить уровнем выше.
Он сам знает что делать — троттлиться.
Дублировать и экранировать.
PsyHaSTe
Основная проблема эксепшнов — уровень выше может не ловить тот эксепшн, который выпал. И не потому, что он хотел его отдать коду выше, а потому что не знал про его существование.
Конкретный пример у меня: был код, который вызывал некий код, все эксепшны которого наследовались от RpcException. Ну я ничтоже сумнящеся написал
catch (RpcException ex)
. И все работало нормально, пока через полгодика другой коллега не обновил либу. И тоже все работало хорошо, но через какое-то время начало падать. После инвестигейта, занявшего какое-то время, стало понятно, что в новой версии добавилсяRpcUnknownException
, который не наследуется от того, и который никак не обрабатывается. Вешать глобальный хэндлер "лови любые необработанные эксепшоны" мы не хотели, т.к. это маскировало бы фатальные ошибки, которые должны вести к крашу приложения (поведение "паника").Я бы очень хотел в таком месте получить ошибку компиляции, а не молчаливое "ну что ж, прокину тогда эксепшон выше".
vintage
Для этого существуют проверяемые исключения.
PsyHaSTe
Проверяемые исключения это и есть что-то вроде резалтов, но более неудобное. Собственно, поэтому в джаве они особой славы не снискали.
vintage
Скорее наоборот.
Там бы и резалты не снискали. А объясняется всё просто: проблема с появлением неизвестного исключения в новой версии зависимости встречается настолько редко, и решается настолько просто, что оно для многих не стоит лишней писанины с перечислением всех исключений. Думаете ручная раскрутка стека в виде резалтов снискала бы популярность?
PsyHaSTe
Я поработал и с тем, и с другим. Возможность `var foo = Bar()` и не гадать, может ли тут быть какая-то ошибка или нет бесценна.
Согласен. Потому что без нормальных АДТ и паттерн матчинга делать нечего.
1. В резалтах нет никакой раскрутки стека
2. Резалты легко прокидываются наверх, если есть необходимость. В том же расте достаточно написать `?` в конце выражения, чтобы ошибку прокинуть наверх. Только вот мы явно видим, что может пойти не так.
Вопросы нового исключения очень даже актуальны для любого разрабатываемого софта. Если конечно экосистема состоит из кучи либ с мажорной версией за десяток это менее актуально. Только вот кроме либ есть еще прикладной софт, и новый эксепшон может добавить парень в соседней команде, а не только либописатель.
vintage
Проверяемые исключения позволяют вам не гадать.
Именно. Поэтому раскручивать приходится вручную.
Это необходимость в 90% случаев. Если вы, конечно не поклонник огромных функций и тривиальных приложений.
Выше я показал, что пойти не так может что угодно. И даже в этом случае приложение не должно падать. Если у вас, конечно, не консольная утилита, выполняющая одну единственную функцию.
PsyHaSTe
И как понять, человек в этом месте забыл проверить ошибку или решил её прокинуть наверх?
Есть ошибки а есть панкики. В случае паники упасть часто наилучший способ, потому что делать какие-то действия в невалидном стейте опасно.
Еще одна проблема эксепшнов — плохая композиция. Например, если я хочу сделать 10 параллельных запросов, а потом сагрегировать результаты, мне надо надеяться, что автор веб-фреймворка предусмотрел возможность этого, и какой-нибудь `WhenAll` позволяет получить результаты и ошибки. А если нет, то упс.
vintage
А как проверить подумал человек, когда писал код, или механически написал то, что от него потребовал компилятор?
Есть лишь исключительные ситуации. А появление паник — следствие протекающей абстракции "ошибок".
А давайте это вызывающий код будет решать опасно ему дальше работать или нет?
Какое это имеет отношение к теме исключений?
И какое отношение имеет веб фреймворк ко многозадачности?
PsyHaSTe
Потому что он явно должен это написать. Например, unwrap(), чтобы сказать «хрен с ней с ошибкой, дай значение». И это будет видно на ревью.
Нет, есть «все плохо, но мы знаем что с этим делать» и «все плохо и мы не знаем, что делать». Это прицнипиально разные вещи. В тот же дуднете поэтому в стандартной либе есть Exception и ApplicationException, который является подмножеством первых.
Нет, не давайте.
Такое, что мне интересно, как исключения работают в таком случае. По-моему опыту, не очень хорошо.
vintage
Мою ремарку про механическое написание вы опять проигнорировали.
Как и то, что в проверяемых исключениях точно так же нужно указать "хрен с ней с ошибкой, хочу чтобы скомпилилось", что тоже будет видно на ревью.
Это не вызываемому коду решать.
Отлично работают.
PsyHaSTe
С тем же успехом можно механически писать
throw new Exception()
в рандомных участках кода.Нет, вызывающий код ничего не знает про эту проблему, и знать не должен. В этом и отличие паники от ошибки. Про ошибку можно сообщить наверх. Про панику нельзя, потому что это нарушение внутреннего инварианта, с которым сделать ничего нельзя. Внешний код про инвариант по-определению ничего не знает, иначе это страшная протечка абстракции.
нет
vintage
Можно. Я выше приводил пример.
PsyHaSTe
Пример нерелевантен. Зачем предпочитать менее удобный инструмент без монадической обработки и явного хэндлинга более удобному мне так и не стало понятно.
vintage
Я бы посмотрел, как вы будете объяснять монады пользователям экселя.
PsyHaSTe
Питон создан для пользователей экселя? Что-то новое.
А вообще объяснить и им тоже несложно. «Если ошибки нет, то выполнится вот это, иначе будет ошибка».
vintage
На питоне можно написать эксель.
Ага, они очень обрадуются, когда на ввод неверной формулы эксель аварийно завершится и похерит всю их работу.
0xd34df00d
Для правильно сделанных резалтов все варианты перечислять и не надо.
vintage
Как и для правильно сделанных проверяемых исключений.
0xd34df00d
Да. Если к проверяемым исключениям добавить аппликативный и монадический интерфейс, нафигачить всяких там
partition
,isLeft
,isRight
и тому подобного, то получитсяEither
.Или можно просто сразу взять
Either
.evgenyk
В Питоне вы бы сразу все поняли бы по логам. С первого падения.
0xd34df00d
Поэтому эти наши любимые чистые функциональщики разделяют синхронные экзепшоны и асинхронные экзепшоны. Синхронные экзепшоны — это ошибки в вашем коде, и они отличные кандидаты на то, чтобы выражаться в виде
Either
. Асинхронные экзепшоны — это когда стек кончился, или память кончилась, или процессор отключился, или наш тред решили прибить, и пора закругляться. Их не надо ловить (и в хаскеле в чистом коде их и не получится поймать, например).vintage
А давайте разработчик прикладного решения, сам решит надо их ловить или нет, а не разработчик какой-то библиотечки где-то в глубине зависимостей? Вы когда-нибудь делали настраиваемую пользователем логику? Систему подключаемых плагинов? Да хотя бы многозадачный веб-сервер?
evgenyk
+1. Ловить нужно все, чтобы записать в логи. Ну или в простейших случаях Python упадет и все напишет сам.
0xd34df00d
Я как раз про это и говорю: в библиотечном чистом коде этого делать не надо (если, конечно, это не цель библиотеки — работа с такими исключениями).
Делал всё из этого. Веб-сервера даже на хаскеле.
А, понял. Я зря так сформулировал — их не надо ловить в чистом коде с бизнес-логикой, условно говоря. Они будут отлавливаться где-то высоко, в effectful-коде.
ivanrt
Занятно. Можно ещё попробовать обработать ситуацию когда ваше приложение убил OOM-киллер.
Напоминает дискуссию про предложенные новые исключения в c++.
evgenyk
Это как? Слова я понимаю, но представить как это сделать в Python не могу. Возможно я что-то не знаю?
amarao
Как забыл, и как заставить вышестоящего по коду обработать ошибку?
kzhyg
И тут на сцену выходят проверяемые исключения :D
woodhead
Кстати, да. Вроде как сейчас все уже поняли, что исключения должны быть непроверяемые. Об этом чётко написано в книге Боба Мартина «Чистый код». Но непонятно, как в этом случае узнать другому программисту, что некий участок кода может выдать исключение, если это исключение явно не описывается в сигнатуре функции?
kzhyg
Мартин — чокнутый идеалист, возводящий в абсолют простые практики, понятные неподготовленной аудитории. Процентов на 60 написанного в книге стоит смотреть с (не)здоровой долей скепсиса.
Отказываясь от проверяемых исключений, вы отказываетесь от автоматический проверки корректности обработки ошибок при обновлении библиотек, например, и никакие @throws вам не помогут.
Hardcoin
Так в статье именно передача наверх. Да, без исключений, но наверх. Так в чем польза передать наверх сверх result вместо того, что бы передать наверх через исключение? Если у нас опять вложенных функций по дороге, не так-то уж это удобно.
amarao
Есть паттерн, когда вы передаёте ошибку без обработки: If Err: return Err.
А разница в том, что exception рейзится где попало, а result — он всегда на выходе функции и надо явно ответить «что с этой ошибкой делать».
Фактически, исключения, это такой Result, для которого поведение по умолчанию (если ничего не сказано — передать дальше). Проблема в том, что это умолчание неявное и оно строго нарушает дзен питона «явное лучше неявного».
BlessMaster
На один уровень наверх, а не "кто его знает куда" наверх.
С необходимостью на этом уровне тоже принять решение что делать с ошибкой.
Hardcoin
Это если на один уровень вверх она ловится. Это совсем не обязательно. Недавно писал модуль, который разбирает эксель. Бросал исключения, если выяснялось, что файл ошибочен. Зачем мне ловить исключение на уровень выше? С ним там нечего делать.
Решение принимает самый верхний уровень модуля — файл разобран быть не может, отказ. По стеку это 2-3 уровня вверх. Передавать статусы на эти два уровня было бы неудобно, не вижу ни одной причины.
Sly_tom_cat
Вы это Go-шникам попробуйте объяснить :)
fukkit
Их не умеют, не хотят, лень.
PsyHaSTe
Вот тут более подробно можно почитать:
www.lighterra.com/papers/exceptionsharmful
Вкратце: неявность (можно легко отстрелить ногу, забыв покрыть где-нибудь какой-то случай), плохо подходит к method-chain вызовам методов (потому что эксепшны хорошо работают со statement, но плохо с expression), как следствие не очень дружит с многопотоком.
Но очень рекомендую ознакомиться со статьей. Кстати, название статьи слегка намекает на сходство с goto.