“请求原谅而不是请求许可”-解释[关闭]
- 2024-12-02 08:42:00
- admin 原创
- 181
问题描述:
我并不是在询问关于这种哲学的个人“宗教”观点,而是询问一些更技术性的东西。
我理解这句话是检验你的代码是否“pythonic”的几个试金石之一。但对我来说,“pythonic”意味着干净、简单和直观,没有加载用于糟糕编码的异常处理程序。
那么,实际的例子。我定义一个类:
class foo(object):
bar = None
def __init__(self):
# a million lines of code
self.bar = "Spike is my favorite vampire."
# a million more lines of code
现在,从程序背景来看,在另一个函数中我想这样做:
if foo.bar:
# do stuff
如果我没有耐心,没有执行初始的 foo = None,我将得到一个属性异常。那么,“请求原谅而不是许可”是否意味着我应该这样做?
try:
if foo.bar:
# do stuff
except:
# this runs because my other code was sloppy?
为什么我最好在 try 块中添加其他逻辑,这样我就可以让类定义更加模糊?为什么不先定义所有内容,然后明确授予权限?
(不要因为我使用 try/except 块而责怪我...我到处都使用它们。我只是认为使用它们来捕获我自己的错误是不对的,因为我并不是一个彻底的程序员。)
或者...我完全误解了“请求原谅”的口头禅?
解决方案 1:
“请求原谅,而不是许可” 反对两种编程风格。“请求许可”是这样的:
if can_do_operation():
perform_operation()
else:
handle_error_case()
“请求原谅”是这样的:
try:
perform_operation()
except Unable_to_perform:
handle_error_case()
在这种情况下,尝试执行操作可能会失败,您必须以某种方式处理操作无法执行的情况。例如,如果操作正在访问文件,则该文件可能不存在。
请求原谅最好有两个主要原因:
在并发世界中(在多线程程序中,或者如果操作涉及程序外部的对象,如文件、其他进程、网络资源等),情况可能会在运行
can_do_operation()
和运行之间发生变化perform_operation()
。所以无论如何你都必须处理错误。您需要使用完全正确的标准来请求权限。如果您做错了,您要么无法执行您可以执行的操作,要么因为您根本无法执行该操作而发生错误。例如,如果您在打开文件之前测试它是否存在,则可能该文件确实存在,但您无法打开它,因为您没有权限。相反,也许文件是在您打开它时创建的(例如,因为它通过网络连接而来,而该网络连接仅在您实际打开文件时才会建立,而不是在您仅查看它是否存在时建立)。
请求原谅的情况的共同点是,您正在尝试执行一项操作,并且您知道该操作可能会失败。
当你写 时foo.bar
, 的不存在bar
通常不被认为是对象 的失败foo
。这通常是程序员的错误:试图以非设计的方式使用对象。Python 中程序员错误的后果是未处理的异常(如果你幸运的话:当然,有些程序员错误无法自动检测到)。因此,如果bar
是对象的可选部分,处理这种情况的正常方法是将一个bar
字段初始化为None
,如果可选部分存在,则将其设置为其他值。要测试 是否bar
存在,请编写
if foo.bar is not None:
handle_optional_part(foo.bar)
else:
default_handling()
你可以缩写if foo.bar is not None:
为if foo.bar:
仅当bar
在解释为布尔值时始终为真时 — 如果bar
可以是 0、[]
或{}
任何其他具有假真值的对象,则需要。如果你正在测试可选部分(而不是在和之间进行测试),is not None
那么它也更清晰。True
`False`
此时你可能会问:为什么不省略bar
不存在时的初始化,而是用处理程序测试它的存在hasattr
或捕获它AttributeError
?因为你的代码只有在两种情况下才有意义:
该对象没有
bar
字段;该对象有一个
bar
字段,其含义就是你所认为的含义。
因此,在编写或决定使用对象时,您需要确保它没有具有bar
不同含义的字段。如果您需要使用没有bar
字段的其他对象,这可能不是您需要适应的唯一事情,因此您可能需要创建一个派生类或将对象封装在另一个类中。
解决方案 2:
dict
经典的“请求原谅而不是请求许可”示例是从可能不存在的值访问。例如:
names = { 'joe': 'Joe Nathan', 'jo': 'Jo Mama', 'joy': 'Joy Full' }
name = 'hikaru'
try:
print names[name]
except KeyError:
print "Sorry, don't know this '{}' person".format(name)
这里说明了可能发生的异常 ( KeyError
),这样您就不必为可能发生的每个错误请求原谅,而只需为自然发生的错误请求原谅。为了进行比较,“先征求许可”方法可能如下所示:
if name in names:
print names[name]
else:
print "Sorry, don't know this '{}' person".format(name)
或者
real_name = names.get(name, None)
if real_name:
print real_name
else:
print "Sorry, don't know this '{}' person".format(name)
try
这种“请求原谅”的例子往往过于简单。在我看来, /块本质上是否比/except
更好并不十分清楚。在执行可能以各种方式失败的操作时,真正的价值要清晰得多——例如解析;使用;访问操作系统、中间件、数据库或网络资源;或执行复杂的数学运算。当存在多种潜在故障模式时,做好获得原谅的准备是非常有价值的。if
`else`eval()
有关您的代码示例的其他说明:
您不需要在每个变量使用周围都加上try
/except
块。那太可怕了。而且您不需要self.bar
在您的中设置,__init__()
因为它已在class
上面的定义中设置。通常在类中定义它(如果它是可能在类的所有实例之间共享的数据)或在__init__()
(如果它是实例数据,则特定于每个实例)。
None
顺便说一句,的值不是未定义的,也不是错误。它是一个特定且合法的值,表示无、零、空或无。许多语言都有这样的值,因此程序员不会“重载” 0
、-1
、''
(空字符串)或类似的有用值。
解决方案 3:
这里有很多很好的答案,我只是想补充一点我到目前为止还没有提到的观点。
经常请求原谅而不是请求许可可以提高绩效。
当你请求权限的时候,每次都要执行额外的操作来请求权限。
当请求原谅时,有时您只需执行额外的操作,即当它失败时。
通常失败的情况很少见,这意味着如果您只是请求权限,那么您几乎不需要执行任何额外操作。是的,当它失败时它会抛出异常,并执行额外操作,但 python 中的异常非常快。您可以在此处查看一些时间:https://jeffknupp.com/blog/2013/02/06/write-cleaner-python-use-exceptions/
解决方案 4:
try
你说得对——和的目的except
不是为了掩盖你草率的编码。这只会导致更草率的编码。
异常处理应该用于处理异常情况(粗心的编码不是异常情况)。但是,通常很容易预测哪些异常情况可能发生。(例如,您的程序接受用户输入并使用它来访问字典,但用户的输入不是字典中的键...)
解决方案 5:
我个人的非宗教观点是,上述咒语主要适用于有记录且易于理解的退出条件和边缘情况(例如 I/O 错误),绝不能被用作草率编程的逃脱惩罚的卡片。
尽管如此,try/except
当有“更好”的替代方案时,通常会使用。例如:
# Do this
value = my_dict.get("key", None)
# instead of this
try:
value = my_dict["key"]
except KeyError:
value = None
至于您的示例,if hasattr(foo, "bar")
如果您无法控制foo
并且需要检查是否符合您的期望,请使用,否则只需使用foo.bar
并让产生的错误成为您识别和修复草率代码的指南。
解决方案 6:
虽然已经有许多高质量的答案,但大多数主要从风格的角度而不是功能的角度来讨论这个问题。
在某些情况下,我们需要请求原谅,而不是请求许可以确保代码正确(在多线程程序之外)。
一个典型的例子是,
if file_exists:
open_it()
在此示例中,文件可能在检查和尝试实际打开文件之间被删除。可以使用以下命令避免这种情况try
:
try:
open_it()
except FileNotFoundException:
pass # file doesn't exist
这出现在各种地方,通常与文件系统或外部 API 一起使用。
解决方案 7:
在 Python 上下文中,“请求原谅而不是请求许可”意味着一种编程风格,即您不会事先检查事物是否符合您的预期,而是处理不符合预期时产生的错误。经典示例是不检查字典是否包含给定的键,如下所示:
d = {}
k = "k"
if k in d.keys():
print d[k]
else:
print "key \"" + k + "\" missing"
但如果密钥丢失,则处理由此产生的错误:
d = {}
k = "k"
try:
print d[k]
except KeyError:
print "key \"" + k + "\" missing"
但是重点不是将if
代码中的每个都替换为try
/ except
;这会使你的代码变得更加混乱。相反,你应该只捕获那些你真正可以处理的错误。理想情况下,这会减少代码中总体错误处理的数量,使其实际目的更加明显。
解决方案 8:
.bar
请求原谅而不是请求许可是为了简化代码。当有合理的预期可能会触发 AttributeError时,代码应该这样写。
try:
print foo.bar
except AttributeError as e
print "No Foo!"
您的代码似乎既请求许可,又请求宽恕:)
问题是,如果您合理地预期某事会失败,请使用 try/catch。如果您不预期某事会失败,但它还是失败了,那么抛出的异常就相当于其他语言中的失败断言。您可以查看意外异常发生的位置,并相应地调整您的代码/假设。