Python - “in”语句搜索对象列表的速度很慢

时间:2014-01-01 11:29:08

标签: python list search python-2.7

我希望有人可以解释为什么搜索对象引用列表比搜索普通列表要慢得多。这是使用python“in”关键字搜索我认为以“C编译器”速度运行的。我认为列表只是一个对象引用数组(指针),所以搜索应该非常快。两个列表在内存中都是412236字节。

正常列表(需要0.000秒才能搜索):

alist = ['a' for x in range(100000)]
if 'b' in alist:
    print("Found")

对象引用列表(需要0.469 !!秒才能搜索):

class Spam:
    pass
spamlist = [Spam() for x in range(100000)]
if Spam() in spamlist:
    print("Found")

<小时/> 编辑:显然这与旧式类有关,比新式类更有开销。我的脚本只有400个对象,现在只需将所有类继承自“object”类,就可以轻松处理多达10000个对象。就在我以为我认识Python的时候!。
我之前读过关于新式和旧式的内容,但从未提到过旧式课程比新式课程慢100倍。搜索特定实例的对象实例列表的最佳方法是什么? 1.继续使用“in”语句,但要确保所有类都是新风格 2.使用“是”语句执行其他类型的搜索,如:

[obj for obj in spamlist if obj is target]

3。还有其他更多的Pythonic方法吗?

4 个答案:

答案 0 :(得分:4)

这主要是由于旧式类的特殊方法查找机制不同。

>>> timeit.timeit("Spam() in l", """
... # Old-style
... class Spam: pass
... l = [Spam() for i in xrange(100000)]""", number=10)
3.0454677856675403
>>> timeit.timeit("Spam() in l", """
... # New-style
... class Spam(object): pass
... l = [Spam() for i in xrange(100000)]""", number=10)
0.05137817007346257
>>> timeit.timeit("'a' in l", 'l = ["b" for i in xrange(100000)]', number=10)
0.03013876870841159

正如您所看到的,Spamobject继承的版本运行得更快,几乎与字符串一样快。

列表的in运算符使用==来比较项目是否相等。 ==被定义为按顺序尝试对象的__eq__方法,__cmp__方法和指针比较。

对于旧式类,这是以简单但缓慢的方式实现的。 Python必须在每个实例的dict中查找__eq____cmp__方法,以及每个实例的类和超类的dicts。作为三向比较过程的一部分,__coerce__也会被抬起。当这些方法实际上都不存在时,这就像12个dict查找只是为了得到指针比较。除了dict查找之外还有一堆其他开销,我实际上并不确定该过程的哪些方面是最耗时的,但是足以说明该过程比它可能更昂贵。

对于内置类型和新式类,事情会更好。首先,Python不会在实例的dict上查找特殊方法。这样可以节省一些dict查找并启用下一部分。其次,类型对象具有与Python级特殊方法相对应的C级函数指针。当在C中实现特殊方法或不存在时,相应的函数指针允许Python完全跳过方法查找过程。这意味着在新式的情况下,Python可以快速检测到它应该直接跳到指针比较。

至于你应该做什么,我建议使用in和新式课程。如果您发现此操作正在成为瓶颈,但您需要旧式类以实现向后兼容,any(x is y for y in l)的运行速度比x in l快20倍:

>>> timeit.timeit('x in l', '''
... class Foo: pass
... x = Foo(); l = [Foo()] * 100000''', number=10)
2.8618816054721936
>>> timeit.timeit('any(x is y for y in l)', '''
... class Foo: pass
... x = Foo(); l = [Foo()] * 100000''', number=10)
0.12331640524583776

答案 1 :(得分:1)

对于您的问题,这不是正确的答案,但对于谁想要理解“关键字”中的关键字如何工作,这将是一个非常好的知识:

ceval源代码:ceval.c source code abstract.c源代码:abstract.c source code 来自邮件:mail about 'in' keywords

来自邮件主题的Expalantion:

我对此很好奇(好吧,我承认,我也喜欢做对了 ;)挖掘细节,如果有人有兴趣...其中之一 Python开源的好处是你可以找到它是如何工作的......

第一步,查看字节码:

>>> import dis
>>> def f(x, y):
...   return x in y
...
>>> dis.dis(f)
2          0 LOAD_FAST                0 (x)
           3 LOAD_FAST                1 (y)
           6 COMPARE_OP               6 (in)
           9 RETURN_VALUE

所以in被实现为COMPARE_OP。查看ceval.c COMPARE_OP,它对一些快速比较有一些优化,然后 调用cmp_outcome(),对于'in',调用PySequence_Contains()。

PySequence_Contains()在abstract.c中实现。如果是容器 实现__contains__,否则称为_PySequence_IterSearch() 使用_PySequence_IterSearch()

PyObject_GetIter()调用PyObject_GetIter()构建一个 序列上的迭代器,然后进入无限循环(for(;;)) 在迭代器上调用PyIter_Next()直到找到项目或者 调用PyIter_Next()会返回错误。

abstract.c也位于__iter__()。如果对象有 调用PySeqIter_New()方法,否则调用PySeqIter_New() 构造一个迭代器。

iterobject.c已在next()中实施。这是iter_iternext()方法 __getitem__()。此方法在其包装对象上调用__getitem__ but not __iter__ 并为下一次增加一个索引。

所以,虽然细节很复杂,但我认为这很公平 该实现使用while循环(在_PySequence_IterSearch()中) 和一个计数器(用PySeqIter_Type包装)来实现'in' 定义__getitem__()的容器。

顺便说一句'for'的实现也调用了PyObject_GetIter(),所以 它使用相同的机制为序列生成迭代器 定义{{1}}。

答案 2 :(得分:0)

Python创建一个不可变的'a'对象,列表中的每个元素都指向同一个对象。由于Spam()是可变的,每个实例都是一个不同的对象,并且解引用spamlist中的指针将访问RAM中的许多区域。性能差异可能与硬件缓存命中/未命中有关。

如果您在结果中包含列表创建时间(而不仅仅是Spam() in spamlist),显然性能差异会更大。另请尝试x = Spam(); x in spamlist以查看是否会产生影响。

我很好奇any(imap(equalsFunc, spamlist))如何比较。

答案 3 :(得分:0)

由于string interning,使用alist = ['a' for x in range(100000)]的测试可能会产生误导。事实证明,Python将实习(在大多数情况下)短不可变 - 特别是字符串 - 因此它们都是相同的对象。

演示:

>>> alist=['a' for x in range(100000)]
>>> len(alist)
100000
>>> len({id(x) for x in alist})
1

您可以看到,在创建100000个字符串的列表时,它只包含一个实习对象。

更公平的情况是使用对object的调用来保证每个都是唯一的Python对象:

>>> olist=[object() for x in range(100000)]
>>> len(olist)
100000
>>> len({id(x) for x in olist})
100000

如果将in运算符与olist进行比较,您会发现相似的时间。