PEP 463 – Exception-catching expressions
- 作者:
- Chris Angelico <rosuav at gmail.com>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2014年2月15日
- Python 版本:
- 3.5
- 发布历史:
- 2014年2月20日, 2014年2月16日
- 决议:
- Python-Dev 消息
拒绝通知
来自 https://mail.python.org/pipermail/python-dev/2014-March/133118.html
“””我希望拒绝这个PEP。我认为提出的语法在期望的语义下是可以接受的,尽管它仍然有些刺眼。它可能不比lambda的冒号(它模仿了def中的冒号,就像这里的冒号模仿了try/except中的冒号一样)更糟糕,肯定比列出的替代方案好。
但是我无法接受的是动机和基本原理。我认为一旦有了except表达式,例如dict.get()就不会是多余的,并且我不同意EAFP比LBYL更好的立场,或者Python“普遍推荐”EAFP。(你从哪里得到这个?从那些如此痴迷于DRY的来源,宁愿引入一个高阶函数而不是重复一行代码?:-)
这大概是我能说的最多的了。考虑到语言峰会即将来临,我很乐意在那里深入探讨我拒绝它的理由(如果有人需要的话)。
我确实认为(除了从不解释那些可怕的首字母缩略词 :-)这是一个写得很好、研究得很充分的PEP,我认为你在管理讨论、收集反对意见、审查替代方案等方面做得很好,以及将一场激烈的辩论变成PEP所需要的一切。做得好,Chris(以及所有帮助过的人),祝你的下一个PEP好运!“””
摘要
正如 PEP 308 引入了一种在表达式中基于值的条件的方法一样,这个系统允许基于异常的条件作为表达式的一部分使用。
动机
许多函数和方法都有参数,这些参数会导致它们返回指定值而不是引发异常。当前系统是临时的且不一致,并且要求每个函数都要单独编写以具有此功能;并非所有函数都支持此功能。
- dict.get(key, default) - 第二个位置参数代替KeyError
- next(iter, default) - 第二个位置参数代替StopIteration
- list.pop() - 没有方法返回默认值
- seq[index] - 没有方法处理界限错误
- min(sequence, default=default) - 关键字参数代替ValueError
- statistics.mean(data) - 没有方法处理空迭代器
如果Python早期就有这个功能,就不需要创建dict.get()及相关方法了;处理缺键的唯一明显方法是响应异常。一种方法被编写成以一种方式指示缺失,然后使用一种一致的技术来响应缺失。相反,我们有dict.get(),并且在Python 3.4中,我们也有min(… default=default),以及无数其他方法。我们有一个用于在表达式中进行测试的LBYL语法,但目前没有EAFP的表示法;比较以下内容
# LBYL:
if key in dic:
process(dic[key])
else:
process(None)
# As an expression:
process(dic[key] if key in dic else None)
# EAFP:
try:
process(dic[key])
except KeyError:
process(None)
# As an expression:
process(dic[key] except KeyError: None)
Python通常推荐EAFP策略,但必须为此推广诸如dic.get(key,None)之类的实用函数。
基本原理
当前系统要求函数作者预测对默认值的需求,并实现支持。如果未这样做,则需要完整的try/except块。
由于try/except是一个语句,因此无法在表达式中间捕获异常。就像if/else处理条件语句和lambda处理函数定义一样,这个功能允许在表达式上下文中捕获异常。
这为函数提供了一种清晰一致地提供默认值的方法:它只需引发适当的异常,调用者捕获它。
在某些情况下,可以使用LBYL技术(例如,在索引之前检查序列是否有足够的长度)。这在所有情况下都不是安全的,但由于它通常很方便,程序员会倾向于牺牲EAFP的安全性,而选择LBYL的简洁表示法。此外,一些LBYL技术(例如涉及带有三个参数的getattr)会将代码扭曲成看起来像文字字符串而不是属性查找,这会影响可读性。方便的EAFP表示法解决了所有这些问题。
没有方便的方法来编写一个辅助函数来做到这一点;最接近的是使用lambda的丑陋方法
def except_(expression, exception_list, default):
try:
return expression()
except exception_list:
return default()
value = except_(lambda: 1/x, ZeroDivisionError, lambda: float("nan"))
这很笨拙,并且无法处理多个except子句;或者eval
def except_(expression, exception_list, default):
try:
return eval(expression, globals_of_caller(), locals_of_caller())
except exception_list as exc:
l = locals_of_caller().copy()
l['exc'] = exc
return eval(default, globals_of_caller(), l)
def globals_of_caller():
return sys._getframe(2).f_globals
def locals_of_caller():
return sys._getframe(2).f_locals
value = except_("""1/x""",ZeroDivisionError,""" "Can't divide by zero" """)
这甚至更笨拙,并且依赖于实现相关的技巧。(为CPython以外的解释器编写globals_of_caller()和locals_of_caller()留给读者练习。)
提案
就像“or”运算符和三元“if-else”表达式提供了捕获假值并替换它的短路方法一样,这种语法提供了一种捕获异常并替换它的短路方法。
这目前有效
lst = [1, 2, None, 3]
value = lst[2] or "No value"
该提案增加了这个
lst = [1, 2]
value = (lst[2] except IndexError: "No value")
具体来说,提出的语法是
(expr except exception_list: default)
其中expr、exception_list和default都是表达式。首先,计算expr。如果没有引发异常,其值就是整个表达式的值。如果引发了任何异常,将计算exception_list,并且应该返回一个类型或一个元组,就像try/except语句形式一样。任何匹配的异常都将导致相应的default表达式被计算并成为表达式的值。与try/except语句形式一样,不匹配的异常将向上传播。
除非根据生成器表达式的相同规则,括号是完全多余的,否则整个表达式都需要用括号括起来。这确保了嵌套的except表达式能够被正确解析,并允许语法将来的扩展 - 请参阅下面的多个except子句。
请注意,当前提案不允许捕获异常对象。在需要捕获异常对象的情况下,必须使用语句形式。(有关讨论和详细说明,请参阅下文。)
这个三元运算符的优先级介于lambda和if/else之间。
考虑这个两级缓存的例子
for key in sequence:
x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key)))
# do something with x
这不能重写为
x = lvl1.get(key, lvl2.get(key, f(key)))
尽管更短,但它违背了缓存的目的,因为它必须计算一个默认值传递给get()。.get()版本是向后计算的;异常测试版本是向前计算的,正如预期的那样。最近似的有用等价物是
x = lvl1.get(key) or lvl2.get(key) or f(key)
这取决于值是否非零,并且还取决于缓存对象是否支持此功能。
替代方案
在python-ideas上的讨论提出了以下语法建议
value = expr except default if Exception [as e]
value = expr except default for Exception [as e]
value = expr except default from Exception [as e]
value = expr except Exception [as e] return default
value = expr except (Exception [as e]: default)
value = expr except Exception [as e] try default
value = expr except Exception [as e] continue with default
value = default except Exception [as e] else expr
value = try expr except Exception [as e]: default
value = expr except default # Catches anything
value = expr except(Exception) default # Catches only the named type(s)
value = default if expr raise Exception
value = expr or else default if Exception
value = expr except Exception [as e] -> default
value = expr except Exception [as e] pass default
也有人建议创建一个新的关键字,而不是重用现有的关键字。此类提案与最后一种形式具有相同的结构,但用不同的关键字代替“pass”。建议包括“then”、“when”和“use”。此外,在“default if expr raise Exception”提案的上下文中,建议使用新关键字“raises”。
所有涉及“as”捕获子句的形式都已从本提案中推迟,以保持简单性,但以上表格中保留了它们,作为建议的准确记录。
本提案最支持的四种形式,按顺序排列
value = (expr except Exception: default)
value = (expr except Exception -> default)
value = (expr except Exception pass default)
value = (expr except Exception then default)
所有四种形式都保持从左到右的求值顺序:首先是基本表达式,然后是异常列表,最后是默认值。这很重要,因为表达式是惰性求值的。相比之下,上面列出的几种临时替代方案(根据函数的性质)必须急切地求值其默认值。首选形式使用冒号,它通过“except exception_list:”模仿try/except,并模仿lambda的“keyword name_list: subexpression”;它还可以像字典一样将Exception映射到默认值。使用箭头会引入一个许多程序员不熟悉的标记,并且目前没有类似含义,但否则可读性很强。英文单词“pass”具有类似含义(考虑函数参数的常用用法“pass by value/reference”),“pass”已经是关键字,但由于其含义明显不相关,这可能会引起混淆。使用“then”在英语中有意义,但这会引入一个新关键字到语言中——尽管不是常用关键字,但仍然是一个新关键字。
从左到右的求值顺序对于可读性极其重要,因为它与大多数表达式的求值顺序相平行。替代方案,如
value = (expr except default if Exception)
打破了这一点,通过先求值两端,然后再计算中间;虽然这看起来不算太糟(因为异常列表通常是一个常量),但当多个子句相遇时,无论是多个except/if还是现有的if/else,或两者的组合,都会增加混乱。使用首选顺序,子表达式将始终从左到右求值,无论语法如何嵌套。
保留现有表示法,但移动强制括号,我们有以下建议
value = expr except (Exception: default)
value = expr except(Exception: default)
这让人联想到函数调用或字典初始化。冒号不会与引入代码块混淆,但另一方面,新语法保证了惰性求值,而字典则不能。潜在地减少混淆被认为与增加混淆的潜在能力不相称。
使用示例
对于每个示例,都提供了一个大致等价的语句形式,以显示表达式的解析方式。这些并不总是严格等价的,但会达到相同的目的。解释器将一个翻译成另一个是不安全的。
其中许多示例直接取自Python标准库,文件和行号截至2014年2月初是正确的。其中许多模式极其常见。
检索参数,默认为None
cond = (args[1] except IndexError: None)
# Lib/pdb.py:803:
try:
cond = args[1]
except IndexError:
cond = None
如果可用,则从系统中获取信息
pwd = (os.getcwd() except OSError: None)
# Lib/tkinter/filedialog.py:210:
try:
pwd = os.getcwd()
except OSError:
pwd = None
尝试翻译,回退到原始
e.widget = (self._nametowidget(W) except KeyError: W)
# Lib/tkinter/__init__.py:1222:
try:
e.widget = self._nametowidget(W)
except KeyError:
e.widget = W
从迭代器读取,一旦迭代器耗尽就继续处理空行
line = (readline() except StopIteration: '')
# Lib/lib2to3/pgen2/tokenize.py:370:
try:
line = readline()
except StopIteration:
line = ''
检索平台特定的信息(注意DRY改进);这个例子可以进一步发展,将一系列单独的赋值变成一个大的字典初始化
# sys.abiflags may not be defined on all platforms.
_CONFIG_VARS['abiflags'] = (sys.abiflags except AttributeError: '')
# Lib/sysconfig.py:529:
try:
_CONFIG_VARS['abiflags'] = sys.abiflags
except AttributeError:
# sys.abiflags may not be defined on all platforms.
_CONFIG_VARS['abiflags'] = ''
检索带默认值的索引项(类似于dict.get)
def getNamedItem(self, name):
return (self._attrs[name] except KeyError: None)
# Lib/xml/dom/minidom.py:573:
def getNamedItem(self, name):
try:
return self._attrs[name]
except KeyError:
return None
将数字转换为名称,回退到数字
g = (grp.getgrnam(tarinfo.gname)[2] except KeyError: tarinfo.gid)
u = (pwd.getpwnam(tarinfo.uname)[2] except KeyError: tarinfo.uid)
# Lib/tarfile.py:2198:
try:
g = grp.getgrnam(tarinfo.gname)[2]
except KeyError:
g = tarinfo.gid
try:
u = pwd.getpwnam(tarinfo.uname)[2]
except KeyError:
u = tarinfo.uid
查找属性,回退到默认值
mode = (f.mode except AttributeError: 'rb')
# Lib/aifc.py:882:
if hasattr(f, 'mode'):
mode = f.mode
else:
mode = 'rb'
return (sys._getframe(1) except AttributeError: None)
# Lib/inspect.py:1350:
return sys._getframe(1) if hasattr(sys, "_getframe") else None
以EAFP模式执行一些耗时的计算,将除以零处理为一种粘性NaN
value = (calculate(x) except ZeroDivisionError: float("nan"))
try:
value = calculate(x)
except ZeroDivisionError:
value = float("nan")
计算一系列数字的平均值,默认为零
value = (statistics.mean(lst) except statistics.StatisticsError: 0)
try:
value = statistics.mean(lst)
except statistics.StatisticsError:
value = 0
在稀疏的覆盖列表中查找对象
(overrides[x] or default except IndexError: default).ping()
try:
(overrides[x] or default).ping()
except IndexError:
default.ping()
缩小异常捕获范围
以下示例直接取自Python标准库,展示了try/except的范围如何可以方便地缩小。使用try/except语句形式来做到这一点需要一个临时变量,但作为表达式要干净得多。
Lib/ipaddress.py:343
try:
ips.append(ip.ip)
except AttributeError:
ips.append(ip.network_address)
变为
ips.append(ip.ip except AttributeError: ip.network_address)
表达式形式几乎等同于此
try:
_ = ip.ip
except AttributeError:
_ = ip.network_address
ips.append(_)
Lib/tempfile.py:130
try:
dirlist.append(_os.getcwd())
except (AttributeError, OSError):
dirlist.append(_os.curdir)
变为
dirlist.append(_os.getcwd() except (AttributeError, OSError): _os.curdir)
Lib/asyncore.py:264
try:
status.append('%s:%d' % self.addr)
except TypeError:
status.append(repr(self.addr))
变为
status.append('%s:%d' % self.addr except TypeError: repr(self.addr))
在每种情况下,缩小的try/except范围都确保意外的异常(例如,如果“append”拼写错误则为AttributeError)不会被同一个处理器捕获。这不太可能成为将调用拆分为单独一行的理由(如上面五行示例所示),但它是转换带来的一个小的副作用好处。
与其他语言的比较
(感谢Andrew Barnert整理本节。请注意,此处给出的示例不反映提案的当前版本,需要进行编辑。)
Ruby 的“begin…rescue…rescue…else…ensure…end”是一个表达式(可能包含语句)。它具有“as”子句的等效项,以及裸except的等效项。它不使用任何标点符号或关键字在裸except/异常类/带as子句的异常类和值之间。(是的,除非你理解Ruby的语句/表达式规则,否则它是模糊的。)
x = begin computation() rescue MyException => e default(e) end;
x = begin computation() rescue MyException default() end;
x = begin computation() rescue default() end;
x = begin computation() rescue MyException default() rescue OtherException other() end;
就本PEP而言
x = computation() except MyException as e default(e)
x = computation() except MyException default(e)
x = computation() except default(e)
x = computation() except MyException default() except OtherException other()
Erlang 有一个try表达式,如下所示
x = try computation() catch MyException:e -> default(e) end;
x = try computation() catch MyException:e -> default(e); OtherException:e -> other(e) end;
类和“as”名称是强制性的,但你可以用“_”代替其中任何一个。每个还有一个可选的“when”保护,还有一个“throw”子句可以捕获,我这里就不细讲了。要处理多个异常,只需用分号分隔子句,我想这可以映射到Python中的逗号。所以
x = try computation() except MyException as e -> default(e)
x = try computation() except MyException as e -> default(e), OtherException as e->other_default(e)
Erlang还有一个“catch”表达式,尽管使用了相同的关键字,但它完全不同,你不想知道它。
ML系列有两种不同的处理方式,“handle”和“try”;两者之间的区别在于“try”模式匹配异常,这让你有多个except子句和as子句的效果。在这两种形式中,处理器子句在某些方言中用“=>”分隔,在其他方言中用“->”分隔。
为避免混淆,我将用Python风格编写函数调用。
这是 SML 的“handle”
let x = computation() handle MyException => default();;
这是 OCaml 的“try”
let x = try computation() with MyException explanation -> default(explanation);;
let x = try computation() with
MyException(e) -> default(e)
| MyOtherException() -> other_default()
| (e) -> fallback(e);;
就本PEP而言,这些可能类似于
x = computation() except MyException => default()
x = try computation() except MyException e -> default()
x = (try computation()
except MyException as e -> default(e)
except MyOtherException -> other_default()
except BaseException as e -> fallback(e))
许多受ML启发但又不直接相关的学术语言会混用,通常使用更多的关键字和更少的符号。所以,Oz 将映射到Python,就像
x = try computation() catch MyException as e then default(e)
许多Lisp派生语言,如 Clojure,将try/catch实现为特殊形式(如果你不知道这意味着什么,可以将其想象成类函数宏),所以你实际上是这样写的
try(computation(), catch(MyException, explanation, default(explanation)))
try(computation(),
catch(MyException, explanation, default(explanation)),
catch(MyOtherException, explanation, other_default(explanation)))
在Common Lisp中,这可以通过一个稍微笨拙的 “handler-case”宏 来完成,但基本思想是相同的。
Lisp风格令人惊讶地被一些没有宏的语言使用,例如Lua,其中 xpcall 接受函数。用Python风格而不是Lua风格编写lambda
x = xpcall(lambda: expression(), lambda e: default(e))
这实际上返回(true,expression())或(false,default(e)),但我想我们可以忽略那部分。
Haskell实际上在这里与Lua类似(除了全部通过monad完成,当然)
x = do catch(lambda: expression(), lambda e: default(e))
你可以在函数内编写一个模式匹配表达式来决定如何处理它;捕获并重新抛出你不想要的异常足够便宜,以至于成为惯用语。
但Haskell的中缀表示使其更漂亮
x = do expression() `catch` lambda: default()
x = do expression() `catch` lambda e: default(e)
这使得lambda冒号和提案中的except冒号之间的并行关系更加明显
x = expression() except Exception: default()
x = expression() except Exception as e: default(e)
Tcl 拥有Lua的xpcall的另一半;catch是一个函数,当捕获到异常时返回true,否则返回false,并且你以其他方式获取值。它全部围绕着Tcl中所有内容的基础隐式引用和执行而构建,这使得用Python术语来描述比Lisp宏更难,但类似
if {[ catch("computation()") "explanation"]} { default(explanation) }
Smalltalk 也比较难映射到Python。基本版本是
x := computation() on:MyException do:default()
…但这基本上是Smalltalk的传递参数带冒号的语法,而不是其异常处理语法。
延迟的子提案
多个except子句
对用例的审查表明,这种需求不如语句形式那么频繁,并且由于其语法尚未达成共识,因此整个功能被推迟了。
可以使用多个“except”关键字,它们都将捕获原始表达式中引发的异常(仅限此)。
# Will catch any of the listed exceptions thrown by expr;
# any exception thrown by a default expression will propagate.
value = (expr
except Exception1: default1
except Exception2: default2
# ... except ExceptionN: defaultN
)
目前,必须使用以下形式之一
# Will catch an Exception2 thrown by either expr or default1
value = (
(expr except Exception1: default1)
except Exception2: default2
)
# Will catch an Exception2 thrown by default1 only
value = (expr except Exception1:
(default1 except Exception2: default2)
)
不带括号列出多个异常子句是语法错误(参见上文),因此Python的未来版本可以添加此功能而不破坏现有代码。
捕获异常对象
在try/except块中,使用“as”捕获异常对象会创建一个局部名称绑定,并在finally子句中隐式删除该绑定(以避免创建引用循环)。在表达式上下文中,这意义不大,并且需要一个适当的子范围来安全地捕获异常对象 - 类似于列表推导式如何处理。但是,CPython目前通过嵌套函数调用来实现推导式的子范围,这在某些上下文(如类定义)中会产生影响,因此不适合此提案。如果将来有办法创建真正的子范围(这可以简化推导式、except表达式、with块,以及可能更多),那么这个提案就可以恢复;在此之前,失去它也不是什么大问题,因为适用于此处表达式表示法的简单异常处理通常只关注异常的类型,而不关注其值 - 进一步分析如下。
此语法确实允许在交互式Python中方便地捕获异常;返回值由“_”捕获,但异常目前不会。这可以写成
>>> (expr except Exception as e: e)
对Python标准库的审查表明,虽然“as”的使用相当普遍(大约每五个except子句中就有一个使用),但在逻辑上可以转换为表达式形式的情况下,它极其罕见。它的少数用法可以保持不变。因此,为了简单起见,“as”子句不包含在此提案中。后续版本的Python可以在不破坏现有代码的情况下添加此功能,因为“as”已经是关键字。
一个可能有用此处的例子是Lib/imaplib.py:568
try: typ, dat = self._simple_command('LOGOUT')
except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
这可以变成
typ, dat = (self._simple_command('LOGOUT')
except BaseException as e: ('NO', '%s: %s' % (type(e), e)))
或者也许是其他变体。这远非最有说服力的用例,但对这段代码进行智能分析可以显著地对其进行整理。在没有进一步的例子表明有需要异常对象的情况下,我选择无限期推迟此建议。
被拒绝的子提案
finally子句
语句形式的try…finally或try…except…finally没有逻辑上对应的表达式形式。因此,finally关键字不包含在此提案的任何方面。
具有不同含义的裸except
对于提出的几种语法,省略异常类型名称将非常简单且简洁,并且会很诱人。为了方便起见,让裸“except”子句具有比“except BaseException”更有用的含义可能会有好处。建议包括让它捕获Exception,或一些特定的“常见异常”(新类型ExpressionError的子类),或让它查找当前作用域中名为ExpressionError的元组,并具有内置默认值(如(ValueError, UnicodeError, AttributeError, EOFError, IOError, OSError, LookupError, NameError, ZeroDivisionError))。所有这些都被拒绝了,原因如下。
- 首先也是最重要的,与try/except语句形式的一致性将被打破。就像列表推导式或三元if表达式可以通过“分解”为垂直语句形式来解释一样,表达式except也应该能够通过相对机械的翻译成近乎等价的语句来解释。因此,对于这两种形式都通用的任何语法,其语义都应该相同,最重要的是不应该有捕获更多或更少的细微差别,因为它容易导致难以察觉的错误。
- 其次,合适的异常集合本身将是一个巨大的争议点。不可能准确预测哪些异常“有意义”被捕获;为什么用方便的语法来祝福其中一些而不祝福其他一些呢?
- 最后(部分原因是建议一旦减少到“合理”集合的异常,就应该积极鼓励裸except),任何你捕获了你没有预料到捕获的异常的情况都是不必要的bug磁铁。
因此,裸“except”的使用只有两种可能性:要么它在语法上被禁止在表达式形式中使用,要么它被允许,其语义与语句形式完全相同(即,它捕获BaseException并且无法用“as”捕获它)。
裸except子句
PEP 8 正确地建议不要使用裸“except”。虽然在语句中语法上是合法的,并且为了向后兼容必须保持如此,但鼓励其使用价值不大。在表达式 except 子句中,“except:”是语法错误;使用等效的长形式“except BaseException:”代替。Python的未来版本可能会选择恢复这一点,这可以在不破坏兼容性的情况下完成。
except子句周围的括号
是否允许将except子句独立于可能引发异常的表达式进行括号化?例如
value = expr (
except Exception1 [as e]: default1
except Exception2 [as e]: default2
# ... except ExceptionN [as e]: defaultN
)
当包含多个except子句和/或异常捕获的两个延迟子提案中的一个或两个时,这更具说服力。在它们缺席的情况下,括号将是
value = expr except ExceptionType: default
value = expr (except ExceptionType: default)
优势很小,并且可能使读者误以为except子句独立于表达式,或者以为这是一个函数调用,这使得它不具说服力。当然,表达式可以根据需要进行括号化,默认值也可以
value = (expr) except ExceptionType: (default)
由于整个表达式现在被要求用括号括起来(这在当时尚未决定),因此将此部分区分开来的必要性减少了,在许多情况下这是多余的。
“except: pass”的简写
以下被建议为一个类似的简写,尽管技术上不是表达式
statement except Exception: pass
try:
statement
except Exception:
pass
例如,一个常见的用例是尝试删除文件
os.unlink(some_file) except OSError: pass
然而,Python 3.4中已经有一个等价物,在contextlib中
from contextlib import suppress
with suppress(OSError): os.unlink(some_file)
由于这已经是一行(或两行,在冒号后换行),因此几乎不需要新的语法和语句与表达式的混淆来达到这个目的。
常见反对意见
冒号总是引入代码块
虽然Python的许多语法元素使用冒号来引入语句块(if, while, with, for, 等),但这绝不是冒号的唯一用途。目前,Python语法包含四种冒号引入子表达式的情况
- 字典显示 - { … key:value … }
- 切片表示法 - [start:stop:step]
- 函数定义 - parameter : annotation
- lambda - arg list: return value
本提案仅添加第五种
- except-expression - exception list: result
风格指南和 PEP 8 应该建议不要将冒号放在换行符的末尾,这可能会看起来像引入了一个代码块,而是提倡在异常列表之前换行,使冒号清晰地位于两个表达式之间。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0463.rst