【问题标题】:Python - "in" statement search slow for list of objectsPython - “in”语句搜索对象列表的速度很慢
【发布时间】:2014-01-19 00:41:05
【问题描述】:

我希望有人能解释为什么搜索对象引用列表比搜索普通列表要慢得多。这是使用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 个对象,现在可以轻松地处理多达 10000 个对象,只需让我的所有类都继承自“对象”类。就在我以为我知道 Python 的时候!。

我以前读过关于新样式与旧样式的文章,但从未提到旧样式类可以比新样式类慢 100 倍。在对象实例列表中搜索特定实例的最佳方法是什么?
1. 继续使用“in”语句,但要确保所有类都是新样式。
2. 使用“is”语句执行一些其他类型的搜索,例如:
[obj for obj in spamlist if obj is target]

3。其他更 Pythonic 的方式?

【问题讨论】:

  • 您使用的是哪个 Python 版本?

标签: python list search python-2.7


【解决方案1】:

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

>>> 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

如您所见,Spam 继承自 object 的版本运行速度要快得多,几乎与使用字符串的情况一样快。

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

对于老式类,这是以简单但缓慢的方式实现的。 Python 必须在每个实例的字典以及每个实例的类和超类的字典中实际查找 __eq____cmp__ 方法。 __coerce__ 也被查找,作为 3 路比较过程的一部分。当这些方法实际上都不存在时,这就像 12 dict 查找只是为了进行指针比较。除了 dict 查找之外,还有很多其他开销,我实际上不确定该过程的哪些方面最耗时,但只要说该过程比它可能的成本更高就足够了。

对于内置类型和新型类,情况会更好。首先,Python 不会在实例的字典上寻找特殊方法。这节省了一些 dict 查找并启用下一部分。其次,类型对象具有对应于 Python 级特殊方法的 C 级函数指针。当一个特殊方法在 C 中实现或不存在时,相应的函数指针允许 Python 完全跳过方法查找过程。这意味着在 new-style 的情况下,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

【讨论】:

  • 好的,你对此有什么解释吗:从字符串列表中搜索字符串,从整数列表中搜索字符串。 >>> Timer("'1' in L", "L = [str(10) for x in range(100000)]").timeit(number=1000) 1.410999059677124 >>> Timer("'1' in L ", "L = [10 for x in range(100000)]").timeit(number=1000) 5.952347993850708 >>>
  • 将“class Spam:”更改为“class Spam(object)”似乎是解决办法。
  • @yopy 将字符串 '1' 与整数 10 进行比较似乎非常昂贵——而且毫无意义。
  • @yopy 我刚刚测试过:你的 2 个测试分别需要 3.64 分。在我的机器上需要 10.17 秒,而 timeit.Timer("1 in L", "L = [10 for x in range(100000)]").timeit(number=1000) 需要 3.15 秒。
  • @glglgl 我知道它毫无意义,但我一直在寻找它的缓慢性。它只是进行比较直到列表元素的末尾。因为列表中没有字符串。
【解决方案2】:

这不是您问题的正确答案,但对于想要了解“in”关键字如何在幕后工作的人来说,这将是一个很好的知识:

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

来自邮件线程的解释:

我对此很好奇(好吧,我承认,我也喜欢正确 ;) 深入了解细节,如果有人有兴趣……其中一个 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() 构造一个 序列上的迭代器,然后进入无限循环(for (;;)) 在迭代器上调用 PyIter_Next() 直到找到该项目或 调用 PyIter_Next() 会返回错误。

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

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

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

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

【讨论】:

    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案4】:

      由于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 进行比较,您会发现时间相似。

      【讨论】:

      • 我不确定这是否会产生巨大的影响。如果我遍历 alist 并且每次都得到相同的对象,那么每个项目都会进行比较。
      猜你喜欢
      • 1970-01-01
      • 2014-01-12
      • 1970-01-01
      • 2013-01-24
      • 1970-01-01
      • 1970-01-01
      • 2011-11-13
      • 1970-01-01
      • 2015-09-21
      相关资源
      最近更新 更多