据我所知,监视异常会使程序变慢。
迭代器异常监视器,例如StopIteration
使for
循环变慢吗?
StopIteration
虽然在通常情况下异常监视有一些小的开销,但在迭代器的情况下,处理异常似乎没有任何开销。Python 将迭代器优化为一种特殊情况,因此StopIteration
不涉及任何异常处理程序。(我还会观察到——我可能会遗漏一些东西——很难想出一个for
不隐式使用迭代器的 Python 循环)。
这里有一些例子,首先使用内置range
函数和一个简单的for
循环:
Python 2.7.5
>>> import dis
>>> def x():
... for i in range(1,11):
... pass
...
>>> dis.dis(x)
2 0 SETUP_LOOP 23 (to 26)
3 LOAD_GLOBAL 0 (range)
6 LOAD_CONST 1 (1)
9 LOAD_CONST 2 (11)
12 CALL_FUNCTION 2
15 GET_ITER
>> 16 FOR_ITER 6 (to 25)
19 STORE_FAST 0 (i)
3 22 JUMP_ABSOLUTE 16
>> 25 POP_BLOCK
>> 26 LOAD_CONST 0 (None)
29 RETURN_VALUE
请注意,范围本质上被视为迭代器。
现在,使用一个简单的生成器函数:
>>> def g(x):
... while x < 11:
... yield x
... x = x + 1
...
>>> def y():
... for i in g(1):
... pass
...
>>> dis.dis(y)
2 0 SETUP_LOOP 20 (to 23)
3 LOAD_GLOBAL 0 (g)
6 LOAD_CONST 1 (1)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 6 (to 22)
16 STORE_FAST 0 (i)
3 19 JUMP_ABSOLUTE 13
>> 22 POP_BLOCK
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE
>>> dis.dis(g)
2 0 SETUP_LOOP 31 (to 34)
>> 3 LOAD_FAST 0 (x)
6 LOAD_CONST 1 (11)
9 COMPARE_OP 0 (<)
12 POP_JUMP_IF_FALSE 33
3 15 LOAD_FAST 0 (x)
18 YIELD_VALUE
19 POP_TOP
4 20 LOAD_FAST 0 (x)
23 LOAD_CONST 2 (1)
26 BINARY_ADD
27 STORE_FAST 0 (x)
30 JUMP_ABSOLUTE 3
>> 33 POP_BLOCK
>> 34 LOAD_CONST 0 (None)
37 RETURN_VALUE
注意y
这里和上面基本相同x
,区别是一条LOAD_CONST
指令,因为x
引用了数字 11。同样,我们的简单生成器基本上等同于写成 while 循环的东西:
>>> def q():
... x = 1
... while x < 11:
... x = x + 1
...
>>> dis.dis(q)
2 0 LOAD_CONST 1 (1)
3 STORE_FAST 0 (x)
3 6 SETUP_LOOP 26 (to 35)
>> 9 LOAD_FAST 0 (x)
12 LOAD_CONST 2 (11)
15 COMPARE_OP 0 (<)
18 POP_JUMP_IF_FALSE 34
4 21 LOAD_FAST 0 (x)
24 LOAD_CONST 1 (1)
27 BINARY_ADD
28 STORE_FAST 0 (x)
31 JUMP_ABSOLUTE 9
>> 34 POP_BLOCK
>> 35 LOAD_CONST 0 (None)
38 RETURN_VALUE
同样,处理迭代器或生成器没有特定的开销(range
可能比生成器版本更优化,仅仅是因为它是内置的,但不是由于 Python 处理它的方式)。
最后,让我们看看一个实际的显式迭代器StopIteration
>>> class G(object):
... def __init__(self, x):
... self.x = x
... def __iter__(self):
... return self
... def next(self):
... x = self.x
... if x >= 11:
... raise StopIteration
... x = x + 1
... return x - 1
...
>>> dis.dis(G.next)
7 0 LOAD_FAST 0 (self)
3 LOAD_ATTR 0 (x)
6 STORE_FAST 1 (x)
8 9 LOAD_FAST 1 (x)
12 LOAD_CONST 1 (11)
15 COMPARE_OP 5 (>=)
18 POP_JUMP_IF_FALSE 30
9 21 LOAD_GLOBAL 1 (StopIteration)
24 RAISE_VARARGS 1
27 JUMP_FORWARD 0 (to 30)
10 >> 30 LOAD_FAST 1 (x)
33 LOAD_CONST 2 (1)
36 BINARY_ADD
37 STORE_FAST 1 (x)
11 40 LOAD_FAST 1 (x)
43 LOAD_CONST 2 (1)
46 BINARY_SUBTRACT
47 RETURN_VALUE
现在,在这里我们可以看到生成器函数涉及的指令比这个简单的迭代器少一些,主要与实现的差异和一些与引发StopIteration
异常有关的指令有关。然而,使用这个迭代器的函数与y
上面的完全等价:
>>> def z():
... for i in G(1):
... pass
...
>>> dis.dis(z)
2 0 SETUP_LOOP 20 (to 23)
3 LOAD_GLOBAL 0 (G)
6 LOAD_CONST 1 (1)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 6 (to 22)
16 STORE_FAST 0 (i)
3 19 JUMP_ABSOLUTE 13
>> 22 POP_BLOCK
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE
当然,这些结果基于 Python for 循环将优化迭代器以消除对StopIteration
异常的显式处理程序的需求这一事实。毕竟,StopIteration
异常本质上构成了 Python for 循环操作的正常部分。
关于为什么以这种方式实现,请参阅定义迭代器的 PEP-234 。这专门解决了例外费用的问题:
有人质疑表示迭代结束的异常是否不太昂贵。已经提出了 StopIteration 异常的几种替代方案:一个特殊的值 End 来表示结束,一个函数 end() 来测试迭代器是否完成,甚至重用 IndexError 异常。
特殊值的问题是,如果一个序列曾经包含该特殊值,则对该序列的循环将在没有任何警告的情况下提前结束。如果使用空终止的 C 字符串的经验没有告诉我们这可能导致的问题,想象一下 Python 自省工具会在遍历所有内置名称列表时遇到的麻烦,假设特殊的 End 值是内置的 -名义上!
调用 end() 函数每次迭代需要两次调用。两次调用比一次调用加上一次异常测试要昂贵得多。尤其是时间紧迫的 for 循环可以非常便宜地测试异常。
重用 IndexError 可能会导致混淆,因为它可能是一个真正的错误,过早结束循环会掩盖它。
查看由带有 atry
和except
块的函数生成的字节码的输出,它看起来会稍微慢一些,但是,在大多数情况下这实际上可以忽略不计,因为它对性能的影响非常小。我认为在进行这样的优化时要考虑的真正问题是正确地确定异常的范围。
编译为字节码时带有try
/块的示例函数的输出:except
Python 2.7.3 (default, Apr 10 2012, 23:31:26) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> import dis
>>> def x():
try:
sd="lol"
except:
raise
>>> dis.dis(x)
2 0 SETUP_EXCEPT 10 (to 13)
3 3 LOAD_CONST 1 ('lol')
6 STORE_FAST 0 (sd)
9 POP_BLOCK
10 JUMP_FORWARD 10 (to 23)
4 >> 13 POP_TOP
14 POP_TOP
15 POP_TOP
5 16 RAISE_VARARGS 0
19 JUMP_FORWARD 1 (to 23)
22 END_FINALLY
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE
>>>