【问题标题】:Why `try ... except` is faster than `if`?为什么 `try ... except` 比 `if` 快?
【发布时间】:2015-08-04 08:48:45
【问题描述】:

在我的代码中,我有一个列表l,我正在从中创建一个列表字典。 (我正在对具有相同key 的对象进行分组)。通过try 语句和if 条件实现它,我在line_profiler 中注意到前者似乎更有效:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================

   293  44378450     59805020      1.3     16.9       for element in l:

                                                        # stuff that compute 'key' from 'element'

   302   2234869      2235518      1.0      0.6              try:                                                         
   303   2234869     82486133     36.9     23.3                  d[key].append(element)                              
   304     57358        72499      1.3      0.0              except KeyError:                                             
   305     57358      1758248     30.7      0.5                  d[key] = [element]  

对比:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================

   293  44378450     60185880      1.4     14.0       for element in l:      

                                                        # stuff that compute 'key' from 'element'

   307   2234869     81683512     36.5     19.1              if key in d.keys():                                  
   308   2177511     76393595     35.1     17.8                  d.get(key).append(element)                       
   309                                                       else:                                                
   310     57358      1717679     29.9      0.4                  d[key] = [element]                               

我知道使用try,只有在引发异常时才进入except(因此,除了少数例外,它的总体成本比每次测试条件的成本要低),但在这里,即使是@ 987654330@ 异常 (1.3+30.7 µs) 比测试条件 (36.5 µs) 慢。我认为引发异常比检查一个键是否在字典中更昂贵(in 只是测试哈希键,不是吗?这不是行搜索)。那为什么呢?

【问题讨论】:

  • 这是 Python 2.x 吗?
  • 这是 Python 3.4。顺便说一句,为什么要投反对票?
  • if 版本必须查找key 两次,一次在if 测试中,一次在get 中。如果这是 Python 2,它会更慢,因为 d.keys() 是一个列表,而不是一个视图,所以查找涉及线性搜索而不是基于哈希的集合测试。
  • Pythonic 的方法是使用d = defaultdict(list)。然后d[key].append(element) 将始终有效。
  • 好的,但是您仍然在 if 版本中进行 2 次查找(如果密钥存在)。 FWIW,在Using try vs if in python 中有一些关于ifexcept 相对速度的好信息

标签: python performance try-catch python-3.4


【解决方案1】:

额外的运行时间来自.keys() 调用。如果您想阻止那个额外的电话并仍然与ifelse 保持联系,请尝试以下操作:

obj = d.get(key)
if obj:
      obj.append(element)
else:
      d[key] = [element]

或者,您可以使用 defaultdict 在后台执行此操作。示例:

from collections import defaultdict
d = defaultdict(list)
d['123'].append('abc')

【讨论】:

    【解决方案2】:

    try...except 只有在实际引发的异常数量与执行循环的次数相当时才会变慢。在您的情况下,只有 2.5% 的循环迭代会引发异常。

    下面我们来分析一下四种场景——

    def func1():
        l = [1,2,3,4]
        d = {}
        for e in l:
            k = e - 1
            try:
                d[k].append(e)
            except KeyError:
                d[k] = [e]
        return d
    
    
    def func2():
        l = [1,2,3,4]
        d = {}
        for e in l:
            k = e - 1
            if k in d.keys():
                d.get(k).append(e)
            else:
                d[k] = [e]
        return d
    
    def func3():
        l = [1,2,3,4]
        d = {}
        for e in l:
            k = 1
            try:
                d[k].append(e)
            except KeyError:
                d[k] = [e]
        return d
    
    
    def func4():
        l = [1,2,3,4]
        d = {}
        for e in l:
            k = 1
            if k in d.keys():
                d.get(k).append(e)
            else:
                d[k] = [e]
        return d
    

    这个的计时结果-

    In [7]: %timeit func1()
    The slowest run took 4.17 times longer than the fastest. This could mean that an intermediate result is being cached
    100000 loops, best of 3: 2.55 µs per loop
    
    In [8]: %timeit func2()
    1000000 loops, best of 3: 1.77 µs per loop
    
    In [10]: %timeit func3()
    The slowest run took 4.34 times longer than the fastest. This could mean that an intermediate result is being cached
    1000000 loops, best of 3: 2.01 µs per loop
    
    In [11]: %timeit func4()
    The slowest run took 6.83 times longer than the fastest. This could mean that an intermediate result is being cached
    100000 loops, best of 3: 2.4 µs per loop
    
    1. func1()func2() 的情况下,每个元素都进入一个单独的列表,因此对于每个键,try..except 块引发并捕获异常。在这种情况下,func2() 更快。

    2. func3()func4()的情况下,异常只抛出一次,所以异常的开销只发生一次,而条件仍然检查每个键(即使它存在),和这就是为什么在这种情况下,try..except 更快。


    我猜您的情况可能会发生类似的情况,即多次计算相同的键,因此使用 try..except 块会更快。您可以查看列表中有多少实际元素与字典中有多少键,以了解这是否是原因。

    假设hits 列是特定行执行的次数,你可以看到,行 -

    d[key].append(element)
    

    执行了 2234869 次,而异常只引发了 - 57358 次,仅占元素总数的 2.56%。

    【讨论】:

      【解决方案3】:

      您应该认为,在每次迭代中,如果条件检查,您会花费一些时间进行测试。如果您不引发异常,则使用 try except 您的代码会更快,否则处理异常需要更多时间。

      换句话说,如果你确定你的异常是异常的(它只发生在特殊情况下),那么使用 try-except 会更便宜。否则,如果您在约 50% 的情况下排除异常,则最好使用 if。

      (“请求宽恕比请求许可更容易”)

      【讨论】:

        【解决方案4】:

        我在我的一门课程中了解到,处理器会尝试预测指令并将指令加载到内存中。 如果你放一个 if 语句,处理器不知道要加载哪个代码,并且浪费时间加载/卸载指令等。

        很可能,在发生异常的情况下,处理器假定异常很少,并且在不考虑异常的情况下加载主代码...

        我不知道这里是不是这样,但这是我们在代码优化课中学到的。

        来源(类https://www.etsmtl.ca/etudes/cours/gti320/

        【讨论】:

        • 正如第一个答案所解释的,额外的时间来自.keys()。虽然您的观点也是有效的,但它的影响比.keys() 要小得多。
        猜你喜欢
        • 2011-04-25
        • 2011-10-06
        • 1970-01-01
        • 2021-07-14
        • 2021-06-27
        • 2021-07-11
        • 1970-01-01
        • 2021-03-15
        • 2015-12-02
        相关资源
        最近更新 更多