迭代时更改列表的最佳方法

时间:2012-04-24 20:47:40

标签: python loops dynamic

我在python脚本(v2.6)中有几个实例,我需要在其中修改列表。我需要从列表中弹出值以响应用户的交互式输入,并且想知道最干净的方法。目前我有一个非常脏的解决方案:a)将列表中的项目设置为我要删除为False并使用过滤器或列表推导删除它们或b)在循环中生成一个全新的列表,这似乎是不必要的将变量添加到命名空间并占用内存。

此问题的一个示例如下:

for i, folder in enumerate(to_run_folders):
    if get_size(folder) < byte_threshold:
        ans = raw_input(('The folder {0}/ is less than {1}MB.' + \
                    ' Would you like to exclude it from' + \
                    ' compression? ').format(folder, megabyte_threshold))
        if 'y' in ans.strip().lower():
            to_run_folders.pop(i)

我想查看列表中的每个文件夹。如果当前文件夹小于特定大小,我想询问用户是否要将其排除。如果是,请从列表中弹出该文件夹。

这个例程的问题是,如果我遍历列表,我会得到意外的行为并提前终止。如果我通过切片迭代副本,pop不会拉出正确的值,因为索引被移动并且问题在弹出更多项时会复合。我还需要在我的脚本的其他区域中进行此类动态列表调整。这种功能有什么干净的方法吗?

5 个答案:

答案 0 :(得分:8)

您可以向后循环列表,或使用视图对象。

请参阅https://stackoverflow.com/a/181062/711085了解如何向后循环列表。基本上使用reversed(yourList)(会发生创建一个向后访问的视图对象)。

如果您需要建立索引,可以执行reversed(enumerate(yourList)),但这会在内存中有效创建一个临时列表,因为enumerate需要在reversed启动之前运行。您需要要么做索引操作,要么这样做:

for i in xrange(len(yourList)-1, -1, -1):
    item = yourList[i]
    ...

更干净:reversed知道range,所以你可以在python3中执行此操作,如果使用xrange则可以在python2中执行此操作:

for i in reversed(range(len(yourList))):  
    item = yourList[i]
    ...

(证明:你可以next(reversed(range(10**10))),但如果使用python2,这会使你的计算机崩溃)

答案 1 :(得分:4)

你可以向后循环

向后:

x = range(10)
l = len(x)-1  # max index

for i, v in enumerate(reversed(x)):
    if v % 2:
        x.pop(l-i)  # l-1 is the forward index

答案 2 :(得分:1)

  

目前我有一个非常脏的解决方案:a)将列表中的项目设置为我要删除为False并使用过滤器或列表理解删除它们或b)在循环中生成一个全新的列表,这似乎不必要地将变量添加到命名空间并占用内存。

实际上,这不是那种肮脏的解决方案。列表通常是多长时间?即使创建新列表也不应该太多内存消耗,因为列表只包含引用。

你也可以在while循环中循环并为自己枚举,如果用户决定(可能单独计算原始位置),则执行del lst[n]

答案 3 :(得分:1)

好的,我已经测量了解决方案。反向解决方案大致相同。前向while循环慢约4倍。 但是!对于100,000个随机整数列表,Patrik的解决方案快了大约80倍 [纠正Patrik2中的错误]

import timeit
import random

def solution_ninjagecko1(lst):
    for i in xrange(len(lst)-1, -1, -1):
        if lst[i] % 2 != 0:    # simulation of the choice
            del lst[i]
    return lst

def solution_jdi(lst):
    L = len(lst) - 1
    for i, v in enumerate(reversed(lst)):
        if v % 2 != 0:
            lst.pop(L-i)  # L-1 is the forward index
    return lst

def solution_Patrik(lst):
    for i, v in enumerate(lst):
        if v % 2 != 0:         # simulation of the choice
            lst[i] = None
    return [v for v in lst if v is not None]

def solution_Patrik2(lst):
    ##buggy lst = [v for v in lst if v % 2 != 0]
    ##buggy return [v for v in lst if v is not None]
    # ... corrected to
    return [v for v in lst if v % 2 != 0]

def solution_pepr(lst):
    i = 0                      # indexing the processed item
    n = 0                      # enumerating the original position
    while i < len(lst):
        if lst[i] % 2 != 0:    # simulation of the choice
            del lst[i]         # i unchanged if item deleted
        else:
            i += 1             # i moved to the next
        n += 1
    return lst

