如何在列表、字典等中收集重复计算的结果(或复制每个元素都经过修改的列表)?

2024-11-20 08:43:00
admin
原创
7
摘要:问题描述:Stack Overflow 上有大量关于这个主题的现有问答,但它们要么质量很差(通常是初学者调试问题所暗示的),要么在其他方面没有达到目标(通常是因为不够通用)。至少有两种极其常见的方式会导致幼稚的代码出错,对于初学者来说,关于循环的规范比他们的问题被关闭为拼写错误或关于打印需要什么的规范更有益。...

问题描述:

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

此处的两个示例中,循环都被放入函数中,以便标记代码并使其可重复使用。这些示例的returnys是为了让调用代码可以使用结果。但当然,计算结果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]-注意两个xs)。有些人觉得这更优雅。

解决方案 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 = 将其赋值给Noneys第二次循环时,ysNone,因此对的调用会.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]第二次迭代的值将是 ),然后将该值解包到oddeven变量中。由于[1, 3, 5]中有三个值,并且oddeven只是两个独立的变量,因此此方法会失败。即使它确实有效(例如,如果oddsevens恰好是正确的长度),结果也会是错误的,因为迭代的顺序是错误的。

解决方案是使用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))

不过,这里有两个有趣的观点。

  1. 注意map版本的不同。虽然当然可以创建一个函数,该函数接受一个字符串并返回方法调用的结果,但这不是必需的。相反,我们可以直接从类中查找lower方法(此处为),这在 3.x 中会产生一个非常普通的函数(而在 2.x 中会产生一个“未绑定”的方法,然后可以使用实例作为显式参数来调用该方法 - 这相当于同一件事)。当将字符串传递给时,结果是一个新字符串,它是输入字符串的小写版本 - 即,正是工作所需的函数。
    其他方法不允许这种简化;循环或使用理解/生成器表达式需要为迭代(循环)变量选择一个名称(在这些示例中)。str`str.lower`map

s

  1. 有时,在编写显式循环版本时,人们希望能够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}

手工制作不太实用。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用