`if key in dict` 和 `try/except` - 哪个更易读?

114
我对成语和可读性有疑问,这个特定情况下似乎存在 Python 哲学的冲突:
我想从字典 B 构建字典 A。如果在 B 中找不到特定的键,则什么也不做继续执行。
哪种方式更好?
try:
    A["blah"] = B["blah"]
except KeyError:
    pass
或者
if "blah" in B:
    A["blah"] = B["blah"]

“Do and ask for forgiveness”与“简单明了”哪个更好?为什么?


1
第二个例子最好写成 if "blah" in B.keys() 或者 if B.has_key("blah") - girasquid
2
A.update(B) 对你没起作用吗? - SilentGhost
22
@Luke:has_key已经被弃用,推荐使用in来检查元素是否在字典中。同时,检查B.keys()会将一个O(1)的操作变成一个O(n)的操作。 - kindall
4
@Luke:不是这样的。在Python2中,.has_key已经被弃用,在使用keys()时会创建不必要的列表,并且在Python3中是多余的。请注意不要改变原意。 - SilentGhost
2
“build” A,就是说A一开始是空的?而且我们只想要特定的键吗?使用推导式:A = dict((k, v) for (k, v) in B if we_want_to_include(k)) - Karl Knechtel
显示剩余7条评论
11个回答

85

异常不是条件语句。

条件语句版本更加清晰。这很自然:这是直接的流程控制,这也是条件语句的设计目的,而不是异常。

异常版本主要用于在循环中进行查找时进行优化:对于某些算法,它允许消除内部循环中的测试。但在这里并没有这种好处。它有一个小优点,就是避免了两次输入"blah",但如果你经常这么做,你应该考虑使用一个辅助函数move_key

总的来说,除非你有特定的原因不这样做,我强烈建议默认使用条件语句版本。条件语句是明显的解决方案,通常建议优先考虑一种解决方案而不是另一种。


3
我不同意。如果你说“做 X,如果不行就做 Y”,这种条件解决方案的主要问题在于你需要更频繁地输入“blah”,这会导致出现更多错误的情况。 - glglgl
6
在Python中,尤其是EAFP非常广泛地使用。 - glglgl
8
除了 Python 以外,我所知道的任何语言都可以使用这个答案。 - Tomáš Zato
4
如果你在Python中把异常当做条件语句使用,我希望没有其他人需要阅读它。 - Glenn Maynard
那么,最终的裁决是什么?:) - floatingpurr
1
我在Python和线程方面是一个完全的新手,但我认为使用EAFP是一种原子操作。使用条件语句则不是。如果原子事务很重要,那么除非这个字典是线程安全的字典,否则这些条件语句也不是线程安全的。 - Isen Ng

84

如果查找操作比较耗费时间,还有一种避免异常和双重查找的第三种方法:

value = B.get("blah", None)
if value is not None: 
    A["blah"] = value

如果你希望字典中包含None值,你可以使用一些更加神秘的常量,比如NotImplementedEllipsis或者自己创建一个新的:

MyConst = object()
def update_key(A, B, key):
    value = B.get(key, MyConst)
    if value is not MyConst: 
        A[key] = value

无论如何,对于我来说,使用update()是最易读的选项:

a.update((k, b[k]) for k in ("foo", "bar", "blah") if k in b)

14

据我理解,您想要使用字典B中的键值对更新字典A。

update是更好的选择。

A.update(B)

示例:

>>> A = {'a':1, 'b': 2, 'c':3}
>>> B = {'d': 2, 'b':5, 'c': 4}
>>> A.update(B)
>>> A
{'a': 1, 'c': 4, 'b': 5, 'd': 2}
>>> 

如果B中不存在特定的键,则不复制任何值。抱歉,我应该表述得更清楚,但我只想复制B中存在的特定键的值,而不是B中的所有键。 - LeeMobile
1
@LeeMobile - A.update({k: v for k, v in B.iteritems() if k in specificset})@LeeMobile - A.update({k: v for k, v in B.iteritems() if k in specificset}) - Omnifarious

10

摘自Python性能维基的直接引用:

除了第一次之外,每次检测单词时,if语句的测试都会失败。如果你要计数大量单词,很可能会有多个单词出现多次。在只需要初始化值一次且该值的增加将发生多次的情况下,使用try语句更便宜。

