我有一个关于习语和可读性的问题,对于这种特殊情况,Python 哲学似乎存在冲突:
我想从字典 B 构建字典 A。如果 B 中不存在特定键,则什么也不做并继续。
哪种方式更好?
try:
A["blah"] = B["blah"]
except KeyError:
pass
或者
if "blah" in B:
A["blah"] = B["blah"]
“做并请求宽恕”与“简单明了”。
哪个更好?为什么?
我有一个关于习语和可读性的问题,对于这种特殊情况,Python 哲学似乎存在冲突:
我想从字典 B 构建字典 A。如果 B 中不存在特定键,则什么也不做并继续。
哪种方式更好?
try:
A["blah"] = B["blah"]
except KeyError:
pass
或者
if "blah" in B:
A["blah"] = B["blah"]
“做并请求宽恕”与“简单明了”。
哪个更好?为什么?
例外不是条件。
有条件的版本更清晰。这很自然:这是直接的流控制,这是为条件设计的,而不是例外。
在循环中进行这些查找时,异常版本主要用作优化:对于某些算法,它允许从内部循环中消除测试。这里没有这个好处。它有一个小优势,它避免了说"blah"
两次,但如果你做了很多这些,你可能应该有一个辅助move_key
函数。
一般来说,我强烈建议您默认使用条件版本,除非您有特定的理由不这样做。条件是执行此操作的明显方法,通常强烈建议首选一种解决方案而不是另一种解决方案。
还有第三种方法可以避免异常和双重查找,如果查找成本很高,这可能很重要:
value = B.get("blah", None)
if value is not None:
A["blah"] = value
如果您希望字典包含None
值,您可以使用一些更深奥的常量,例如NotImplemented
,Ellipsis
或创建一个新常量:
MyConst = object()
def update_key(A, B, key):
value = B.get(key, MyConst)
if value is not MyConst:
A[key] = value
无论如何, usingupdate()
对我来说是最易读的选项:
a.update((k, b[k]) for k in ("foo", "bar", "blah") if k in b)
据我了解,您想使用 dict B 中的键、值对更新 dict 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}
>>>
来自 Python 性能 wiki 的直接引用:
除了第一次,每次看到一个单词时,if 语句的测试都会失败。如果您计算大量单词,很多单词可能会出现多次。在值的初始化只会发生一次并且该值的增加会发生多次的情况下,使用 try 语句会更便宜。
因此,根据情况,这两种选择似乎都是可行的。有关更多详细信息,您可能想查看此链接:Try-except-performance
我认为第二个例子是你应该去的,除非这段代码有意义:
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})
我认为这里的一般规则A["blah"]
通常会存在,如果是这样,try-except 是好的,如果不是那么使用if "blah" in b:
我认为“尝试”在时间上很便宜,但“除外”更贵。
其他语言的规则是为异常情况保留例外,即在常规使用中不会发生的错误。不知道该规则如何适用于 Python,因为该规则不应该存在 StopIteration。
开始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}
就个人而言,我倾向于第二种方法(但使用has_key
):
if B.has_key("blah"):
A["blah"] = B["blah"]
这样,每个赋值操作只有两行(而不是使用 try/except 的 4 行),并且抛出的任何异常都将是真正的错误或您错过的事情(而不仅仅是尝试访问不存在的键) .
事实证明(请参阅对您问题的评论),has_key
已弃用 - 所以我想最好写成
if "blah" in B:
A["blah"] = B["blah"]
尽管公认的答案强调“先看再跳”原则可能适用于大多数语言,但基于 python 原则,更多的 Pythonic 可能是第一种方法。更不用说它在 python 中是一种合法的编码风格。重要的是确保您在正确的上下文中使用 try except 块并遵循最佳实践。例如。在 try 块中做了太多的事情,捕获了一个非常广泛的异常,或者更糟糕的是 - 赤裸裸的 except 子句等。
请求宽恕比请求许可更容易。(EAFP)
请参阅此处的 python 文档参考。
此外,核心开发人员之一 Brett 的这篇博客简要介绍了大部分内容。
在此处查看另一个 SO 讨论:
除了讨论可读性之外,我认为性能在某些情况下也很重要。快速时间基准测试表明测试(即“请求许可”)实际上比处理异常(即“请求宽恕”)稍快。
这是设置基准的代码,生成一个随机键值对的大字典:
setup = """
import random, string
d = {"".join(random.choices(string.ascii_letters, k=3)): "".join(random.choices(string.ascii_letters, k=3)) for _ in range(10000)}
"""
然后if
测试:
stmt1 = """
key = "".join(random.choices(string.ascii_letters, k=3))
if key in d:
_ = d[key]
"""
给我们:
>>> timeit.timeit(stmt=stmt1, setup=setup, number=1000000)
1.6444563979999884
而利用异常的方法
stmt2 = """
key = "".join(random.choices(string.ascii_letters, k=3))
try:
_ = d[key]
except KeyError:
pass
"""
给我们:
>>> timeit.timeit(stmt=stmt2, setup=setup, number=1000000)
1.8868465850000575
有趣的是,将key
生成从实际基准提升到设置中,然后一遍又一遍地寻找相同的密钥,提供了截然不同的数字:
>>> timeit.timeit(stmt=stmt1, setup=setup, number=100000000)
2.3290171539999847
>>> timeit.timeit(stmt=stmt2, setup=setup, number=100000000)
26.412447488999987
我不想推测这是否强调了测试与异常处理的好处,或者字典是否缓冲了先前查找的结果,从而使基准测试结果偏向于测试......