查找和替换列表中的元素
Posted
技术标签:
【中文标题】查找和替换列表中的元素【英文标题】:Finding and replacing elements in a list 【发布时间】:2011-02-04 15:28:32 【问题描述】:我必须搜索一个列表并将所有出现的一个元素替换为另一个。到目前为止,我在代码方面的尝试无济于事,最好的方法是什么?
例如,假设我的列表有以下整数
>>> a = [1,2,3,4,5,1,2,3,4,5,1]
我需要用值 10 替换所有出现的数字 1,所以我需要的输出是
>>> a = [10, 2, 3, 4, 5, 10, 2, 3, 4, 5, 10]
因此,我的目标是用数字 10 替换数字 1 的所有实例。
【问题讨论】:
【参考方案1】:尝试使用list comprehension 和conditional expression。
>>> a=[1,2,3,1,3,2,1,1]
>>> [4 if x==1 else x for x in a]
[4, 2, 3, 4, 3, 2, 4, 4]
【讨论】:
但这并没有改变a
,对吧?我认为 OP 希望 a
改变
@Dula 你可以做 a = [4 if x==1 else x for x in a],这会影响 a
@Dula:关于a
是否应该变异的问题含糊不清,但是(如 Alekhya 所示)在使用列表理解时处理这两种情况都很简单。
如果你想改变a
,那么你应该做a[:] = [4 if x==1 else x for x in a]
(注意完整的列表切片)。只需执行a =
将创建一个新列表a
,其id()
(身份)与原始列表不同
仅出于评估目的,请注意,此解决方案是迄今为止快速解决方案中最准时的解决方案(无论要替换的项目是常见还是稀有,运行时间都保持有效不变) .当list
主要是保持不变的项目时,这比优化的就地解决方案(如kxr's answer)要慢。 kxr 的答案,对于 len 1000 个输入,需要 1/3 的时间(当没有需要更换的物品时)到 3 倍的时间(当所有的物品都必须更换时);更多变数。【参考方案2】:
>>> 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]
>>>
【讨论】:
中速但非常明智的方法。请在我的回答中查看时间安排。【参考方案3】:您可以在迭代列表时使用内置的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]
【讨论】:
这是一个糟糕且非常非 Python 的解决方案。考虑使用列表推导。 这是一个很好的非 Python 解决方案。考虑使用列表推导。 这比列表理解性能更好,不是吗?它进行就地更新而不是生成新列表。 @neverendingqs:不。解释器的开销在操作中占主导地位,而理解的开销更少。理解的表现稍好一些,尤其是在通过替换条件的元素比例较高的情况下。有一些时间安排:ideone.com/ZrCy6z 与使用像.index(10)
这样的原生列表方法相比,这真的很慢。没有理由列出每个列表元素来查找需要替换的元素。请在此处查看我的答案中的时间。【参考方案4】:
列表推导效果很好,使用 enumerate 循环可以节省一些内存(b/c 操作基本上就地完成了)。
还有函数式编程。查看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]
【讨论】:
+1。太糟糕了lambda
和map
被认为是非pythonic。
我不确定 lambda 或 map 本质上是 unpythonic,但我同意列表推导比同时使用它们两者更清晰、更易读。
我自己并不认为它们是非 Python 的,但很多人认为它们是非 Python 的,包括 Guido van Rossum (artima.com/weblogs/viewpost.jsp?thread=98196)。这是宗派主义的事情之一。
@outis: map
+lambda
的可读性和比等效的 listcomp 慢。当映射函数是在 C 中实现的内置函数并且输入足够大以使 map
的每项优势克服稍高的固定开销时,您可以从 map
中挤出一些性能,但是当 @987654330 @ 需要一个 Python 级别的函数(例如 lambda
),一个等效的geneexpr/listcomp 可以内联(避免函数调用开销),map
确实没有任何好处(从 3.9 开始,对于 a = [*range(10)] * 100
上的简单测试用例) ,这个 map
需要 2 倍的等效 listcomp)。
就我个人而言,我主要为lambda
保留我的愤怒;我喜欢map
,因为我已经有一个函数可以满足我的需要(该函数可能足够复杂以至于不值得在 listcomp 中内联,或者它是一个你无论如何都不能内联的内置函数,例如@ 987654337@ 以从文件中逐一获取预先设置的行),但如果我没有这样的功能,我将不得不使用lambda
,它最终会变得更丑陋且更慢,如前所述,所以我不妨使用 listcomp/genexpr。【参考方案5】:
如果要替换多个值,也可以使用字典:
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]
请注意,这种方法仅在要替换的元素是可散列的情况下才有效。这是因为字典键必须是可散列的。
【讨论】:
@jrjc @roipoussiere 对于就地替换,try-except
至少快 50%!看看这个answer
谢谢! try-except
更快,但它会在第一次出现未知项目时打破循环,dic.get(n,n)
很漂亮但比if n in dic
慢。我编辑了我的答案。
这对于不可散列的元素将失败。这是所有基于 dict 的简单替换的问题。 (试试[1, 'boom!', 3]
)
@Iftah:如果您非常关心性能,在 listcomp 之外预绑定get
方法将大大减少大型输入的运行时间。由于您实际上不需要引用dict
本身,您可以将其更改为dget = 1:10, 2:20, 3:'foo'.get
,并将listcomp 更改为[dget(n, n) for n in a]
。即使在显着优化方法调用的 CPython 3.9 中(在简单的情况下,它不再需要创建绑定的方法对象),这仍然将 len 1000 输入的开销减少了约 30%(通过将 LOAD_METHOD
/CALL_METHOD
替换为只是CALL_FUNCTION
)。
预先绑定的get
优化使这一点可以与dic[n] if n in dic else n
方法相媲美(在我的大多数测试用例中,需要20-30% 的时间,而60-100%当您必须在每个循环中查找 dic.get
时,时间会更长)。【参考方案6】:
我知道这是一个非常古老的问题,并且有无数种方法可以解决。我发现的更简单的是使用numpy
包。
import numpy
arr = numpy.asarray([1, 6, 1, 9, 8])
arr[ arr == 8 ] = 0 # change all occurrences of 8 by 0
print(arr)
【讨论】:
假设您已经在使用numpy
,这是一个很好的解决方案; O(n)
与所有其他好的解决方案相同,但将所有工作推向矢量化 C 层操作意味着它将通过消除每个项目的解释器开销而显着优于其他解决方案。【参考方案7】:
与其他答案中提供的单步迭代方法相比,使用 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
【讨论】:
这是我找到的最快的方法。请在我的回答中查看时间安排。太好了! 请注意,它的原始版本(不使用i
为start
提供start
参数)是O(n²)
;在一个简单的本地测试中,lst
参数是 list(range(10)) * 100
的结果(1000 个元素 list
,其中 100 个元素,均匀分布,被替换),这是一个明显的;这个答案(这不是幼稚的,并且达到了O(1)
的性能)在大约 25 µs 内完成了工作,而幼稚版本在同一台机器上花费了大约 615 µs。【参考方案8】:
我的用例是用一些默认值替换 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
,我希望rep4
和rep5
会非常慢,因为方法是O(nm),而其他元素是O(n),有n 个元素和m 个None
值.
@CrisLuengo: rep4
/rep5
缩放精细;它们都使用基于最后替换位置的start
参数,所以它们仍然是O(n)
; index
是 O(n)
如果每次都在整个 list
上运行,但 start
参数确保所有 index
调用放在一起遍历 list
的每个索引恰好一次。随着点击次数的增加,它们会变慢,但由于与大 O 无关的原因(index
调用的固定开销支付更多);在实践中,使用 kxr's better version of rep4
, 1000 None
s 只需要比 1000 1
s 长约 3 倍的时间。
如果您确实想合并index
调用的固定开销,真正完成的工作是O(n + m)
,而不是O(nm)
;您为每个None
支付一次index
的固定开销(以及重新分配值的相关工作),并且所有index
调用的累积非固定开销成本合计为O(n)
list
的长度。真正的 big-O 计算仍将其称为 O(n)
,因为 m
以 n
为界,这意味着 m
可以解释为另一个 n
术语,而 O(n + n)
与 @987654362 相同@(因为 2n
中的常数系数被删除)。
@ShadowRanger:谢谢,我以为index
每次都从头搜索,没注意。不过,我对测试的评论仍然有效:它显示了不需要更换任何东西的时间。需要更好的测试数据。【参考方案9】:
这个古老但相关的问题的答案在速度上千差万别。
kxr 发布的solution 中最快。
但是,这甚至会更快,否则就不在这里了:
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 倍。
公平地说,所有方法都需要对发送给函数的数组进行 inlace 替换。
请看下面的计时码:
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'\ntxt:')
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% --
【讨论】:
您的 f1 和 f6 是 O(n^2),因此对于足够大的列表,它们最终会比 O(n) 解决方案慢得多。对于某些长度的列表,可能值得找到近似的交叉和切换策略。 f6 O(n^2) 怎么样? @dawg:f6
是 O(n²)
因为它在内部使用 index
而不调整搜索的开始位置。在一个只包含要替换的东西的list
中,这意味着n
调用index
,每个调用平均n / 2
工作(第一个是1
工作,最后一个n
工作, 它在两者之间计数;list
的第一个元素被检查n
次,第二个 n - 1
次等)。 kxr's answer 跟踪每个替换的位置并使用它来避免重新检查,将其保留为 O(n)
。
@ShadowRanger:我拿走了你的 cmets 并修复了 f1
,所以现在它跟踪基本偏移量。没有更多的O(n²)
@dawg:是的,这行得通。如果没有+1
,它会重新扫描它刚刚替换的每个元素,因此在要替换的所有元素的list
中,它会检查每个索引两次,而不是只检查一次,但这是一个固定的乘数,不会影响大-O(避免+ 1
可以节省大量工作;简单数学的开销非常高)。有一个重大问题:如果替换值与搜索值比较,它将进入无限循环,因此,如果您将1
替换为True
或1.0
,kaboom;我更喜欢 kxr 的防弹方法。【参考方案10】:
我可能是个笨蛋,但我会为此编写一个单独的简单函数:
def convertElements( oldlist, convert_dict ):
newlist = []
for e in oldlist:
if e in convert_dict:
newlist.append(convert_dict[e])
else:
newlist.append(e)
return newlist
然后根据需要调用它,如下所示:
a = [1,2,3,4,5,1,2,3,4,5,1]
a_new = convertElements(a, 1: 10)
## OUTPUT: a_new=[10, 2, 3, 4, 5, 10, 2, 3, 4, 5, 10]
【讨论】:
不需要if/else
。只需执行newlist.append(convert_dict.get(e, e))
。 get
方法有一个 default
参数,如果键不在字典中,则返回该参数。所以如果不是,就返回吧……那它也可以更方便的变成list-comp:newlist = [convert_dict.get(e, e) for e in oldlist]
以上是关于查找和替换列表中的元素的主要内容,如果未能解决你的问题,请参考以下文章