如何在列表、字典等中收集重复计算的结果(或复制每个元素都经过修改的列表)?
- 2024-11-20 08:43:00
- admin 原创
- 7
问题描述:
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
仅包含最后的结果。
解决方案 1:
一般方法
有三种常用的方法来解决这个问题:明确使用循环(通常是循环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
此处的两个示例中,循环都被放入函数中,以便标记代码并使其可重复使用。这些示例的return
值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]
请注意,这不会替代循环;这里while
没有有效的语法替换。一般来说,列表推导式用于获取现有值并对每个值进行单独的计算 - 不适用于任何涉及从一次迭代到下一次迭代“记住”任何内容的逻辑(尽管这可以解决,特别是在 Python 3.8 及更高版本中)。for
`while`
类似地,只要在每次迭代中计算键和值,就可以使用字典推导来创建字典结果。根据确切需求,集合推导(生成set
不包含重复值的)和生成器表达式(生成惰性求值结果;请参阅下文关于map
和生成器表达式的内容)也可能适用。
使用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))
除了需要输入序列(它不会替代循环while
)之外,计算还需要使用函数或其他可调用函数来完成,比如上面显示的lambda(当传递给时,其中任何一个map
都是所谓的“高阶函数”)。
在 Python 3.x 中,map
是一个类,因此调用它会创建该类的一个实例 - 并且该实例是一种特殊的迭代器(而不是列表),不能迭代多次。(我们可以使用生成器表达式而不是列表推导来获得类似的结果;只需使用()
而不是[]
。)
因此,上面的代码明确地从映射值中创建了一个列表。在其他情况下,可能不需要这样做(即,如果只会迭代一次)。另一方面,如果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
与列表推导相比,列表推导是有限且不常见的,在大多数代码中应该首选列表推导。但是,它确实提供了一些优势。特别是,它可以避免指定和使用迭代变量的需要:当我们编写时list(map(calc_y, xs))
,我们不需要编写一个x
来命名元素xs
,也不必编写代码将其传递给calc_y
(如列表推导等价物,[calc_y(x) for x in xs]
-注意两个x
s)。有些人觉得这更优雅。
解决方案 2:
常见错误和陷阱
尝试通过分配缺失索引来附加元素
有时人们会错误地尝试使用类似以下的代码来实现循环代码:
xs = [1, 3, 5]
ys = []
for i, x in enumerate(xs):
ys[i] = x + 1
只能将已存在的列表中的索引赋值 - 但此处,列表从空开始,因此尚无任何内容。第一次循环将引发IndexError
。相反,请使用.append
方法附加值。
还有其他更晦涩难懂的方法,但它们没有实际意义。特别是:“预分配”列表(使用类似ys = [None] * len(xs)
在某些情况下可能会带来轻微的性能改进,但它很丑陋,更容易出错,并且只有在可以提前知道元素数量的情况下才有效(例如,如果xs
实际上来自使用相同循环读取文件,它将不起作用)。
使用append
不当
append
列表的方法返回的是列表None
,而不是附加到的列表。有时人们会错误地尝试如下代码:
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
变为[]
,然后添加一个元素 - 然后[]
再变为,然后是下一次循环。
有时人们这样做是因为他们认为应该“作用于”循环 - 但这不是好的理由(毕竟,整个要点是在循环完成后ys
能够使用!),并且 Python不会为循环创建单独的作用域。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]
这些尝试将引发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))
不过,这里有两个有趣的观点。
注意
map
版本的不同。虽然当然可以创建一个函数,该函数接受一个字符串并返回方法调用的结果,但这不是必需的。相反,我们可以直接从类中查找lower
方法(此处为),这在 3.x 中会产生一个非常普通的函数(而在 2.x 中会产生一个“未绑定”的方法,然后可以使用实例作为显式参数来调用该方法 - 这相当于同一件事)。当将字符串传递给时,结果是一个新字符串,它是输入字符串的小写版本 - 即,正是工作所需的函数。
其他方法不允许这种简化;循环或使用理解/生成器表达式需要为迭代(循环)变量选择一个名称(在这些示例中)。str
`str.lower`map
s
有时,在编写显式循环版本时,人们希望能够
s.lower()
在原始列表中直接写入并转换字符串strings
。如上所述,可以使用这种通用方法修改列表 - 但只能使用实际修改对象的方法。Python 的字符串是不可变的,因此这不起作用。
解决方案 3:
当输入是字符串时
字符串可以直接迭代。但是,通常当输入是字符串时,也期望输出单个字符串。列表推导将生成一个列表,生成器表达式同样将生成一个生成器。
有许多可能的策略将结果连接成一个字符串;但对于将字符串中每个字符“翻译”或“映射”为某些输出文本的常见情况,使用内置字符串功能更简单、更有效:字符串的方法,以及字符串类提供的translate
静态方法。maketrans
该translate
方法直接根据输入中的字符创建一个字符串。它需要一个字典,其中的键是 Unicode 代码点数字(应用于ord
单字符字符串的结果),值是 Unicode 代码点数字、字符串或 None。它将遍历输入字符串,按数字查找。如果未找到输入字符,则将其复制到输出字符串(它将在内部使用缓冲区,并且仅在末尾创建一个字符串对象)。如果映射确实包含字符代码点的条目:
如果它是一个字符串,那么该字符串将被复制。
如果是另一个代码点,则会复制相应的字符。
如果是
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!'
该代码生成大写和小写字母的旋转版本和正常版本,然后使用str.maketrans
将字母映射到相同大小写中移位 13 个位置的相应字母。然后.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}
手工制作不太实用。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件