【问题标题】:Python 3: Most efficient way to create a [func(i) for i in range(N)] list comprehensionPython 3:创建 [func(i) for i in range(N)] 列表理解的最有效方法
【发布时间】:2011-01-27 06:05:36
【问题描述】:

假设我有一个函数 func(i) 为整数 i 创建一个对象,而 N 是某个非负整数。那么创建等于该列表的列表(不是范围)的最快方法是什么

mylist = [func(i) for i in range(N)]

不求助于像在 C 中创建函数这样的高级方法?我对上述列表理解的主要担忧是,我不确定 python 是否事先知道 range(N) 的长度来预分配 mylist,因此必须逐步重新分配列表。是这种情况还是python足够聪明,可以先将 mylist 分配给长度 N 然后计算它的元素?如果没有,创建 mylist 的最佳方法是什么?也许是这个?

mylist = [None]*N
for i in range(N): mylist[i] = func(i)

重新编辑:从以前的编辑中删除了误导性信息。

【问题讨论】:

    标签: python list-comprehension


    【解决方案1】:

    有人写道:“”“Python 足够聪明。只要你迭代的对象有__len____length_hint__ 方法,Python 就会调用它来确定大小并预分配数组。”” "

    据我所知,列表理解中没有预分配。 Python 无法根据 INPUT 的大小来判断 OUTPUT 的大小。

    看看这个 Python 2.6 代码:

    >>> def foo(func, iterable):
    ...     return [func(i) for i in iterable]
    ...
    >>> import dis; dis.dis(foo)
      2           0 BUILD_LIST               0 #### build empty list
                  3 DUP_TOP
                  4 STORE_FAST               2 (_[1])
                  7 LOAD_FAST                1 (iterable)
                 10 GET_ITER
            >>   11 FOR_ITER                19 (to 33)
                 14 STORE_FAST               3 (i)
                 17 LOAD_FAST                2 (_[1])
                 20 LOAD_FAST                0 (func)
                 23 LOAD_FAST                3 (i)
                 26 CALL_FUNCTION            1
                 29 LIST_APPEND      #### stack[-2].append(stack[-1]); pop()
                 30 JUMP_ABSOLUTE           11
            >>   33 DELETE_FAST              2 (_[1])
                 36 RETURN_VALUE
    

    它只是构建一个空列表,并附加迭代提供的任何内容。

    现在看看这段代码,它在列表推导中有一个“if”:

    >>> def bar(func, iterable):
    ...     return [func(i) for i in iterable if i]
    ...
    >>> import dis; dis.dis(bar)
      2           0 BUILD_LIST               0
                  3 DUP_TOP
                  4 STORE_FAST               2 (_[1])
                  7 LOAD_FAST                1 (iterable)
                 10 GET_ITER
            >>   11 FOR_ITER                30 (to 44)
                 14 STORE_FAST               3 (i)
                 17 LOAD_FAST                3 (i)
                 20 JUMP_IF_FALSE           17 (to 40)
                 23 POP_TOP
                 24 LOAD_FAST                2 (_[1])
                 27 LOAD_FAST                0 (func)
                 30 LOAD_FAST                3 (i)
                 33 CALL_FUNCTION            1
                 36 LIST_APPEND
                 37 JUMP_ABSOLUTE           11
            >>   40 POP_TOP
                 41 JUMP_ABSOLUTE           11
            >>   44 DELETE_FAST              2 (_[1])
                 47 RETURN_VALUE
    >>>
    

    相同的代码,加上一些代码以避免 LIST_APPEND。

    在 Python 3.X 中,您需要深入挖掘:

    >>> import dis
    >>> def comprehension(f, iterable): return [f(i) for i in iterable]
    ...
    >>> dis.dis(comprehension)
      1           0 LOAD_CLOSURE             0 (f)
                  3 BUILD_TUPLE              1
                  6 LOAD_CONST               1 (<code object <listcomp> at 0x00C4B8D
    8, file "<stdin>", line 1>)
                  9 MAKE_CLOSURE             0
                 12 LOAD_FAST                1 (iterable)
                 15 GET_ITER
                 16 CALL_FUNCTION            1
                 19 RETURN_VALUE
    >>> dis.dis(comprehension.__code__.co_consts[1])
      1           0 BUILD_LIST               0
                  3 LOAD_FAST                0 (.0)
            >>    6 FOR_ITER                18 (to 27)
                  9 STORE_FAST               1 (i)
                 12 LOAD_DEREF               0 (f)
                 15 LOAD_FAST                1 (i)
                 18 CALL_FUNCTION            1
                 21 LIST_APPEND              2
                 24 JUMP_ABSOLUTE            6
            >>   27 RETURN_VALUE
    >>>
    

    还是老套路:从构建一个空列表开始,然后遍历可迭代对象,根据需要添加到列表中。我在这里看不到预分配。

    您正在考虑的优化在单个操作码中使用,例如如果iterable 可以准确报告其长度,则list.extend(iterable) 的实现可以预先分配。 list.append(object) 被赋予一个对象,而不是一个可迭代对象。

    【讨论】:

    • 感谢你教我如何在 python 中反汇编,这对我以后有很大帮助。但似乎您使用的是 Python 2,在 Python 3 中我得到不同的输出。它不适合此评论,我将尽快编辑我的问题以显示我的发现。
    • 啊,明白了!好的,这说服了我,将您的答案标记为已接受的答案。至于“我假设”、“可能”、“事实上”的事情:似乎我必须稍微润色一下我的英语:-) 我将删除我添加的误导性信息。
    • @Daniel:很高兴你还在这里,我以为我接受这个答案把你吓跑了。 @John:为了 100% 确定:如果 iterable 可以报告它的长度,您是否有任何证据表明 list.extend(iterable) 实际上预先分配了?如果你不能,没有问题,你坚持让我接受正确答案已经很棒了:)
    • @mejiwa: http://svn.python.org/view/python/branches/py3k/Objects/listobject.c?view=markup Ctrl-F 搜索_PyObject_LengthHint
    【解决方案2】:

    如果你使用timeit模块,你可能会得出相反的结论:列表推导比预分配更快:

    f=lambda x: x+1
    N=1000000
    def lc():
        return [f(i) for i in range(N)]
    def prealloc():
        mylist = [None]*N
        for i in range(N): mylist[i]=f(i)
        return mylist
    def xr():
        return map(f,xrange(N))
    
    if __name__=='__main__':
        lc()
    

    警告:这些是我电脑上的结果。您应该自己尝试这些测试,因为您的结果可能会根据您的 Python 版本和硬件而有所不同。 (见 cmets。)

    % python -mtimeit -s"import test" "test.prealloc()"
    10 loops, best of 3: 370 msec per loop
    % python -mtimeit -s"import test" "test.lc()"
    10 loops, best of 3: 319 msec per loop
    % python -mtimeit -s"import test" "test.xr()"
    10 loops, best of 3: 348 msec per loop
    

    请注意,与 Javier 的回答不同,我将 mylist = [None]*N 作为代码时间的一部分包含在使用“预分配”方法时的时间。 (这不仅仅是设置的一部分,因为它是只有在使用预分配时才需要的代码。)

    PS。 time 模块(和 time (unix) 命令)can give unreliable results。如果您希望对 Python 代码进行计时,我建议您坚持使用 timeit 模块。

    【讨论】:

    • 这要快很多map(f, xrange(N))
    • @jleedev:我添加了xr 来测试map(f,xrange(N))。它出现在 CPython 2.6 中,列表理解比 map 更快。
    • 奇怪,我使用的是 cpython 2.6.1(在 darwin 上),而 xrlc 快大约 20%。
    • @jleedev:嗯,这很有趣。也许这是一个很好的提醒,什么被认为是最快的可能取决于很多事情,包括硬件。
    • 刚刚在 Pentium III 上的 Linux 上尝试了 2.5.2,分别获得了 3/2.63/2.82 秒。与许多问题一样,答案是“视情况而定”,尽管在 Python 中手动预分配数组通常没有用处。
    【解决方案3】:

    有趣的问题。从下面的测试来看,预分配似乎并没有提高当前 CPython 实现的性能(Python 2 代码但结果排名相同,只是 Python 3 中没有 xrange):

    N = 5000000
    
    def func(x):
        return x**2
    
    def timeit(fn):
        import time
        begin = time.time()
        fn()
        end = time.time()
        print "%-18s: %.5f seconds" % (fn.__name__, end - begin)
    
    def normal():
        mylist = [func(i) for i in range(N)]
    
    def normalXrange():
        mylist = [func(i) for i in xrange(N)]
    
    def pseudoPreallocated():
        mylist = [None] * N
        for i in range(N): mylist[i] = func(i)
    
    def preallocated():
        mylist = [None for i in range(N)]
        for i in range(N): mylist[i] = func(i)
    
    def listFromGenerator():
        mylist = list(func(i) for i in range(N))
    
    def lazy():
        mylist = (func(i) for i in xrange(N))
    
    
    
    timeit(normal)
    timeit(normalXrange)
    timeit(pseudoPreallocated)
    timeit(preallocated)
    timeit(listFromGenerator)
    timeit(lazy)
    

    结果(括号内排名):

    normal            : 7.57800 seconds (2)
    normalXrange      : 7.28200 seconds (1)
    pseudoPreallocated: 7.65600 seconds (3)
    preallocated      : 8.07800 seconds (5)
    listFromGenerator : 7.84400 seconds (4)
    lazy              : 0.00000 seconds
    

    但是psyco.full():

    normal            : 7.25000 seconds  (3)
    normalXrange      : 7.26500 seconds  (4)
    pseudoPreallocated: 6.76600 seconds  (1)
    preallocated      : 6.96900 seconds  (2)
    listFromGenerator : 10.50000 seconds (5)
    lazy              : 0.00000 seconds
    

    如您所见,psyco 的伪预分配速度更快。无论如何,xrange 解决方案(我推荐)和其他解决方案之间没有太大区别。如果您稍后不处理列表的所有元素,您还可以使用惰性方法(如上面的代码所示),该方法将创建一个生成器,在您迭代它时生成元素。

    【讨论】:

    • 我认为在 Python 3 中范围与 xrange 相同,并且 xrange 已被删除,所以我已经以某种方式使用 xrange。为了我的需要,我需要访问列表中的所有元素,我什至依赖于 func(i) 的执行。但感谢所有这些基准测试,非常有用。
    • @mejiwa:是的,Python 3 的范围现在类似于 Python 2 的 xrange。在我的答案中添加了评论。如果您将基准测试与 Python 3.1 进行比较,您会注意到它只需要 2-3 秒而不是 6-7 秒,这显示了实现的改进程度。也许你想试试 Unladen Swallow(可能更快)。
    • 我提出问题的动机更多地是想知道在我未来的代码中选择什么策略,或多或少地不管python实现,而不是优化单个问题。实际上我什至不能选择实现,因为它是用于搅拌机导出脚本的。但是 Unladen Swallow 对于个人使用项目似乎很有趣,感谢您的提示:)
    【解决方案4】:

    使用自动调整大小的数组和预分配数组的计算复杂度没有区别。在最坏的情况下,它的成本约为 O(2N)。见这里:

    Constant Amortized Time

    函数调用的成本加上函数中发生的任何事情都将使这个额外的 n 变得微不足道。这不是你应该担心的事情。只需使用列表推导即可。

    【讨论】:

    • 当然是一回事。 O(2n) 等于 O(n)。
    • 不知道这么便宜,谢谢指点,我去看看!但我仍然试图避免持续的分配开销。
    【解决方案5】:

    在这里不得不不同意哈维尔的观点......

    使用以下代码:

    print '%5s | %6s | %6s' % ('N', 'l.comp', 'manual')
    print '------+--------+-------'
    for N in 10, 100, 1000, 10000:
        num_iter = 1000000 / N
    
        # Time for list comprehension.
        t1 = timeit.timeit('[func(i) for i in range(N)]', setup='N=%d;func=lambda x:x' % N, number=num_iter)
    
        # Time to build manually.
        t2 = timeit.timeit('''mylist = [None]*N
    for i in range(N): mylist[i] = func(i)''', setup='N=%d;func=lambda x:x' % N, number=num_iter)
    
        print '%5d | %2.4f | %2.4f' % (N, t1, t2)
    

    我得到下表:

        N | l.comp | manual
    ------+--------+-------
       10 | 0.3330 | 0.3597
      100 | 0.2371 | 0.3138
     1000 | 0.2223 | 0.2740
    10000 | 0.2185 | 0.2771
    

    从该表中可以看出,在这些不同长度的每种情况下,列表理解都比预分配更快。

    【讨论】:

      【解决方案6】:

      使用列表推导来完成您想要做的事情将是更 Pythonic 的方式。尽管有性能损失:)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-13
        • 2016-03-12
        • 2020-11-04
        • 1970-01-01
        • 1970-01-01
        • 2017-11-05
        相关资源
        最近更新 更多