在列表中查找和替换元素

412

我需要搜索列表并将其中所有一个元素的出现替换为另一个元素。到目前为止,我的代码尝试都没有成功,什么是最好的方法?

例如,假设我的列表包含以下整数

a = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1]

我需要将所有数字1的出现替换为值10,以便得到所需的输出

a = [10, 2, 3, 4, 5, 10, 2, 3, 4, 5, 10]

因此,我的目标是将所有数字1的实例替换为数字10。

11个回答

698

10
但是这并不会改变a,对吧?我认为原帖作者想要改变a - Dula
18
@Dula,你可以执行a = [4 if x==1 else x for x in a],这会影响到a。 - Alekhya Vemavarapu
@Dula:这个问题不太明确,不确定 a 是否应该变异,但是(正如 Alekhya 所示),当使用列表推导式时,处理任何一种情况都是微不足道的。 - outis
66
如果您想突变 a,那么应该执行 a[:] = [4 if x==1 else x for x in a](注意完整的列表切片)。仅仅执行 a= 会创建一个具有不同 id()(标识)的新列表 a,与原始列表不同。 - Chris_Rands
1
仅供评估目的,请注意,这个解决方案在快速解决方案中是最一致的(无论要替换的项是常见还是罕见,运行时间都保持有效恒定)。当list大多数项目保持不变时,这比优化的原地解决方案慢,例如kxr的答案。对于长度为1000的输入,kxr的答案需要的时间从此解决方案的1/3(当没有需要替换的项时)到3倍(当所有项都必须被替换);变化更大。 - ShadowRanger

315

您可以使用内置的enumerate来在迭代列表时获取索引和值。然后,使用值来测试条件,使用索引来替换原始列表中的该值:

>>> a = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1]
>>> for i, n in enumerate(a):
...   if n == 1:
...      a[i] = 10
...
>>> a
[10, 2, 3, 4, 5, 10, 2, 3, 4, 5, 10]

42
这是一个不好的、非常不符合Python风格的解决方案。考虑使用列表推导式。 - AdHominem
308
这是一个虽然不太符合 Python 风格但仍可行的解决方案。建议考虑使用列表推导式。 - Jean-François Corbett
8
这比列表推导表现更好,是吗?它执行原地更新而不是生成一个新列表。 - neverendingqs
2
@neverendingqs:不。解释器开销占主导地位,并且理解方式的开销较小。理解方式表现略好,尤其是在更高比例的元素通过替换条件的情况下。以下是一些定时信息:https://ideone.com/ZrCy6z - user2357112
3
与使用本地列表方法(如.index(10))相比,这真的很慢。没有必要列出每个列表元素来查找需要替换的元素。请参见我在此处的答案中的时间安排。 - dawg
显示剩余4条评论

85

如果您有多个值需要替换,您也可以使用字典:

a = [1, 2, 3, 4, 1, 5, 3, 2, 6, 1, 1]
replacements = {1:10, 2:20, 3:'foo'}
replacer = replacements.get  # For faster gets.

print([replacer(n, n) for n in a])

> [10, 20, 'foo', 4, 10, 5, 'foo', 20, 6, 10, 10]

请注意,此方法仅适用于可哈希的要替换的元素。这是因为需要将字典键设为可哈希。