def solution_pepr_reversed(lst):
    i = len(lst) - 1           # indexing the processed item backwards
    while i > 0:
        if lst[i] % 2 != 0:    # simulation of the choice
            del lst[i]         # i unchanged if item deleted
        i -= 1                 # i moved to the previous
    return lst

def solution_steveha(lst):
    def should_keep(x):
        return x % 2 == 0
    return filter(should_keep, lst)

orig_lst = range(30)
print 'range() generated list of the length', len(orig_lst)
print orig_lst[:20] + ['...']   # to have some fun :)

lst = orig_lst[:]  # copy of the list
print solution_ninjagecko1(lst)

lst = orig_lst[:]  # copy of the list
print solution_jdi(lst)

lst = orig_lst[:]  # copy of the list
print solution_Patrik(lst)

lst = orig_lst[:]  # copy of the list
print solution_pepr(lst)

orig_lst = [random.randint(1, 1000000) for n in xrange(100000)]
print '\nrandom list of the length', len(orig_lst)
print orig_lst[:20] + ['...']   # to have some fun :)

lst = orig_lst[:]  # copy of the list
t = timeit.timeit('solution_ninjagecko1(lst)',
                  'from __main__ import solution_ninjagecko1, lst',
                  number=1)
print 'solution_ninjagecko1: ', t

lst = orig_lst[:]  # copy of the list
t = timeit.timeit('solution_jdi(lst)',
                  'from __main__ import solution_jdi, lst',
                  number=1)
print 'solution_jdi: ', t

lst = orig_lst[:]  # copy of the list
t = timeit.timeit('solution_Patrik(lst)',
                  'from __main__ import solution_Patrik, lst',
                  number=1)
print 'solution_Patrik: ', t

lst = orig_lst[:]  # copy of the list
t = timeit.timeit('solution_Patrik2(lst)',
                  'from __main__ import solution_Patrik2, lst',
                  number=1)
print 'solution_Patrik2: ', t

lst = orig_lst[:]  # copy of the list
t = timeit.timeit('solution_pepr_reversed(lst)',
                  'from __main__ import solution_pepr_reversed, lst',
                  number=1)
print 'solution_pepr_reversed: ', t

lst = orig_lst[:]  # copy of the list
t = timeit.timeit('solution_pepr(lst)',
                  'from __main__ import solution_pepr, lst',
                  number=1)
print 'solution_pepr: ', t

lst = orig_lst[:]  # copy of the list
t = timeit.timeit('solution_steveha(lst)',
                  'from __main__ import solution_steveha, lst',
                  number=1)
print 'solution_steveha: ', t

它在我的控制台上打印:

c:\tmp\_Python\Patrick\so10305762>python a.py
range() generated list of the length 30
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, '...']
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]

random list of the length 100000
[915411, 954538, 794388, 847204, 846603, 454132, 866165, 640004, 930488, 609138,
 333405, 986073, 318301, 728151, 996047, 117633, 455353, 581737, 55350, 485030,
'...']
solution_ninjagecko1:  2.41921752625
solution_jdi:  2.45477176569
solution_Patrik:  0.0468565138865
solution_Patrik2:  0.024270403082
solution_pepr_reversed:  2.43338888043
solution_pepr:  9.11879694207

所以,我尝试了更长的清单。使用只有两倍的长度会产生很大的不同(在我的旧电脑上)。 Patrik的 dirty 解决方案表现得非常好。它比反向解决方案快约200倍:

random list of the length 200000
[384592, 170167, 598270, 832363, 123557, 81804, 319315, 445945, 178732, 726600,
516835, 392267, 552608, 40807, 349215, 208111, 880032, 520614, 384119, 350090, 
'...']
solution_ninjagecko1:  17.362140719
solution_jdi:  17.86837545
solution_Patrik:  0.0957998851809
solution_Patrik2:  0.0500024444448
solution_pepr_reversed:  17.5078452708
solution_pepr:  52.175648581

[在ninjagecko的评论后添加]

校正后的Patrik2解决方案比2阶段Patrick解决方案快两倍。

