现在将Python异常视为反模式

有什么例外? 顾名思义,它们是在程序中发生异常时出现的。 您可能会问为什么异常是一种反模式,它们与输入有何关系? 我试图弄清楚 ,现在我想和你一起讨论这个问题,harazhiteli。

例外问题


很难发现您每天面对的问题。 习惯和盲目性将错误变成功能,但是让我们尝试以开放的态度看待异常。

很难发现例外


异常有两种类型:“显式”是通过在您正在阅读的代码中直接调用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中的任何内容都可以停止使用不同类型的异常:除法,函数调用, intstr ,生成器,循环中的迭代器,对属性或键的访问。 甚至raise something()也可能导致崩溃。 而且,我什至没有提到输入和输出操作。 并且在不久的将来将不再支持检查的异常

无法恢复正常行为


但恰恰在这种情况下,我们有例外。 让我们只处理ZeroDivisionError ,代码将变为类型安全。

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

现在一切都很好。 但是为什么我们返回0? 为什么不选择1或None ? 当然,在大多数情况下,获取None几乎和异常一样糟(如果不是更糟),但是使用该功能仍然需要依靠业务逻辑和选项。

我们到底分享了什么? 任意数字,任何特定单位或金额? 并非每个选项都易于预见和恢复。 可能会导致您下次使用一个功能时,您需要使用不同的恢复逻辑。

可悲的结论是:每个问题的解决方案都是个别的,具体取决于使用的具体情况。

没有一劳永逸地解决ZeroDivisionError 。 而且,我们并不是在讨论带有重复请求和超时的复杂I / O的可能性。

也许没有必要精确地处理异常发生的地方? 也许只是将其放入代码执行过程中-稍后有人会弄清楚。 然后我们被迫回到目前的局势。

执行过程不清楚


好吧,让我们希望其他人能够捕获并处理该异常。 例如,系统可能要求用户更改输入的值,因为它不能被0 divide并且divide功能不应明确负责从错误中恢复。

在这种情况下,您需要检查我们在哪里捕获到异常。 顺便说一句,如何确定确切的处理位置? 是否可以在代码中找到正确的位置? 事实证明,不, 这是不可能的

无法确定引发异常后将执行哪一行代码。 可以使用except选项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: ,您将不得不几乎以每行开始if something is not None:并且所有逻辑将在清理检查的垃圾后面丢失,否则您将一直遭受TypeError困扰。 不是一个好选择。
  • 为特殊用例编写类。 例如,具有子类的User基类具有UserNotFoundMissingUser类的错误。 可以在某些特定情况下使用此方法,例如Django中的AnonymousUser ,但是将所有可能的错误包装在类中是不现实的。 这将需要太多的工作,并且域模型将变得难以想象的复杂。
  • 使用容器将结果变量或错误值包装在包装器中,然后继续使用容器值。 这就是为什么我们创建@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) 

我们将值包含在两个包装器之一中: SuccessFailure 。 这些类是从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不会对具有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()任何异常,因为不使用.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调用.unwrap更易于阅读。

通过遵守这些规则,我们可以做完全相同的事情-只有安全且易于阅读。 解决了所有与异常有关的问题:

  • “很难发现例外 现在,将它们包装在一个类型化的Result容器中,这使它们完全透明。
  • 恢复正常行为是不可能的 现在,您可以安全地将恢复过程委托给调用方。 对于这种情况,有.fix().rescue()
  • “执行的顺序尚不清楚 现在,它们是具有通常业务流程的一种。 从头到尾。
  • “例外并不例外 我们知道! 并且我们希望某些事情会出错并且为任何事情做好准备。

用例和限制


显然,您不能在所有代码中都使用这种方法。 对于大多数日常情况而言,它太安全了 ,并且与其他库或框架不兼容。 但是,您必须完全按照我的说明编写业务逻辑中最重要的部分,以确保系统正确运行并促进将来的支持。

这个话题会让您思考甚至看起来像霍利瓦尔尼吗? 4月5日来到莫斯科Python Conf ++ ,我们将进行讨论! 除了我之外,干Python项目的创始人和Django Channels的核心开发人员Artyom Malyshev也将在那里。 他更多地讨论干Python和业务逻辑。

Source: https://habr.com/ru/post/zh-CN445234/


All Articles