【问题标题】:Check if in dict or try/except which has better performance in python?检查是否在 dict 或 try/except 中哪个在 python 中具有更好的性能?
【发布时间】:2012-12-01 12:06:42
【问题描述】:

我有一些包含类似数据的字典。

大多数查询将在一个字典的一次搜索中得到解决。

那么,省略对字典中是否存在键的初步检查并尝试在except子句中捕获键错误的下一个字典是否更好?

或者类似的东西

# d1, d2, d3 = bunch of dictionaries

value = d1.get(key, d2.get(key, d3.get(key, 0)))

?

【问题讨论】:

  • 试试看。 Python 有一个很棒的 timeit 模块,它允许你比较什么是最好的、性能方面的。请注意,性能远不如可读性重要,除非它是一个已证明的瓶颈。以最清晰的方式编写它,然后在需要时进行优化。还值得注意的是,您的第一个方法(检查密钥是否存在)引入了潜在的竞争条件。
  • 这实际上也取决于您的代码。如果字典中没有键是不好的,那么 try:except 更合乎逻辑,如果没有键是微不足道的,那么使用 get 和默认的None,检查一个值是否在字典中不是你的事真的“需要”做,你应该总是知道你的字典里有什么,检查意味着你收到了一个未知的字典。
  • 您的dict.get 字符串将查看密钥是否在d3d2d1 中,因为要解析d1 的方法调用,您需要知道传递给get的参数
  • 你可以使用Chained map recipe,或者,在python 3.3上,新的ChainMap type
  • FWIW 在我的简单测试中使用新发现的神奇时间,链式地图比 try:except 块更快。

标签: python dictionary


【解决方案1】:

取决于字典中的键。

如果您有信心预测缺少键的情况更为常见,请使用 get。

如果您有把握地预测键更常见,请使用 try except。

【讨论】:

  • 请提供支持证据,证明 dict 会根据是否存在密钥而改变 KeyError 的行为。
  • 请看我的回答,实际结果看起来与你的假设不同
【解决方案2】:

由于您说大多数查询将通过查看第一个字典来解决,您的 fastest 解决方案将是执行以下操作:

try:
    item = d1[key]
except KeyError:
    try:
        item = d2[key]
    except KeyError:
        ...

但是,这肯定不是最可维护的解决方案,我不建议使用它。你可以创建一个函数:

def get_from(item,dicts):
    for d in dicts:
        try:
           return d[item]
        except KeyError:
           pass
    else:
        raise KeyError("No item in dicts")

你会这样称呼:

get_from(key,(d1,d2,d3))

(这是@MartijnPieters 在原始问题的 cmets 中建议的已经非常简单的链式地图配方的简化版,稍微不那么干净,我建议在此处发布的代码上使用它。此代码仅以更简化的方式展示概念。)

最后,也许混合解决方案在实践中效果最好。将第一个try 排除在循环之外——这有点难看,但它避免了loop 大部分时间的开销。只有当第一个 try 引发 KeyError 时,您才会输入我在上面建议的剩余字典的循环类型解决方案。例如:

try:
   item = d1[key]
except KeyError:
   item = get_from(key,(d2,d3))

再一次,只有当你能够可靠地证明(想想timeit)它会产生可衡量的差异时才这样做


要知道的重要一点是,在 python 中,try 很便宜,但except 花费相当多的时间。如果您的代码预计会成功,请使用try-except。如果预计不会成功,通常最好还是使用try-except,但在这种情况下,你应该评估性能是否真的是一个问题,只有当你能证明这是一个问题时,你才应该求助于“寻找在你跳跃之前”。

最后一点,如果字典是相对静态的,可能值得将它们组合成 1 dict

d1.update(d2)
d1.update(d3)

现在您可以只使用d1 -- 它包含来自d2d3 的所有信息。 (当然,如果 dicts 具有相同但具有不同值的键,则更新的顺序很重要)。

