python中的next()真的那么快吗?

时间:2015-06-25 02:03:07

标签: python list python-2.7 search optimization

通过这里的一篇帖子,我读到使用next()来搜索和检索列表中第一次出现的元素可能很快。然而,我惊讶地看到传统的for-if-break语法在相当长的时间内表现更好。如果我在分析中犯了错误,请纠正我。 以下是我尝试的内容片段:

>>> def compare_2():
...     device = 'a'
...     l = ['a', 'b', 'c', 'd']
...     z = next((device for x in l if x==device), None)

>>> def compare_1():
...     device = 'a'
...     l = ['a', 'b', 'c', 'd']
...     z = None
...     for x in l:
...             if x == device:
...                     z = device
...                     break

>>> import timeit
>>> t = timeit.Timer(setup='from __main__ import compare_2', stmt='compare_2()')
>>> t.timeit()
1.5207240581512451
>>> t = timeit.Timer(setup='from __main__ import compare_1', stmt='compare_1()')
>>> t.timeit()
0.46623396873474121

我认为这可能会发生,因为我试图搜索和检索列表中的第一个元素作为示例。我也试过最后一个元素并注意到next()的表现并不比以前好。

>>> def compare_2():
...     device = 'd'
...     l = ['a', 'b', 'c', 'd']
...     z = next((device for x in l if x==device), None)
...
>>>
>>> def compare_1():
...     device = 'd'
...     l = ['a', 'b', 'c', 'd']
...     z = None
...     for x in l:
...             if x == device:
...                     z = device
...                     break
...

>>>
>>> t = timeit.Timer(setup='from __main__ import compare_2', stmt='compare_2()')
>>> t.timeit()
1.6903998851776123
>>> t = timeit.Timer(setup='from __main__ import compare_1', stmt='compare_1()')
>>> t.timeit()
0.66585493087768555

在优化代码方面,很想知道何时实际使用next(),何时不知道。 谢谢!

更新: if device in l肯定会更快。 我其实只想把一个简单的案例原型化。我在尝试根据属性匹配从对象列表中检索对象时遇到了这种情况。例如: obj = next(如果obj.value == 1,obj_list中的obj为obj)

3 个答案:

答案 0 :(得分:2)

我认为您的代码不公平。当你打电话给下一个时,你就是在里面创建一个生成器"从头开始"使用内置的python语法。当你在for循环中遍历列表时,for循环将调用列表的 iter 方法,该方法可能返回预先创建的迭代器,或者至少比生成器更有效地创建迭代器。我会重新尝试你手头创建发生器的时间,然后再打电话。另一种更公平地计时的方法可能是使用一个非常大的列表,这样,与遍历查找符合条件的第一个元素相比,创建生成器的开销会很小。

最后,我不认为你应该担心所有语言的python中的这种事情。尽可能写出最清晰的代码,不要过早优化。特别是在像python这样的语言中,优化的路径通常涉及调用用Cthon编写的库和python绑定(例如numpy)或者用C / C ++重写部分代码。

编辑:这里有一些源代码和结果

x = [1] * 1000000
x[500000] = 0

def func1(l):
   ...:     for n in l:
   ...:         if n == 0:
   ...:             break
   ...:         

def func2(l):
   ...:     z = next((n for n in x if n == 0))
   ...:     

%timeit func1(x)
100 loops, best of 3: 10.4 ms per loop

%timeit func2(x)
100 loops, best of 3: 10.4 ms per loop

接下来很好,但说实话,我并不认为我曾经在找到满足某些条件的第一个元素之外使用它。说实话,我是一位经验丰富的python程序员,熟悉这个成语,但如果我不熟悉那么我会发现for循环更清晰。

答案 1 :(得分:2)

next()与生成器结合使用非常有用且快速。不幸的是,您一直使用普通列表作为生成器的源。如果你想充分发挥下一个潜力,你也必须使用发电机。

>>> timeit.timeit('next((i for i in range(1000)))')
10.571892976760864
>>> timeit.timeit('next((i for i in xrange(1000)))')
0.9348869323730469

元素越多,差异越大。使用第二个版本,Python不必处理所有列表,而只需处理第一个元素。

所以next()并不快,但是当使用时,使用iteratabels和generator的概念就是这样。 next()是为他们设计的。

答案 2 :(得分:1)

我想知道是否还会发生其他事情。创建生成器有一些开销,但我认为将条件if x==device放在生成器中会强制它生成整个列表,并在next()运行之前创建一个新列表。

请参阅此示例,比较强制创建新列表的列表推导,以及懒惰并且不强制它的生成器:

>>> from timeit import Timer
>>> # List comprehension forces a new list to be created in memory
>>> def f1():
...     q = [x for x in xrange(1000)]
...     r = q[1]
...     return r
... 
>>> # Generator comprehension does 'lazy' iteration, only when needed
>>> def f2():
...     q = (x for x in xrange(1000))
...     r = next(q)
...     return r
... 
>>> Timer(f1).timeit()
47.420308774268435
>>> Timer(f2).timeit()
1.346566078497844

看到列表理解比较慢,而生成器惰性方法意味着它只在你调用next()时开始迭代,取值并停止。

现在这个例子,唯一的变化是两个都采用if x = 999的最后一个元素:

>>> # List comprehension still forces creation of a new list
>>> # although the list only ends up with one element
>>> # nb. it's the last element
>>> def f1():
...     q = [x for x in xrange(1000) if x == 999]
...     r = q[0]
...     return r
... 
>>> # Generator comprehension is lazy
>>> # nb. it also only returns the last element
>>> def f2():
...     q = (x for x in xrange(1000) if x == 999)
...     r = next(q)
...     return r
... 
>>> Timer(f1).timeit()
37.279105355189984
>>> Timer(f2).timeit()
37.46816399778598

看他们现在基本相同。发电机正在减速。这个条件迫使它做了与列表理解相同的事情,在没有评估整个列表的情况下,它不能懒得只拿一个匹配的东西。

所以我认为在你的例子中,你不是只是看到创建一个生成器的开销,然后像其他人回答的那样调用它,正如我原来的评论所说的那样。

我认为通过包含if x==device条件,您强制生成器构造迭代整个列表,创建一个新的列表对象,用所有结果填充它,然后在新列表上创建一个生成器,然后调用它来获得结果。

因此,在迭代现有列表的for循环中, lot 的开销更大,这不是因为next()本质上很慢。

修改:您可以在生成器表达式添加到Python时在提案中看到它:PEP-0289 - Generator Expressions,在Early Binding vs Late Binding

部分中
  

要求总结绑定第一个表达的推理,Guido提出[5]:

     

考虑sum(x for x in foo())。现在假设foo()中存在一个错误   引发异常,sum()中的一个错误引发了一个异常   在开始迭代其参数之前的异常。哪一个   您希望看到例外吗?如果是那个人,我会感到惊讶   sum()被引发而不是foo()中的那个,因为调用了foo()   是sum()的参数的一部分,我希望参数是   在调用函数之前处理。

     

OTOH,sum(bar(x) for x in foo()),其中sum()和foo()   是无bug的,但bar()引发了异常,我们别无选择   延迟调用bar()直到sum()开始迭代 - 那就是   发电机合同的一部分。 (他们什么也没做   首先调用next()方法。)

换句话说,如果x==device要提出异常,因为列表中的一个项目无法进行比较,例如来自自定义对象的类型错误,您可能希望在next()被调用之前看到该异常,因此强制整个列表被迭代,失去您可能希望看到的生成器延迟的保存,并创建更多列表与for循环相比,对象创建开销。