因此,根据情况选择两种选项都是可行的。有关更多详细信息,请查看此链接:Try-except-performance


这是一篇有趣的阅读,但我认为它有些不完整。使用的字典只有一个元素,我怀疑更大的字典将会对性能产生重大影响。 - user2682863

3
我认为第二个例子是您应该采用的,除非这段代码很好理解:
try:
    A["foo"] = B["foo"]
    A["bar"] = B["bar"]
    A["baz"] = B["baz"]
except KeyError:
    pass

请记住,只要键不在 B 中,代码就会终止。如果这段代码有意义,那么应该使用异常方法,否则使用测试方法。在我看来,因为它更简短并明确表达了意图,所以比异常方法更容易阅读。
当然,建议你使用 update。如果您正在使用支持字典推导的 Python 版本,则强烈建议使用以下代码:
updateset = {'foo', 'bar', 'baz'}
A.update({k: B[k] for k in updateset if k in B})

1
请记住,只要有一个键不在B中,代码就会中止。这就是为什么最好将绝对最少的内容放在try块中的最佳实践,通常只有一行。第一个示例最好作为循环的一部分,例如for key in ["foo", "bar", "baz"]: try: A[key] = B[key] - Zim

3

我认为这里的一般规则是,如果A["blah"]通常存在,那么使用try-except是好的,如果不存在,则使用if "blah" in b:

我认为"try"在时间上比较廉价,但"except"更昂贵。


10
默认情况下不要从优化的角度来处理代码;应该从可读性和可维护性的角度来处理。除非目标是优化,否则这是错误的标准(如果是优化,答案是基准测试,而不是猜测)。 - Glenn Maynard
我应该把最后一点用括号或其他方式表述得更加模糊 - 我的主要观点是第一个,并且我认为它还具有第二个的附加优势。 - neil

2
Python 3.8 开始,并引入 赋值表达式(PEP 572) (:= 运算符),我们可以将条件值 dictB.get('hello', None) 捕获到变量 value 中,以便在条件体内既检查它是否不是 None (因为 dict.get('hello', None) 返回关联值或 None),然后在条件体中使用它:
# dictB = {'hello': 5, 'world': 42}
# dictA = {}
if value := dictB.get('hello', None):
  dictA["hello"] = value
# dictA is now {'hello': 5}

3
如果value等于0,那么这就失败了。 - Eric
请注意,dict.get(key, None)dict.get(key)是相同的。(文档) - Grilse
如果目标值恰好为 None 呢?例如,dictA["A"]=None。 - kilasuelika

2

在其他语言中,规则是将例外保留给异常情况,即在常规使用中不会发生的错误。不知道这条规则如何适用于Python,因为按照这个规则,StopIteration不应该存在。


我认为这个陈词滥调源自那些异常处理代价昂贵且可能对性能产生重大影响的语言。我从未见过任何真正的理由或解释。 - John La Rooy
@JohnLaRooy 不,性能并不是真正的原因。异常是一种非本地跳转,有些人认为这会影响代码的可读性。然而,在Python中以这种方式使用异常被认为是惯用法,因此上述情况不适用。 - Ian Goldby
条件返回也被称为“非局部跳转”,许多人更喜欢这种风格,而不是检查代码块末尾的标志。 - cowbert

1

就我个人而言,我更倾向于使用第二种方法(但使用has_key):

if B.has_key("blah"):
  A["blah"] = B["blah"]

那么,每个赋值操作仅有两行(而不是try/except中的4行),并且抛出的任何异常都将是真正的错误或您错过的内容(而不仅仅是尝试访问不存在的键)。正如(请参见您问题的评论),has_key已弃用-因此,我想最好写成。
if "blah" in B:
  A["blah"] = B["blah"]

1
尽管接受的答案强调“三思而后行”的原则适用于大多数语言,但基于 Python 原则的第一种方法可能更具 Pythonic 风格。更不用说这是 Python 中一种合法的编码风格了。重要的是确保在正确的上下文中使用 try except 块并遵循最佳实践。例如,在 try 块中执行太多操作、捕获非常广泛的异常,或者更糟糕的是使用裸的 except 子句等。

宁愿请求原谅,而不是事先获得许可。(EAFP)

请参阅 Python 文档的引用here

此外,核心开发人员之一 Brett 的 blog 简要涵盖了大部分内容。

请参阅另一个 SO 讨论 here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接