【讨论】:

    【解决方案3】:

    你也可以这样做

    sentinel = object()
    values = (d.get(key, sentinel) for d in (d1, d2, d3))
    value = next(v for v in values if v is not sentinel)
    

    如果没有任何字典包含密钥,则会引发 StopIteration 而不是 KeyError

    【讨论】:

    • 我也想过这个——但问题是创建小型生成器表达式可能有点贵。再说一次,我的解决方案中的 for 循环也可以......我不确定是否有任何优化的方式来真正做这种事情。
    【解决方案4】:

    检查条件的区别

    if 'key' in a_dict 或类似地, if a_dct.get('key') == None

    和处理not 'key' in a_dict 时抛出的KeyError 通常被认为是微不足道的,并且可能取决于您正在使用的python 的实现。

    使用条件形式无疑更符合 Python 风格,通常被认为比捕获异常更具表现力,通常会导致代码更简洁。但是,如果您的字典可能包含任意数据,并且您不知道 None 的值或其他一些魔术值表示您的键未找到,则使用条件形式将需要两次查找,因为您首先检查是否键在字典中,然后检索值。即:

    if 'key': in a_dict:
       val = a_dcit['key']
    

    鉴于您描述的情况,您提供的代码可能是最慢的选项,因为将在每个字典中查找 key。更快的选择是猜测它所在的字典,然后依次搜索其他字典:

    my_val = d1.get(key,None)
    
    if my_val == None:
        my_val = d2.get(key,None)
        if my_val == None:
            my_val = d3.get(key,None)
            if my_val == None:
                return False #handle not found in any case
    

    但是,您的特定用例听起来既有趣又奇怪。为什么有多个具有相似数据的字典?这些字典是如何存储的?如果您已经有一个列表或其他一些数据结构来保存这些字典,那么循环遍历这些字典会更有表现力。

    dict_list = [{},{},{}] #pretend you have three dicts in a list
    
    for d in dict_list:
       val = d.get('key',None)
       if val == None:
          break
    #val is now either None, or found.
    

    【讨论】:

    • 我不认为条件比捕获异常更 Pythonic。您在这里有一个竞争条件,使用异常是 EAFP 而不是 LBYL。
    • 关于 EAFP 与 EAFP 的公平评论LBYL,给定docs.python.org/2/glossary.html。这里介绍的所有解决方案都受到竞争条件的影响。如果线程安全是一个问题,在列表上使用锁就足以防止竞争条件。
    【解决方案5】:

    try...except 通常比使用get 需要更长的时间,但这取决于一些事情......

    尝试使用 timeit 模块在您的特定情况下测试性能,如下所示:

    def do_stuff():
        blah
    
    timeit.timeit('testfunc()', 'from __main__ import do_stuff as testfunc')
    

    【讨论】:

      【解决方案6】:

      似乎在几乎所有情况下,使用get 会更快。这是我使用try..except 运行的测试并得到

      >>> def foo1(n):
          spam = dict(zip(range(-99,100,n),[1]*200))
          s = 0
          for e in range(1,100):
              try:
                  s += spam[e]
              except KeyError:
                  try:
                      s += spam[-e]
                  except KeyError:
                      s += 0
          return s
      
      >>> def foo2(n):
          spam = dict(zip(range(-99,100,n),[1]*200))
          s = 0
          for e in range(1,100):
              s += spam.get(e, spam.get(-e,0))
          return s
      
      
      >>> for i in range(1,201,10):
          res1 =  timeit.timeit('foo1({})'.format(i), setup = "from __main__ import foo1", number=1000)
          res2 =  timeit.timeit('foo2({})'.format(i), setup = "from __main__ import foo2", number=1000)
          print "{:^5}{:10.5}{:10.5}{:^10}{:^10}".format(i,res1,res2,foo1(i),foo2(i))
      
      
        1    0.075102  0.082862    99        99    
       11     0.25096  0.054272    9         9     
       21      0.2885  0.051398    10        10    
       31     0.26211  0.060171    7         7     
       41     0.26653  0.053595    5         5     
       51      0.2609  0.052511    4         4     
       61      0.2686  0.052792    4         4     
       71     0.26645  0.049901    3         3     
       81     0.26351  0.051275    3         3     
       91     0.26939  0.051192    3         3     
       101      0.264  0.049924    2         2     
       111     0.2648  0.049875    2         2     
       121    0.26644  0.049151    2         2     
       131    0.26417  0.048806    2         2     
       141    0.26418  0.050543    2         2     
       151    0.26585  0.049787    2         2     
       161    0.26663  0.051136    2         2     
       171    0.26549  0.048601    2         2     
       181    0.26425  0.050964    2         2     
       191     0.2648  0.048734    2         2     
      >>>
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-05-09
        • 1970-01-01
        • 2011-05-29
        • 1970-01-01
        • 2011-11-24
        • 1970-01-01
        • 2018-03-19
        • 2013-04-14
        相关资源
        最近更新 更多