【问题标题】:Is next() in python really that fast?python中的next()真的那么快吗?
【发布时间】:2015-09-11 10:48:15
【问题描述】:

通过此处的一篇文章,我了解到使用 next() 搜索和检索列表中第一次出现的元素可能会很快。然而,我很惊讶地看到传统的 for-if-break 语法在相当长的一段时间内表现得更好。如果我在分析中犯了错误,请纠正我。 这是我尝试过的sn-p:

>>> 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 for obj_list if obj.value == 1)

【问题讨论】:

  • 为什么你认为它会更快? (此外,在这种情况下,if device in l 比您计时的任何一个选项都要好。)
  • 如果将列表的大小增加到 1M,例如使用l = range(1000000),然后从比较函数中取出列表结构并将其导入设置中,请设置 device=500000 , 然后你会看到他们有非常相似的时间。 if device in l 更快。我猜只有在不中断 for 循环并坚持遍历整个列表的情况下,使用 next 和生成器表达式才会更快。正如@TessellatingHeckler 指出的那样,创建生成器会给您带来一些开销,因此对于小型列表来说效率很低。

标签: python list python-2.7 search optimization


【解决方案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 循环相比,开销很多,这并不是因为 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() 没有错误,但 bar() 引发异常,我们别无选择 延迟对 bar() 的调用,直到 sum() 开始迭代——那就是 发电机合同的一部分。 (他们什么都不做,直到他们 next() 方法首先被调用。)

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

【讨论】:

  • 很好的解释 TessellatingHeckler!
  • 但是,我仍然想知道从对象列表中检索具有属性匹配的第一个对象的最快方法是什么。
  • 我认为(无需尝试)在纯 Python 中获得的最快速度是 for x in L: if x[y]=='device': return x。最少的新对象或内存流失,没有中断,没有临时变量。如果列表是随机的并且属性是均匀分布的,则最少需要 1 次检查,最多 len(L) 次检查,平均 len(L)/2 次检查,它是 O(n)。如果您可以将循环推到堆栈中的较低位置,您可能会变得更好,例如 min(L) 应该比 tmp=L[0]; for i in xrange(len(L)): if L[i]<tmp: tmp=L[i] ; return tmp 快,但我不能很快想到如何为一般属性匹配做到这一点。
  • 出于某种原因,我只是喜欢使用 next(),因为它“神奇地”根据您的匹配条件为您获得第一个匹配项。我还读到避免点是一种优化形式 (wiki.python.org/moin/PythonSpeed/PerformanceTips)。所以想知道执行如下所示的操作是否具有性能效率: x = None for obj in obj_list: if obj.value == my_val: x = obj break
【解决方案2】:

next() 与生成器结合使用非常有用且快速。不幸的是,您一直使用普通列表作为生成器的来源。如果你想充分发挥 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() 并不快,但是使用迭代标签和生成器的概念是,因此使用时。而next() 就是为他们设计的。

【讨论】:

  • 那么从对象列表中检索第一个基于属性匹配的对象的最快方法是什么?
  • 列表并不是最好的数据结构。您将不得不触摸 evety 项目。如果问题占用了您程序运行时的很大一部分,您可以考虑将它们按键放入字典中。 defaultdict 提供了一种方便的方式来获取项目列表的字典。
【解决方案3】:

我不认为您的代码是一个公平的比较。当您调用 next 时,您正在使用内置的 python 语法“从头开始”创建一个生成器。当您在 for 循环中迭代列表时,for 循环将调用列表的 iter 方法,该方法可能返回一个预先创建的迭代器,或者至少比生成器更有效地创建.我会重试您之前创建生成器的时间,然后调用下一个。另一种更公平地计时的方法可能是使用一个非常大的列表,这样与遍历查找第一个满足条件的元素相比,创建生成器的开销会很小。

最后,我认为你不应该担心所有语言的 python 中的这种事情。尽可能写出最清晰的代码,不要过早优化。尤其是在像 python 这样的语言中,优化的途径通常涉及调用用 python 绑定(例如 numpy)用 C 编写的库,或者自己用 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

Next 很好,但老实说,除了找到满足某些条件的第一个元素之外,我认为我从未使用过它。老实说,我是一位经验丰富的 Python 程序员,熟悉这个习语,但如果我不熟悉,我会发现 for 循环更加清晰。

【讨论】:

    猜你喜欢
    • 2012-09-16
    • 2018-04-05
    • 2011-11-12
    • 2013-05-07
    • 2012-02-13
    • 2010-12-27
    • 2014-12-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多