列表推导、map和numpy.vectorize性能比较
我有一个叫做foo(i)的函数,它需要一个整数作为输入,而且执行起来非常耗时。请问,下面这些初始化的方式,性能上会有很大的差别吗:
a = [foo(i) for i in xrange(100)]
a = map(foo, range(100))
vfoo = numpy.vectorize(foo)
a = vfoo(range(100))
(我不在乎输出是列表还是numpy数组。)
有没有更好的方法呢?
4 个回答
如果一个函数本身执行起来需要很长时间,那么你把它的输出结果放到一个数组里其实没什么关系。不过,一旦你开始处理上百万个数字的数组时,使用numpy可以帮你节省很多内存。
你为什么要优化这个呢?你有没有写出可以正常运行并经过测试的代码,然后检查过你的算法,看看优化这个部分是否真的会有帮助?你是在一个深层循环中发现自己花了很多时间在这里吗?如果不是,那就别费心了。
你只有通过计时才能知道哪个方法对你来说最快。为了有效地计时,你需要根据你的实际使用情况来进行调整。例如,在列表推导中调用一个函数和直接写一个表达式,性能差异可能会很明显;但你可能并不清楚自己到底是想要前者,还是为了让情况看起来相似而简化成了那样。
你说不管最后得到的是numpy数组还是一个
list
都没关系,但如果你在做这种微优化,那其实是有区别的,因为它们在后续使用时的表现会不同。找出这一点可能会有点棘手,所以希望最后发现整个问题其实是多余的。通常来说,使用合适的工具来完成工作会更清晰、更易读等等。我很少会在这些选择之间感到困惑。
- 如果我需要numpy数组,我就会使用它们。我会用它们来存储大型的同类数组或多维数据。我经常使用它们,但很少在我认为想用列表的地方使用。
- 如果我使用numpy,我会尽量让我的函数在一开始就已经向量化,这样就不需要用到
numpy.vectorize
。比如,下面的times_five
可以直接在numpy数组上使用,不需要任何修饰。
- 如果我没有理由使用numpy,也就是说,如果我不是在解决数值数学问题,或者不需要使用特殊的numpy功能,或者不需要存储多维数组等等……
- 如果我有一个已经存在的函数,我会使用
map
。这就是它的用途。 - 如果我有一个可以放在小表达式里的操作,而不需要函数,我会使用列表推导。
- 如果我只是想对所有情况执行这个操作,但不需要存储结果,我会用普通的for循环。
- 在很多情况下,我实际上会使用
map
和列表推导的懒惰等价物:itertools.imap
和生成器表达式。这些在某些情况下可以减少内存使用,并且有时可以避免执行不必要的操作。
如果最终发现性能问题确实出在这里,搞清楚这些事情是很棘手的。人们常常会在实际问题上计时错误的简单案例。更糟糕的是,很多人会基于这些错误的案例制定出愚蠢的通用规则。
考虑以下情况(timeme.py的代码在下面)
python -m timeit "from timeme import x, times_five; from numpy import vectorize" "vectorize(times_five)(x)"
1000 loops, best of 3: 924 usec per loop
python -m timeit "from timeme import x, times_five" "[times_five(item) for item in x]"
1000 loops, best of 3: 510 usec per loop
python -m timeit "from timeme import x, times_five" "map(times_five, x)"
1000 loops, best of 3: 484 usec per loop
一个天真的观察者可能会得出结论,map是这些选项中表现最好的,但答案仍然是“这要看情况”。考虑一下使用你所用工具的好处:列表推导让你避免定义简单的函数;如果你做对了事情,numpy可以让你在C语言中进行向量化。
python -m timeit "from timeme import x, times_five" "[item + item + item + item + item for item in x]"
1000 loops, best of 3: 285 usec per loop
python -m timeit "import numpy; x = numpy.arange(1000)" "x + x + x + x + x"
10000 loops, best of 3: 39.5 usec per loop
但这还不是全部——还有更多。考虑一下算法变化的力量。这可能会更加显著。
python -m timeit "from timeme import x, times_five" "[5 * item for item in x]"
10000 loops, best of 3: 147 usec per loop
python -m timeit "import numpy; x = numpy.arange(1000)" "5 * x"
100000 loops, best of 3: 16.6 usec per loop
有时候,算法的改变可能会更有效。随着数字的增大,这种效果会越来越明显。
python -m timeit "from timeme import square, x" "map(square, x)"
10 loops, best of 3: 41.8 msec per loop
python -m timeit "from timeme import good_square, x" "map(good_square, x)"
1000 loops, best of 3: 370 usec per loop
即使现在,这一切可能对你的实际问题影响不大。看起来numpy非常棒,如果你能正确使用它,但它也有局限性:这些numpy示例中没有使用实际的Python对象在数组里。这会让事情变得复杂,甚至很多。而如果我们真的使用C数据类型呢?这些比Python对象要脆弱。它们不能为null。整数会溢出。你需要额外的工作来提取它们。它们是静态类型的。有时候这些事情会成为问题,甚至是意想不到的问题。
所以,结论就是:一个明确的答案。“这要看情况。”
# timeme.py
x = xrange(1000)
def times_five(a):
return a + a + a + a + a
def square(a):
if a == 0:
return 0
value = a
for i in xrange(a - 1):
value += a
return value
def good_square(a):
return a ** 2
首先要说的是,不要在你的示例中混用 xrange(
) 和 range()
,这样会让你的问题变得不成立,因为你是在比较不同的东西。
我同意 @Gabe 的观点,如果你有很多大的数据结构,使用 numpy 整体上会更好……不过要记住,大多数情况下 C 语言比 Python 快,但 PyPy 通常又比 CPython 快。:-)
关于列表推导式和 map()
函数的比较……一个会调用 101 次函数,而另一个会调用 102 次。这意味着在时间上你不会看到明显的差别,下面用 timeit 模块来演示,就像 @Mike 提到的那样:
列表推导式
$ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
1000000 loops, best of 3: 0.216 usec per loop
$ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
1000000 loops, best of 3: 0.21 usec per loop
$ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
1000000 loops, best of 3: 0.212 usec per loop
map()
函数调用$ python -m timeit "def foo(x):pass; map(foo, range(100))"
1000000 loops, best of 3: 0.216 usec per loop
$ python -m timeit "def foo(x):pass; map(foo, range(100))"
1000000 loops, best of 3: 0.214 usec per loop
$ python -m timeit "def foo(x):pass; map(foo, range(100))"
1000000 loops, best of 3: 0.215 usec per loop
不过要注意,除非你打算使用你用这两种方法创建的列表,否则尽量避免使用列表。换句话说,如果你只是想遍历这些元素,而不需要保存它们,创建一个可能很大的列表在内存中是没有必要的,处理完每个元素后就可以丢弃列表。
在这种情况下,我强烈推荐使用生成器表达式,因为它们不会在内存中创建整个列表……这是一种更节省内存的懒惰迭代方式,可以逐个处理元素,而不需要在内存中创建一个较大的数组。最棒的是,它的语法几乎和列表推导式一样:
a = (foo(i) for i in range(100))
仅限 2.x 用户:如果你在进行更多的迭代,将所有的 range()
调用改为 xrange()
,然后在迁移到 Python 3 时再改回 range()
,因为 xrange()
在 Python 3 中被替换并重命名为 range()
。