为了模拟不那么频繁地删除元素,像if v % 2 != 0:这样的测试已更改为if v % 100 == 0:。然后应该删除大约1%的项目。很明显,它花费的时间更少。对于列表中的500,000个随机整数,结果如下:

random list of the length 500000
[403512, 138135, 552313, 427971, 42358, 500926, 686944, 304889, 916659, 112636,
791585, 461948, 82622, 522768, 485408, 774048, 447505, 830220, 791421, 580706, 
'...']
solution_ninjagecko1:  6.79284210703
solution_jdi:  6.84066913532
solution_Patrik:  0.241242951269
solution_Patrik2:  0.162481823807
solution_pepr_reversed:  6.92106007886
solution_pepr:  7.12900522273

Patrick的解决方案速度提高了大约30倍。

[已添加2012/04/25]

另一种可以就地运行的解决方案,即循环前进,这与Patrick的解决方案一样快。删除元素时,它不会移动所有尾部。相反,它将所需元素移动到最终位置,然后剪切列表中未使用的尾部。

def solution_pepr2(lst):
    i = 0
    for v in lst:
        lst[i] = v              # moving the element (sometimes unneccessary)
        if v % 100 != 0:        # simulation of the choice
            i += 1              # here will be the next one
    lst[i:] = []                # cutting the tail of the length of the skipped
    return lst

# The following one only adds the enumerate to simulate the situation when
# it is needed -- i.e. slightly slower but with the same complexity.        
def solution_pepr2enum(lst):
    i = 0
    for n, v in enumerate(lst):
        lst[i] = v              # moving the element (sometimes unneccessary)
        if v % 100 != 0:        # simulation of the choice
            i += 1              # here will be the next one
    lst[i:] = []                # cutting the tail of the length of the skipped
    return lst

v % 100 != 0的上述解决方案相比:

random list of the length 500000
[533094, 600755, 58260, 295962, 347612, 851487, 523927, 665648, 537403, 238660,
781030, 940052, 878919, 565870, 717745, 408465, 410781, 560173, 51010, 730322, 
'...']
solution_ninjagecko1:  1.38956896051
solution_jdi:  1.42314502685
solution_Patrik:  0.135545530079
solution_Patrik2:  0.0926935780151
solution_pepr_reversed:  1.43573239178
solution_steveha:  0.122824246805
solution_pepr2:  0.0938177241656
solution_pepr2enum:  0.11096263294

答案 4 :(得分:0)

处理此问题的最佳方法,最“Pythonic”方式,实际上是循环遍历您的列表并创建一个仅包含您想要的文件夹的新列表。我将如何做到这一点:

def want_folder(fname):
    if get_size(folder) >= byte_threshold:
        return True
    ans = raw_input(('The folder {0}/ is less than {1}MB.' + \
                ' Would you like to exclude it from' + \
                ' compression? ').format(folder, megabyte_threshold))
    return 'y' not in ans.strip().lower()

to_run_folders = [fname for fname in to_run_folders if want_folder(fname)]

如果您的列表真的很大,那么您可能需要担心此解决方案的性能并使用肮脏的技巧。但是如果你的列表那么大,那么让一个人回答关于可能出现的所有文件的是/否问题可能会有点疯狂。

性能是一个实际问题还是一种唠叨的担忧?因为我非常确定上面的代码足够快以便实际使用,并且比棘手的代码更容易理解和修改。

编辑:@jdi建议在评论中使用itertools.ifilter()filter()

我测试了,这实际上应该比我上面显示的更快:

to_run_folders = filter(want_folder, to_run_folders)

我刚刚复制了@ pepr的基准测试代码,并使用filter()测试了解决方案,如下所示。它总体上排名第二,只有Patrik2更快。 Patrik2的速度提高了一倍,但同样,任何数据设置都足够小以至于让人回答是/否问题可能很小,因为两个因素并不重要。

编辑:为了好玩,我继续编写了一个纯粹的列表理解版本。它有一个要评估的表达式,没有Python函数调用。

to_run_folders = [fname for fname in to_run_folders
        if get_size(fname) >= mb_threshold or
                'y' not in raw_input(('The folder {0}/ is less than {1}MB.' +
                ' Would you like to exclude it from compression? '
                ).format(fname, mb_threshold)).strip().lower()

]

呸!我更喜欢做功能。