1
对于原地替换,try-except 至少快 50%!请看这个答案。@jrjc @roipoussiere - lifebalance
谢谢!try-except更快,但它会在第一个未知项出现时中断循环,而dic.get(n,n)虽然优美,但比if n in dic慢。我修改了我的答案。 - roipoussiere
1
这将无法处理不可哈希的元素。这是所有基于朴素字典的替换的问题。(只需尝试使用[1,{'boom!'},3] - VPfB
1
@Iftah:如果你非常关注性能,那么在列表推导式之外预先绑定get方法将大大减少大型输入的运行时间。由于实际上不需要对dict本身的引用,因此您可以将其更改为dget = {1:10, 2:20, 3:'foo'}.get,并将列表推导式更改为[dget(n, n) for n in a]。即使在CPython 3.9中,它显着优化了方法调用(在简单情况下不再需要创建绑定方法对象),这仍然通过将LOAD_METHOD/CALL_METHOD替换为仅CALL_FUNCTION来减少长度为1000的输入的开销约30%。 - ShadowRanger
1
预先绑定的 get 优化使其与 dic[n] if n in dic else n 方法相当,(在我的大多数测试用例中需要花费20-30% 的时间,而当您必须在每个循环中查找 dic.get 时,需要花费60-100% 的时间)。 - ShadowRanger
显示剩余2条评论

42
列表推导很有效,使用枚举循环可以节省一些内存(因为操作基本上是在原地完成的)。
还有函数式编程。请参见map的用法:
>>> a = [1,2,3,2,3,4,3,5,6,6,5,4,5,4,3,4,3,2,1]
>>> map(lambda x: x if x != 4 else 'sss', a)
[1, 2, 3, 2, 3, 'sss', 3, 5, 6, 6, 5, 'sss', 5, 'sss', 3, 'sss', 3, 2, 1]

20
很遗憾,“lambda”和“map”被认为不符合Python风格。 - outis
6
我不确定lambda或map在本质上是否与Python格格不入,但我同意使用列表推导式比将它们结合使用更加简洁和易读。 - damzam
7
我认为它们并不违反Python的风格,但许多人认为违反了,包括Guido van Rossum (http://www.artima.com/weblogs/viewpost.jsp?thread=98196)。这是一个教派性的问题。 - outis
@outis:map+lambda比等效的列表推导式不仅可读性差,而且速度更慢。当映射函数是C中实现的内置函数且输入足够大以使map的每个项目的优势超过稍高的固定开销时,您可以从map中挤出一些性能;但是当map需要Python级别的函数(例如lambda)时,等效的生成器表达式/列表推导式可以内联(避免函数调用开销),map实际上没有任何好处(截至3.9,对于一个简单的测试用例a = [*range(10)] * 100,这个map所需时间是等效列表推导式的两倍)。 - ShadowRanger
个人而言,我主要保留了我的愤怒,针对的是 lambda;当我已经有一个能够满足需求的函数时,我喜欢使用 map(该函数可能已经足够复杂,不值得在列表推导式中内联,或者它是一个内置函数,无论如何都无法内联,例如 for line in map(str.rstrip, fileob): 从文件中逐行获取 prestripped 的行),但是如果我没有这样的函数,我必须使用 lambda,这最终会变得更加丑陋和缓慢,正如先前所述,因此我最好使用列表推导式/生成器表达式。 - ShadowRanger

12
>>> a=[1,2,3,4,5,1,2,3,4,5,1]
>>> item_to_replace = 1
>>> replacement_value = 6
>>> indices_to_replace = [i for i,x in enumerate(a) if x==item_to_replace]
>>> indices_to_replace
[0, 5, 10]
>>> for i in indices_to_replace:
...     a[i] = replacement_value
... 
>>> a
[6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6]
>>> 

中等快速但非常敏感的方法。请查看我的答案中的时间。 - dawg

12

在长列表和罕见情况下,使用 list.index() 比其他答案中提出的单步迭代方法快约3倍。

def list_replace(lst, old=1, new=10):
    """replace list elements (inplace)"""
    i = -1
    try:
        while True:
            i = lst.index(old, i + 1)
            lst[i] = new
    except ValueError:
        pass

这是我找到的最快方法。请查看我的答案中的时间。太好了! - dawg
请注意,没有使用ilist.index提供start参数的天真版本是O(n²);在一个简单的本地测试中,其中lst参数是list(range(10)) * 100的结果(1000个元素的列表,其中100个元素等间距替换),这是显而易见的;这个答案(不是天真的,并且实现了O(1)的性能)在大约25微秒内完成工作,而天真的版本在同一台机器上大约需要615微秒。 - ShadowRanger

8

我知道这是一个非常古老的问题,有很多种方法可以解决。我发现比较简单的方法是使用numpy包。

import numpy

arr = numpy.asarray([1, 6, 1, 9, 8])
arr[ arr == 8 ] = 0 # change all occurrences of 8 by 0
print(arr)

2
假设您已经在使用 numpy,这是一个很好的解决方案;它与所有其他好的解决方案一样都是 O(n),但将所有工作推到向量化的 C 层操作中意味着通过消除每个元素的解释器开销而显着优于其他解决方案。 - ShadowRanger

7

我的用例是将None替换为某个默认值。

我尝试了这里展示的解决此问题的方法,包括@kxr提供的使用str.count的方法。

在Python 3.8.1中使用ipython进行测试代码:

def rep1(lst, replacer = 0):
    ''' List comprehension, new list '''

    return [item if item is not None else replacer for item in lst]


def rep2(lst, replacer = 0):
    ''' List comprehension, in-place '''    
    lst[:] =  [item if item is not None else replacer for item in lst]

    return lst


def rep3(lst, replacer = 0):
    ''' enumerate() with comparison - in-place '''
    for idx, item in enumerate(lst):
        if item is None:
            lst[idx] = replacer

    return lst


def rep4(lst, replacer = 0):
    ''' Using str.index + Exception, in-place '''

    idx = -1
    # none_amount = lst.count(None)
    while True:
        try:
            idx = lst.index(None, idx+1)
        except ValueError:
            break
        else:
            lst[idx] = replacer

    return lst


def rep5(lst, replacer = 0):
    ''' Using str.index + str.count, in-place '''

    idx = -1
    for _ in range(lst.count(None)):
        idx = lst.index(None, idx+1)
        lst[idx] = replacer

    return lst


def rep6(lst, replacer = 0):
    ''' Using map, return map iterator '''

    return map(lambda item: item if item is not None else replacer, lst)


def rep7(lst, replacer = 0):
    ''' Using map, return new list '''

    return list(map(lambda item: item if item is not None else replacer, lst))


lst = [5]*10**6
# lst = [None]*10**6

%timeit rep1(lst)    
%timeit rep2(lst)    
%timeit rep3(lst)    
%timeit rep4(lst)    
%timeit rep5(lst)    
%timeit rep6(lst)    
%timeit rep7(lst)    

我得到:

26.3 ms ± 163 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
29.3 ms ± 206 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
33.8 ms ± 191 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
11.9 ms ± 37.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
11.9 ms ± 60.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
260 ns ± 1.84 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
56.5 ms ± 204 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

使用内置的 str.index 比手动比较要快。

我不知道测试 4 中出现异常是否比使用 str.count 更费力,但两者之间的差异似乎微不足道。

请注意,map()(测试 6)返回一个迭代器而不是实际的列表,因此需要进行测试 7。


你已经证明了,如果没有要替换的内容,使用内部的 str.index 更快。如果所有元素都是 None,我预计 rep4rep5 会非常慢,因为该方法的时间复杂度为 O(nm),而其他方法的时间复杂度为 O(n),其中 n 是元素数量,m 是 None 值的数量。 - Cris Luengo
1
@CrisLuengo:rep4/rep5的比例很好;它们都使用基于最后替换位置的“start”参数,因此它们保持为O(n);如果每次在整个列表上运行,则“index”为O(n),但是“start”参数确保所有“index”调用组合仅遍历列表的每个索引一次。随着命中次数的增加,它们变得越来越慢,但原因与大O无关(更多的固定开销支付了“index”调用);实际上,使用kxr的更好版本的rep4,1000个None只需要比1000个1长约3倍的时间。 - ShadowRanger
如果你确实想要将 index 调用的固定开销纳入考虑,那么实际工作量为 O(n + m),而不是 O(nm);你需要针对每个 None 支付一次 index 的固定开销(以及重新分配值所涉及的相关工作),所有 index 调用的累积非固定开销成本在 list 长度方面为 O(n)。但真正的大 O 计算仍然会称其为 O(n),因为 mn 限制,意味着可以将 m 解释为另一个 n 项,而 O(n+n)O(n) 相同(因为 2n 中的常系数被省略)。 - ShadowRanger
@ShadowRanger:谢谢,我以为index每次都是从开头开始搜索,没有注意到这一点。不过我的评论仍然适用:它显示了不需要替换任何内容的时间。需要更好的测试数据。 - Cris Luengo

4

这个旧但相关的问题的答案速度差异很大。

kxr发布的解决方案中,最快的是:

然而,这个 更快 的方案却没有在这里出现:

def f1(arr, find, replace):
    # fast and readable
    base=0
    for cnt in range(arr.count(find)):
        offset=arr.index(find, base)
        arr[offset]=replace
        base=offset+1

以下是各种解决方案的时间。较快的解决方案比接受的答案快3倍,比这里最慢的答案快5倍

为了公平起见,所有方法都需要对发送到函数的数组进行就地替换。

请参阅下面的计时代码:

def f1(arr, find, replace):
    # fast and readable
    base=0
    for cnt in range(arr.count(find)):
        offset=arr.index(find, base)
        arr[offset]=replace
        base=offset+1
        
def f2(arr,find,replace):
    # accepted answer
    for i,e in enumerate(arr):
        if e==find: 
            arr[i]=replace
        
def f3(arr,find,replace):
    # in place list comprehension
    arr[:]=[replace if e==find else e for e in arr]
    
def f4(arr,find,replace):
    # in place map and lambda -- SLOW
    arr[:]=list(map(lambda x: x if x != find else replace, arr))
    
def f5(arr,find,replace):
    # find index with comprehension
    for i in [i for i, e in enumerate(arr) if e==find]:
        arr[i]=replace
        
def f6(arr,find,replace):
    # FASTEST but a little les clear
    try:
        while True:
            arr[arr.index(find)]=replace
    except ValueError:
        pass    

def f7(lst, old, new):
    """replace list elements (inplace)"""
    i = -1
    try:
        while 1:
            i = lst.index(old, i + 1)
            lst[i] = new
    except ValueError:
        pass
    
    
import time     

def cmpthese(funcs, args=(), cnt=1000, rate=True, micro=True):
    """Generate a Perl style function benchmark"""                   
    def pprint_table(table):
        """Perl style table output"""
        def format_field(field, fmt='{:,.0f}'):
            if type(field) is str: return field
            if type(field) is tuple: return field[1].format(field[0])
            return fmt.format(field)     

        def get_max_col_w(table, index):
            return max([len(format_field(row[index])) for row in table])         

        col_paddings=[get_max_col_w(table, i) for i in range(len(table[0]))]
        for i,row in enumerate(table):
            # left col
            row_tab=[row[0].ljust(col_paddings[0])]
            # rest of the cols
            row_tab+=[format_field(row[j]).rjust(col_paddings[j]) for j in range(1,len(row))]
            print(' '.join(row_tab))                

    results={}
    for i in range(cnt):
        for f in funcs:
            start=time.perf_counter_ns()
            f(*args)
            stop=time.perf_counter_ns()
            results.setdefault(f.__name__, []).append(stop-start)
    results={k:float(sum(v))/len(v) for k,v in results.items()}     
    fastest=sorted(results,key=results.get, reverse=True)
    table=[['']]
    if rate: table[0].append('rate/sec')
    if micro: table[0].append('\u03bcsec/pass')
    table[0].extend(fastest)
    for e in fastest:
        tmp=[e]
        if rate:
            tmp.append('{:,}'.format(int(round(float(cnt)*1000000.0/results[e]))))

        if micro:
            tmp.append('{:,.1f}'.format(results[e]/float(cnt)))

        for x in fastest:
            if x==e: tmp.append('--')
            else: tmp.append('{:.1%}'.format((results[x]-results[e])/results[e]))
        table.append(tmp) 

    pprint_table(table)                    



if __name__=='__main__':
    import sys
    import time 
    print(sys.version)
    cases=(
        ('small, found', 9, 100),
        ('small, not found', 99, 100),
        ('large, found', 9, 1000),
        ('large, not found', 99, 1000)
    )
    for txt, tgt, mul in cases:
        print(f'\n{txt}:')
        arr=[1,2,3,4,5,6,7,8,9,0]*mul 
        args=(arr,tgt,'X')
        cmpthese([f1,f2,f3, f4, f5, f6, f7],args)   

以下是结果:

3.9.1 (default, Feb  3 2021, 07:38:02) 
[Clang 12.0.0 (clang-1200.0.32.29)]

small, found:
   rate/sec μsec/pass     f4     f3     f5     f2     f6     f7     f1
f4  133,982       7.5     -- -38.8% -49.0% -52.5% -78.5% -78.6% -82.9%
f3  219,090       4.6  63.5%     -- -16.6% -22.4% -64.8% -65.0% -72.0%
f5  262,801       3.8  96.1%  20.0%     --  -6.9% -57.8% -58.0% -66.4%
f2  282,259       3.5 110.7%  28.8%   7.4%     -- -54.6% -54.9% -63.9%
f6  622,122       1.6 364.3% 184.0% 136.7% 120.4%     --  -0.7% -20.5%
f7  626,367       1.6 367.5% 185.9% 138.3% 121.9%   0.7%     -- -19.9%
f1  782,307       1.3 483.9% 257.1% 197.7% 177.2%  25.7%  24.9%     --

small, not found:
   rate/sec μsec/pass     f4     f5     f2     f3     f6     f7     f1
f4   13,846      72.2     -- -40.3% -41.4% -47.8% -85.2% -85.4% -86.2%
f5   23,186      43.1  67.5%     --  -1.9% -12.5% -75.2% -75.5% -76.9%
f2   23,646      42.3  70.8%   2.0%     -- -10.8% -74.8% -75.0% -76.4%
f3   26,512      37.7  91.5%  14.3%  12.1%     -- -71.7% -72.0% -73.5%
f6   93,656      10.7 576.4% 303.9% 296.1% 253.3%     --  -1.0%  -6.5%
f7   94,594      10.6 583.2% 308.0% 300.0% 256.8%   1.0%     --  -5.6%
f1  100,206      10.0 623.7% 332.2% 323.8% 278.0%   7.0%   5.9%     --

large, found:
   rate/sec μsec/pass     f4     f2     f5     f3     f6     f7     f1
f4      145   6,889.4     -- -33.3% -34.8% -48.6% -85.3% -85.4% -85.8%
f2      218   4,593.5  50.0%     --  -2.2% -22.8% -78.0% -78.1% -78.6%
f5      223   4,492.4  53.4%   2.3%     -- -21.1% -77.5% -77.6% -78.2%
f3      282   3,544.0  94.4%  29.6%  26.8%     -- -71.5% -71.6% -72.3%
f6      991   1,009.5 582.4% 355.0% 345.0% 251.1%     --  -0.4%  -2.8%
f7      995   1,005.4 585.2% 356.9% 346.8% 252.5%   0.4%     --  -2.4%
f1    1,019     981.3 602.1% 368.1% 357.8% 261.2%   2.9%   2.5%     --

large, not found:
   rate/sec μsec/pass     f4     f5     f2     f3     f6     f7     f1
f4      147   6,812.0     -- -35.0% -36.4% -48.9% -85.7% -85.8% -86.1%
f5      226   4,424.8  54.0%     --  -2.0% -21.3% -78.0% -78.1% -78.6%
f2      231   4,334.9  57.1%   2.1%     -- -19.6% -77.6% -77.7% -78.2%
f3      287   3,484.0  95.5%  27.0%  24.4%     -- -72.1% -72.2% -72.8%
f6    1,028     972.3 600.6% 355.1% 345.8% 258.3%     --  -0.4%  -2.7%
f7    1,033     968.2 603.6% 357.0% 347.7% 259.8%   0.4%     --  -2.3%
f1    1,057     946.2 619.9% 367.6% 358.1% 268.2%   2.8%   2.3%     --

f6的时间复杂度是O(n^2)吗? - dawg
@dawg: f6O(n²),因为它在内部使用 index 时没有调整搜索的起始位置。在仅由要替换的内容组成的 list 中,这意味着需要调用 nindex,每次平均执行 n / 2 的工作量(第一个是 1 的工作量,最后一个是 n 的工作量,中间递增;list 的第一个元素被检查了 n 次,第二个元素被检查了 n - 1 次,以此类推)。kxr's answer 跟踪每个替换的位置,并使用它来避免重新检查,将其保持为 O(n) - ShadowRanger
@ShadowRanger:我采纳了你的意见,现在已经修复了f1,它可以跟踪基本偏移量。不再是O(n²)了。 - dawg
@dawg:没错,那个方法可行。如果没有 +1,它会重新扫描每个被替换的元素,所以在所有要替换的元素列表中,它会检查每个索引两次,而不是一次,但这是一个固定的乘数,不影响大 O 记号(避免使用 +1 可以节省意外的工作量;简单数学运算的开销非常高)。但有一个重要问题:如果替换值与搜索值相等,它将进入无限循环,因此,如果您要用 True1.0 替换 1,程序就会崩溃;我更喜欢 kxr 的方法来保险。 - ShadowRanger
附注:在你使用的版本中,没有必要有分离值。你可以删除最后一行,并将所有offset实例替换为base,它会表现得等效。或者你可以疯狂地使用海象运算符使循环体变成单行代码:arr[(base := arr.index(find, base))] = replace。 :-) - ShadowRanger
显示剩余2条评论

1
在许多情况下,定义替换函数并在循环中调用它非常易读。如果需要使用某些规则替换值,则非常有用。例如,要根据可被15、3或5整除的条件替换值,可以定义一个检查条件并返回适当值的函数。
def fizzbuzz(num):
    if num % 15 == 0:
        return 'FizzBuzz'
    elif num % 3 == 0:
        return 'Fizz'
    elif num % 5 == 0:
        return 'Buzz'
    else:
        return num

a = [1, 2, 3, 4, 5, 10, 15, 1]
a[:] = (fizzbuzz(n) for n in a)
# or 
a[:] = map(fizzbuzz, a)
a # [1, 2, 'Fizz', 4, 1, 'Buzz', 'Fizz', 2, 'Fizz', 1, 1]

需要注意的一点是(至少在Python 3.10中),如果需要对列表中的每个元素调用函数,则使用map()比使用推导式更快。1

对于内置方法(例如dict.get()),这种差异更加明显。2因此,@roipoussiere's solution可以通过以下方式简单地映射来使其速度提高一倍。

a = [1, 2, 3, 4, 1, 5, 3, 2, 6, 1, 1]
replacer = {1:10, 2:20, 3:'foo'}.get
a[:] = map(replacer, a, a)        # in-place replacement
a1 = [*map(replacer, a, a)]       # new list

1 在下面的示例中,map() 比在列表推导式中调用函数快约20%。

a = [1, 2, 3, 4, 1, 5, 3, 2, 6, 1, 1]*10000
%timeit [fizzbuzz(n) for n in a]
# 15.1 ms ± 443 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

%timeit list(map(fizzbuzz, a))
# 12.7 ms ± 27.4 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

2使用dict.get()替换值时,将其映射比在列表推导式中调用它快约2倍。

a = [1, 2, 3, 4, 1, 5, 3, 2, 6, 1, 1]*10000
replacer = {1:10, 2:20, 3:'foo'}.get

%timeit [replacer(n,n) for n in a]
# 4.64 ms ± 195 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

%timeit list(map(replacer, a, a))
# 2.21 ms ± 13.3 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接