有什么例外? 顾名思义,它们是在程序中发生异常时出现的。 您可能会问为什么异常是一种反模式,它们与输入有何关系? 我试图
弄清楚 ,现在我想和你一起讨论这个问题,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中的任何内容都可以停止使用不同类型的异常:除法,函数调用,
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
。 而且,我们并不是在讨论带有重复请求和超时的复杂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
基类具有UserNotFound
和MissingUser
类的错误。 可以在某些特定情况下使用此方法,例如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)
我们将值包含在两个包装器之一中:
Success
或
Failure
。 这些类是从
Result
基类继承的。 可以使用返回的函数在批注中指定打包值的类型,例如,
Result[float, ZeroDivisionError]
返回
Success[float]
或
Failure[ZeroDivisionError]
。
这给了我们什么? 更多的
例外并非例外,但这是预期的问题 。 另外,将异常包装在“
Failure
解决了第二个问题:识别潜在异常的复杂性。
1 + divide(1, 0)
现在,它们很容易发现。 如果您在代码中看到
Result
,则该函数可能会引发异常。 您甚至可以提前知道他的类型。
此外,该库是完全类型化的,并且
与PEP561兼容 。 也就是说,如果您尝试返回与声明的类型不匹配的内容,mypy将警告您。
from returns.result import Result, Success, Failure def divide(first: float, second: float) -> Result[float, ZeroDivisionError]: try: return Success('Done')
如何使用容器?
有两种
方法 :
map
返回正常值的函数;bind
返回其他容器的函数。
Success(4).bind(lambda number: Success(number / 2))
这样做的好处是,此代码将保护您免受失败的脚本的攻击,因为
.bind
和
.map
不会对具有
Failure
容器执行:
Failure(4).bind(lambda number: Success(number / 2))
现在,您可以专注于正确的执行过程,并确保错误的状态不会在意外的地方中断程序。 而且总是有机会
确定错误的状态,进行更正 ,然后返回到流程的构想路径。
Failure(4).rescue(lambda number: Success(number + 1))
在我们的方法中,“所有问题都是单独解决的”,“执行过程现在是透明的”。 享受编程的乐趣!
但是如何从容器中扩展价值呢?
确实,如果您使用对容器一无所知的函数,那么您本身就需要这些值。 然后,您可以使用
.unwrap()
或
.value_or()
方法:
Success(1).unwrap()
等等,我们不得不摆脱异常,现在事实证明,所有
.unwrap()
调用都可能导致另一个异常?
如何不考虑UnwrapFailedErrors?
好的,让我们看看如何使用新的异常。 考虑以下示例:您需要检查用户输入并在数据库中创建两个模型。 每个步骤都可能以异常结束,这就是为什么所有方法都包装在
Result
:
from returns.result import Result, Success, Failure class CreateAccountAndUser(object): """Creates new Account-User pair."""
首先,您根本不必在自己的业务逻辑中扩展价值:
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
第一个使用
@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."""
总结如何摆脱异常并保护代码 :
- 对可能引发异常的所有方法使用
@safe
包装器。 它将函数的返回类型更改为Result[OldReturnType, Exception]
。 - 使用
Result
作为容器,可以将值和错误转换为简单的抽象。 - 使用
.unwrap()
扩展容器中的值。 - 使用
@pipeline
使.unwrap
调用.unwrap
更易于阅读。
通过遵守这些规则,我们可以做完全相同的事情-只有安全且易于阅读。 解决了所有与异常有关的问题:
- “很难发现例外 。 ” 现在,将它们包装在一个类型化的
Result
容器中,这使它们完全透明。 - 恢复正常行为是不可能的 。 ” 现在,您可以安全地将恢复过程委托给调用方。 对于这种情况,有
.fix()
和.rescue()
。 - “执行的顺序尚不清楚 。 ” 现在,它们是具有通常业务流程的一种。 从头到尾。
- “例外并不例外 。 ” 我们知道! 并且我们希望某些事情会出错并且为任何事情做好准备。
用例和限制
显然,您不能在所有代码中都使用这种方法。 对于大多数日常情况而言,它
太安全了 ,并且与其他库或框架不兼容。 但是,您必须完全按照我的说明编写业务逻辑中最重要的部分,以确保系统正确运行并促进将来的支持。
这个话题会让您思考甚至看起来像霍利瓦尔尼吗? 4月5日来到莫斯科Python Conf ++ ,我们将进行讨论! 除了我之外,干Python项目的创始人和Django Channels的核心开发人员Artyom Malyshev也将在那里。 他将更多地讨论干Python和业务逻辑。