提问人:Karl Knechtel 提问时间:3/8/2023 最后编辑:Karl Knechtel 更新时间:7/28/2023 访问量:2022
如何在列表、字典等中收集重复计算的结果(或复制每个元素的列表)?
How can I collect the results of a repeated calculation in a list, dictionary etc. (or make a copy of a list with each element modified)?
问:
关于这个一般主题的 Stack Overflow 上有很多现有的问答,但它们要么质量很差(通常,从初学者的调试问题中暗示),要么以其他方式错过了标记(通常是由于不够通用)。至少有两种非常常见的方法可以弄错幼稚的代码,初学者从关于循环的规范中获益更多,而不是将他们的问题作为错别字关闭或关于打印需要什么的规范。因此,这是我尝试将所有相关信息放在同一个地方的尝试。
假设我有一些简单的代码,它使用一个值进行计算并将其分配给:x
y
y = x + 1
# Or it could be in a function:
def calc_y(an_x):
return an_x + 1
现在,我想对 的许多可能值重复计算。我知道,如果我已经有要使用的值列表(或其他序列),我可以使用循环:x
for
xs = [1, 3, 5]
for x in xs:
y = x + 1
或者,如果有其他逻辑来计算值的顺序,我可以使用循环:while
x
def next_collatz(value):
if value % 2 == 0:
return value // 2
else:
return 3 * value + 1
def collatz_from_19():
x = 19
while x != 1:
x = next_collatz(x)
问题是:如何收集这些值并在循环后使用它们?我尝试了循环中的值,但它没有给我任何有用的东西:print
xs = [1, 3, 5]
for x in xs:
print(x + 1)
结果显示在屏幕上,但我找不到任何在代码的下一部分使用它们的方法。所以我认为我应该尝试将值存储在容器中,例如列表或字典。但是当我尝试这样做时:
xs = [1, 3, 5]
for x in xs:
ys = []
y = x + 1
ys.append(y)
或
xs = [1, 3, 5]
for x in xs:
ys = {}
y = x + 1
ys[x] = y
在任一尝试之后,仅包含最后一个结果。ys
答:
一般方法
有三种常见的方法可以解决这个问题:显式使用循环(通常是循环,但也可以使用循环);通过使用列表推导式(或字典推导式、集合推导式或生成器表达式,以适应上下文中的特定需求);或者使用内置的(其结果可用于显式构造列表、集合或字典)。for
while
map
使用显式循环
在循环之前创建一个列表或字典,并在计算时添加每个值:
def make_list_with_inline_code_and_for():
ys = []
for x in [1, 3, 5]:
ys.append(x + 1)
return ys
def next_collatz(value):
if value % 2 == 0:
return value // 2
else:
return 3 * value + 1
def make_dict_with_function_and_while():
x = 19
ys = {}
while x != 1:
y = next_collatz(x)
ys[x] = y # associate each key with the next number in the Collatz sequence.
x = y # continue calculating the sequence.
return ys
在这两个示例中,循环被放入函数中,以便标记代码并使其可重用。这些示例返回
该值,以便调用代码可以使用结果。但是,当然,计算出来的也可以稍后在同一个函数中使用,像这样的循环也可以写在任何函数之外。ys
ys
当存在现有输入时,请使用 for
循环,其中每个元素都应独立处理。使用 while
循环创建输出元素,直到满足某个条件。Python 不直接支持运行特定次数的循环(提前计算);通常的成语是制作一个适当长度的假人,然后用它做一个循环。range
for
使用推导式或生成器表达式
列表推导式为从现有值序列创建列表提供了优雅的语法。在可能的情况下,它应该是首选,因为这意味着代码不必关注如何构建列表的细节,从而使其更易于阅读。它也可以更快,尽管这通常无关紧要。
它可以与函数调用或其他计算(“源”元素的任何表达式)一起使用,它看起来像:
xs = [1, 3, 5]
ys = [x + 1 for x in xs]
# or
def calc_y(an_x):
return an_x + 1
ys = [calc_y(x) for x in xs]
请注意,这不会取代循环;没有有效的语法替换为 here。一般来说,列表推导式用于获取现有值并对每个值进行单独的计算 - 而不是用于任何涉及从一次迭代到下一次迭代“记住”任何内容的逻辑(尽管这可以解决,尤其是在 Python 3.8 及更高版本中)。while
for
while
同样,可以使用字典推导创建字典结果 - 只要在每次迭代中同时计算键和值即可。根据确切的需要,集合推导式(生成不包含重复值的 ,)和生成器表达式(生成延迟计算的结果;参见下文关于和生成器表达式)也可能是合适的。set
map
使用地图
这类似于列表推导,但更具体。 是一个内置函数,可以将函数重复应用于来自某个输入序列(或多个序列)的多个不同参数。map
获取与前面代码等效的结果如下所示:
xs = [1, 3, 5]
def calc_y(an_x):
return an_x + 1
ys = list(map(calc_y, xs))
# or
ys = list(map(lambda x: x + 1, xs))
除了需要输入序列(它不替换循环)之外,还需要使用函数或其他可调用对象来完成计算,例如上面显示的 lambda(其中任何一个,当传递给 时,都是所谓的“高阶函数”)。while
map
在 Python 3.x 中,是一个类,因此调用它会创建该类的实例 - 该实例是一种特殊类型的迭代器(不是列表),不能多次迭代。(我们可以使用生成器表达式而不是列表推导式来获得类似的东西;只需使用 而不是 。map
()
[]
因此,上面的代码从映射的值显式创建一个列表。在其他情况下,可能没有必要这样做(即,如果它只会迭代一次)。另一方面,如果 a 是必需的,则对象可以直接传递给而不是以相同的方式传递。要生成字典,应设置 每个输出元素都是一个元组;那么它可以传递给 ,如下所示:set
map
set
list
map
(key, value)
dict
def dict_from_map_example(letters):
return dict(map(lambda l: (l, l.upper()), letters))
# equivalent using a dict comprehension:
# return {l:l.upper() for l in letters}
一般来说,与
列表推导式相比,map 是有限且不常见的,在大多数代码中应该首选列表推导式。但是,它确实提供了一些优势。特别是,它可以避免指定和使用迭代变量的需要:当我们编写 时,我们不需要编造 an 来命名 的元素,也不必编写代码来传递它(如在列表推导等价物中,注意两个 s)。有些人觉得这更优雅。list(map(calc_y, xs))
x
xs
calc_y
[calc_y(x) for x in xs]
x
常见错误和陷阱
尝试通过赋值给缺少的索引来追加元素
有时人们会错误地尝试用如下内容来实现循环代码:
xs = [1, 3, 5]
ys = []
for i, x in enumerate(xs):
ys[i] = x + 1
只能分配给列表中已经存在的索引 - 但在这里,列表开始时是空的,所以还没有任何东西存在。第一次通过循环将引发一个 .请改用该方法追加值。IndexError
.append
还有其他更晦涩难懂的方法,但它们没有真正的意义。特别是:“预先分配”列表(在某些情况下,类似的东西可能会提供小的性能改进,但它很丑陋,更容易出错,并且只有在可以提前知道元素数量的情况下才有效(例如,如果实际上来自使用相同的循环读取文件,它将不起作用)。ys = [None] * len(xs)
xs
使用不当append
lists 的方法返回 None
,而不是追加到的列表。有时人们会错误地尝试这样的代码:append
xs = [1, 3, 5]
ys = []
for x in xs:
ys = ys.append(x) # broken!
第一次通过循环,将修改列表,并计算为 ,然后将其分配给 。第二次通过,是 ,所以调用 会引发一个 .ys.append(x)
ys
None
ys =
None
ys
ys
None
.append
AttributeError
list.append
在理解中
像这样的代码是行不通的:
# broken!
xs = [1, 3, 5]
y = []
y = [y.append(x + 1) for x in xs]
有时这是由于思维不明确造成的;有时,这是由于尝试将带有循环的旧代码转换为使用理解,并且没有进行所有必要的更改。
如果故意这样做,它显示了对列表理解的误解。.append
方法返回 None
,因此该值最终(重复)出现在推导式创建的列表中。但更重要的是,它在概念上是错误的:推导的目的是从计算值构建列表,所以调用是没有意义的——它试图做推导已经负责的工作。虽然可以跳过此处的赋值(然后已经附加了适当的值),但使用列表推导式来描述其副作用是很糟糕的风格 - 尤其是当这些副作用做了一些推导可以自然做的事情时。.append
y
在循环中重新创建新列表
显式循环代码中的关键点是设置为初始空或列表或字典一次。它确实需要发生(以便可以添加元素或插入键),但是在循环中这样做意味着结果会不断被覆盖。ys
也就是说,此代码已损坏:
def broken_list_with_inline_code_and_for():
for x in [1, 3, 5]:
ys = []
ys.append(x + 1)
return ys
一旦解释,这应该是显而易见的,但对于新程序员来说,这是一个非常常见的逻辑错误。每次通过循环,再次成为,然后添加一个元素 - 在再次成为之前,下一次通过循环。ys
[]
[]
有时人们这样做是因为他们认为应该“限定”循环 - 但这不是很好的推理(毕竟,重点是能够在循环完成后使用!),无论如何,Python 不会为循环创建单独的范围。ys
ys
尝试使用多个输入而不使用zip
使用循环或理解的代码需要特殊处理,以便“配对”来自多个输入源的元素。这些方法行不通:
# broken!
odds = [1, 3, 5]
evens = [2, 4, 6]
numbers = []
for odd, even in odds, evens:
numbers.append(odd * even)
# also broken!
numbers = [odd * even for odd, even in odds, evens]
这些尝试将引发 .问题在于创建单个列表元组;循环或理解将尝试遍历该元组(因此该值将是第一次通过和第二次通过),然后将该值解压缩到 and 变量中。由于其中有三个值,并且 和 只是两个独立的变量,因此此操作失败。即使它确实有效(例如,如果 和 恰好是正确的长度),结果也会是错误的,因为迭代的顺序是错误的。ValueError
odds, evens
[1, 3, 5]
[2, 4, 6]
odd
even
[1, 3, 5]
odd
even
odds
evens
解决方案是使用 zip
,如下所示:
# broken!
odds = [1, 3, 5]
evens = [2, 4, 6]
numbers = []
for odd, even in zip(odds, evens):
numbers.append(odd * even)
# or
numbers = [odd * even for odd, even in zip(odds, evens)]
当使用而不是循环或理解时,这不是问题 - 配对是通过自动完成的:map
map
numbers = list(map(lambda x, y: x * y, odds, evens))
尝试修改输入列表
列表推导式从输入创建一个新列表,并类似地迭代新结果。这两种方法都不适合尝试直接修改输入列表。但是,可以将原始列表替换为新列表:map
xs = [1, 3, 5]
ys = xs # another name for that list
xs = [x + 1 for x in xs] # ys will be unchanged
或者使用切片分配替换其内容:
xs = [1, 3, 5]
ys = xs
# The actual list object is modified, so ys is changed too
xs[:] = [x + 1 for x in xs]
给定一个输入列表,可以使用显式循环将列表元素替换为计算结果 - 但是,这并不简单。例如:
numbers = [1, 2, 3]
for n in numbers:
n += 1
assert numbers == [1, 2, 3] # the list will not change!
只有当底层对象实际被修改时,这种列表修改才有可能 - 例如,如果我们有一个列表列表,并修改每个列表:
lol = [[1], [3]]
for l in lol:
# the append method modifies the existing list object.
l.append(l[0] + 1)
assert lol == [[1, 2], [3, 4]]
另一种方法是保留索引并分配回原始列表:
numbers = [1, 2, 3]
for i, n in enumerate(numbers):
numbers[i] = n + 1
assert numbers == [2, 3, 4]
但是,在几乎所有正常情况下,创建一个新列表会是一个更好的主意。
一个不那么特殊的情况:降低字符串列表
这个问题的许多重复项专门寻求将字符串的输入列表全部转换为小写(或全部转换为大写)。这并不特别;任何解决问题的实用方法都将涉及解决“小写单个字符串”和“重复计算并收集结果”(即这个问题)的问题。但是,这是一个有用的演示案例,因为计算涉及使用列表元素的方法。
一般方法如下所示:
def lowercase_with_explicit_loop(strings):
result = []
for s in strings:
result.append(s.lower())
return result
def lowercase_with_comprehension(strings):
return [s.lower() for s in strings]
def lowercase_with_map(strings):
return list(map(str.lower, strings))
但是,这里有两个有趣的观点要提出。
请注意版本的不同之处。虽然当然可以创建一个接受字符串并返回方法调用结果的函数,但这不是必需的。取而代之的是,我们可以直接从类中查找该方法(此处),该方法在 3.x 中会生成一个完全普通的函数(在 2.x 中会生成一个“未绑定”方法,然后可以使用实例作为显式参数调用该方法 - 这相当于相同的事情)。当一个字符串被传递给 时,结果是一个新字符串,它是输入字符串的小写版本 - 即,正是工作所需的函数。
其他方法不允许这种简化;循环或使用推导式/生成器表达式需要为迭代(循环)变量选择一个名称(在这些示例中)。map
lower
str
str.lower
map
s
有时,在编写显式循环版本时,人们希望能够在原始列表中就地编写并转换字符串。如上所述,可以使用这种通用方法修改列表,但只能使用实际修改对象的方法。Python 的字符串是不可变的,所以这是行不通的。
s.lower()
strings
当输入为字符串时
字符串可以直接迭代。但是,通常当输入是字符串时,也需要单个字符串作为输出。列表推导式将生成一个列表,而生成器表达式同样会生成一个生成器。
有许多可能的策略可以将结果合并成一个字符串;但是,对于将字符串中的每个字符“翻译”或“映射”到某些输出文本的常见情况,使用内置的字符串功能更简单、更高效:字符串的方法以及字符串类提供的静态方法。translate
maketrans
该方法直接根据输入中的字符创建字符串。它需要一个字典,其中键是 Unicode 码位编号(应用于单字符字符串的结果),值是 Unicode 码位编号、字符串或 None。它将遍历输入字符串,按数字查找。如果未找到输入字符,则将其复制到输出字符串(它将在内部使用缓冲区,并且仅在末尾创建字符串对象)。如果映射确实包含字符代码点的条目:translate
ord
- 如果是字符串,则将复制该字符串。
- 如果是另一个码位,则将复制相应的字符。
- 如果是 ,则不会复制任何内容(与空字符串的效果相同)。
None
由于这些映射很难手动创建,因此该类提供了一种提供帮助的方法。它可以采用字典,也可以采用两个或三个字符串。str
maketrans
- 当给定字典时,它应该与方法期望的字典类似,只是它也可以使用单字符字符串作为键。 将用相应的代码点替换这些代码点。
translate
maketrans
- 当给定两个字符串时,它们需要具有相同的长度。 将第一个字符串的每个字符作为键,第二个字符串中的对应字符作为对应的值。
maketrans
- 当给定三个字符串时,前两个字符串的工作方式与以前相同,第三个字符串包含将映射到 .
None
例如,下面是在解释器提示符下实现的简单 ROT13 密码的演示:
>>> import string
>>> u, l = string.ascii_uppercase, string.ascii_lowercase
>>> u_rot, l_rot = u[13:] + u[:13], l[13:] + l[:13]
>>> mapping = str.maketrans(u+l, u_rot+l_rot)
>>> 'Hello, World!'.translate(mapping)
'Uryyb, Jbeyq!'
该代码生成大写和小写字母的旋转和正常版本,然后用于将字母映射到在同一大小写中移动 13 个位置的相应字母。然后应用此映射。作为参考,映射如下所示:str.maketrans
.translate
>>> mapping
{65: 78, 66: 79, 67: 80, 68: 81, 69: 82, 70: 83, 71: 84, 72: 85, 73: 86, 74: 87, 75: 88, 76: 89, 77: 90, 78: 65, 79: 66, 80: 67, 81: 68, 82: 69, 83: 70, 84: 71, 85: 72, 86: 73, 87: 74, 88: 75, 89: 76, 90: 77, 97: 110, 98: 111, 99: 112, 100: 113, 101: 114, 102: 115, 103: 116, 104: 117, 105: 118, 106: 119, 107: 120, 108: 121, 109: 122, 110: 97, 111: 98, 112: 99, 113: 100, 114: 101, 115: 102, 116: 103, 117: 104, 118: 105, 119: 106, 120: 107, 121: 108, 122: 109}
手工制作不是很实用。
评论
dict(zip(map(ord, u+l), u_rot+l_rot))
评论
filter
print
return
return
从循环中取回多个值?我可以把它们放在列表中吗?我在答案中引用了它。转念一想,它肯定只是相关的,而不是重复的,因为它解决了一个函数每次调用只能一次的单独和关键问题。return