【问题标题】:`if key in dict` vs. `try/except` - which is more readable idiom?`if key in dict` 与 `try/except` - 哪个更易读?
【发布时间】:2011-05-29 14:14:09
【问题描述】:

我有一个关于习语和可读性的问题,对于这种特殊情况,Python 哲学似乎存在冲突:

我想从字典 B 构建字典 A。如果 B 中不存在特定键,则什么都不做并继续。

哪种方式更好?

try:
    A["blah"] = B["blah"]
except KeyError:
    pass

if "blah" in B:
    A["blah"] = B["blah"]

“做事并请求原谅”与“简单明了”。

哪个更好,为什么?

【问题讨论】:

  • 第二个例子最好写成if "blah" in B.keys()if B.has_key("blah")
  • A.update(B) 不适合你吗?
  • @Luke: has_key 已被弃用,取而代之的是 in 并且检查 B.keys() 会将 O(1) 操作更改为 O(n) 操作。
  • @Luke:不是这样。 .has_key 已弃用,keys 在 py2k 中创建不需要的列表,在 py3k 中是多余的
  • 'build' A,例如,A 一开始是空的?我们只想要某些键?使用理解:A = dict((k, v) for (k, v) in B if we_want_to_include(k)).

标签: python idioms readability defaultdict code-readability


【解决方案1】:

除了讨论可读性之外,我认为性能在某些情况下也很重要。快速的timeit 基准测试表明测试(即“请求许可”)实际上比处理异常(即“请求宽恕”)要快一些。

这是设置基准的代码,生成一个较大的随机键值对字典:

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 一代从实际基准提升到设置中,然后一遍又一遍地寻找 same 键,提供了截然不同的数字:

>>> timeit.timeit(stmt=stmt1, setup=setup, number=100000000)
2.3290171539999847
>>> timeit.timeit(stmt=stmt2, setup=setup, number=100000000)
26.412447488999987

我不想推测这是否强调了测试与异常处理的好处,或者字典是否缓冲了先前查找的结果,从而使基准测试结果偏向于测试……?

【讨论】:

    【解决方案2】:

    还有第三种方法可以避免异常和双重查找,如果查找成本很高,这可能很重要:

    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)
    

    【讨论】:

      【解决方案3】:

      虽然公认的答案强调“先看后跳”原则可能适用于大多数语言,但基于 python 原则,更多的 Pythonic 可能是第一种方法。更不用说它在 python 中是一种合法的编码风格。重要的是确保您在正确的上下文中使用 try except 块并遵循最佳实践。例如。在 try 块中做了太多的事情,捕捉到一个非常广泛的异常,或者更糟糕的是 - 赤裸裸的 except 子句等。

      请求宽恕比请求许可更容易。 (EAFP)

      请参阅 python 文档参考here

      此外,来自核心开发人员之一 Brett 的 blog 简要介绍了大部分内容。

      查看另一个 SO 讨论 here:

      【讨论】:

        【解决方案4】:

        Python 3.8开始,并引入assignment expressions (PEP 572):=运算符),我们可以在变量value中捕获条件值dictB.get('hello', None),以便同时检查它是否不是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}
        

        【讨论】:

        • 如果 value == 0 则失败
        • 请注意dict.get(key, None)dict.get(key) 相同。 (docs)
        【解决方案5】:

        直接引用 Python 性能 wiki:

        除了第一次,每次看到一个单词时,if 语句的测试都会失败。如果您计算大量单词,很多单词可能会出现多次。在值的初始化只会发生一次并且该值的增加会发生多次的情况下,使用 try 语句会更便宜。

        因此,根据情况,这两种选择似乎都是可行的。欲了解更多详情,请查看此链接:Try-except-performance

        【讨论】:

        • 读起来很有趣,但我认为有些不完整。使用的字典只有 1 个元素,我怀疑更大的字典会对性能产生重大影响
        【解决方案6】:

        例外不是条件。

        条件版本更清晰。这很自然:这是直接的流控制,这是为条件设计的,而不是例外。

        异常版本主要用作在循环中进行这些查找时的优化:对于某些算法,它允许从内部循环中消除测试。这里没有这个好处。它的一个小优势是它可以避免说两次"blah",但如果你做了很多这些,你可能应该有一个帮助器move_key 函数。

        一般来说,我强烈建议您默认使用条件版本,除非您有特定的理由不这样做。条件是执行此操作的明显方法,通常强烈建议首选一种解决方案而不是另一种解决方案。

        【讨论】:

        • 我不同意。如果你说“做 X,如果这不起作用,做 Y”。反对条件解决方案的主要原因是,您必须更频繁地写"blah",这导致更容易出错。
        • 而且,特别是在 Python 中,EAFP 的使用非常广泛。
        • 这个答案对于我知道的任何语言都是正确的,除了 Python。
        • 如果你在 Python 中像使用条件一样使用异常,我希望没有其他人需要阅读它。
        • 那么,最后的判决是什么? :)
        【解决方案7】:

        我认为除非这段代码有意义,否则你应该使用第二个示例:

        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})
        

        【讨论】:

        • "请记住,只要有不在 B 中的键,代码就会中止。" - 这就是为什么最好只在 try: 块中放置绝对最小值的原因,通常这是一行。第一个示例作为循环的一部分会更好,例如for key in ["foo", "bar", "baz"]: try: A[key] = B[key]
        【解决方案8】:

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

        【讨论】:

        • 我认为这个栗子起源于异常处理成本高昂的语言,因此会对性能产生重大影响。我从未见过任何真正的理由或理由。
        • @JohnLaRooy 不,性能并不是真正的原因。异常是一种non-local goto,有些人认为这会妨碍代码的可读性。但是,以这种方式使用异常在 Python 中被认为是惯用的,因此上述内容不适用。
        • 条件返回也是“非本地 goto”,许多人更喜欢这种风格,而不是在代码块末尾检查哨兵。
        【解决方案9】:

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

        我认为“尝试”在时间上很便宜,但“除外”更昂贵。

        【讨论】:

        • 默认情况下不要从优化的角度来处理代码;从可读性和可维护性的角度来处理它。除非目标是专门优化,否则这是错误的标准(如果是优化,答案是基准测试,而不是猜测)。
        • 我可能应该把最后一点放在括号中或者以某种方式模糊 - 我的主要观点是第一个,我认为它具有第二个的额外优势。跨度>
        【解决方案10】:

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

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

        这样,每个赋值操作只有两行(而不是使用 try/except 时的 4 行),并且抛出的任何异常都将是真正的错误或您错过的事情(而不是仅仅尝试访问不存在的键)那里)。

        事实证明(请参阅问题中的 cmets),has_key 已被弃用 - 所以我想最好写成

        if "blah" in B:
          A["blah"] = B["blah"]
        

        【讨论】:

          【解决方案11】:

          据我了解,您想用字典 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 - A.update({k: v for k, v in B.iteritems() if k in specificset})
          猜你喜欢
          • 1970-01-01
          • 2021-03-15
          • 2011-04-25
          • 2013-12-16
          • 2012-12-01
          • 1970-01-01
          • 2013-02-16
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多