Python 最显着的功能之一是列表推导,您可以使用它在一行代码中创建强大的功能。然而,许多开发人员很难充分利用 Python 中列表推导式的更高级功能。一些程序员甚至过多地使用它们,这可能导致代码效率降低且更难阅读。

在 Python 中转换列表

有几种不同的方法可以在 Python 中创建项目并将其添加到列表中。在本节中,你将探索循环和执行这些任务的函数。然后,您将继续学习如何使用列表推导式以及何时列表推导式可以使您的 Python 程序受益。formap()

使用循环for

最常见的循环类型是 for 循环。您可以使用循环通过三个步骤创建元素列表:for

  1. 实例化一个空列表。
  2. 循环遍历可迭代元素或元素范围。
  3. 将每个元素追加到列表的末尾。

如果要创建包含前十个完美正方形的列表,则可以在三行代码中完成这些步骤:

>>> squares = []
>>> for number in range(10):
...     squares.append(number * number)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

在这里,您实例化一个空列表 .然后,使用循环进行迭代。最后,将每个数字乘以自身,并将结果附加到列表的末尾。squaresforrange(10)

使用对象map

对于基于函数式编程的替代方法,可以使用 map()。你传入一个函数和一个可迭代对象,并将创建一个对象。此对象包含通过提供的函数运行每个可迭代元素所获得的结果。map()

例如,考虑一种情况,在这种情况下,您需要计算交易列表的税后价格:

>>> prices = [1.09, 23.56, 57.84, 4.56, 6.78]
>>> TAX_RATE = .08
>>> def get_price_with_tax(price):
...     return price * (1 + TAX_RATE)
...

>>> final_prices = map(get_price_with_tax, prices)
>>> final_prices
<map object at 0x7f34da341f90>

>>> list(final_prices)
[1.1772000000000002, 25.4448, 62.467200000000005, 4.9248, 7.322400000000001]

在这里,你有一个可迭代对象 , 和一个函数 .将这两个参数传递给 并将生成的对象存储在 中。最后,使用 转换为列表。pricesget_price_with_tax()map()mapfinal_pricesfinal_priceslist()

杠杆列表推导

列表推导是制作或转换列表的第三种方式。使用这种优雅的方法,您只需一行代码即可重写第一个示例中的循环:for

>>> squares = [number * number for number in range(10)]
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

无需创建空列表并将每个元素添加到末尾,只需按照以下格式同时定义列表及其内容:

new_list = [expression for member in iterable]

Python 中的每个列表推导都包含三个元素:

  1. expression 是成员本身、对方法的调用或返回值的任何其他有效表达式。在上面的示例中,表达式是成员值的平方。number * number
  2. member 是 List 或 Iterable 中的对象或值。在上面的示例中,成员值为 。number
  3. Iterable 是一个列表、集合、序列、生成器或任何其他可以一次返回一个元素的对象。在上面的示例中,可迭代对象为 。range(10)

由于表达式要求非常灵活,因此 Python 中的列表推导在许多您会使用 .您可以使用自己的列表推导式重写定价示例:map()

>>> prices = [1.09, 23.56, 57.84, 4.56, 6.78]
>>> TAX_RATE = .08
>>> def get_price_with_tax(price):
...     return price * (1 + TAX_RATE)
...

>>> final_prices = [get_price_with_tax(price) for price in prices]
>>> final_prices
[1.1772000000000002, 25.4448, 62.467200000000005, 4.9248, 7.322400000000001]

此实现与之间的唯一区别是 Python 中的列表推导返回列表,而不是映射对象。map()

增强您的 Python 列表理解能力

在 Python 中使用列表推导的一个好处是,它是一个可以在许多不同情况下使用的单一工具。除了标准列表创建之外,列表推导式还可用于映射和筛选。在本节中,您将找到在 Python 中使用列表推导式的高级技术。

筛选列表中的值

向列表推导式添加条件逻辑的最常见方法是在表达式的末尾添加条件逻辑。

之前,您看到了有关如何创建列表推导式以下公式:

new_list = [expression for member in iterable]

虽然这个公式是准确的,但它也有点不完整。对推导公式的更完整描述增加了对可选条件的支持。

在这里,你的条件语句正好在右括号之前:

new_list = [expression for member in iterable if conditional]

条件很重要,因为它们允许列表推导式过滤掉不需要的值,这通常需要调用 filter():

>>> sentence = "the rocket came back from mars"
>>> [char for char in sentence if char in "aeiou"]
['e', 'o', 'e', 'a', 'e', 'a', 'o', 'a']

在此代码块中,条件语句筛选掉任何非元音的字符。sentence

条件可以测试任何有效的表达式。如果你需要一个更复杂的过滤器,那么你甚至可以将条件逻辑移动到一个单独的函数中:

>>> sentence = (
...     "The rocket, who was named Ted, came back "
...     "from Mars because he missed his friends."
... )
>>> def is_consonant(letter):
...     vowels = "aeiou"
...     return letter.isalpha() and letter.lower() not in vowels
...

>>> [char for char in sentence if is_consonant(char)]
['T', 'h', 'r', 'c', 'k', 't', 'w', 'h', 'w', 's', 'n', 'm', 'd',
 'T', 'd', 'c', 'm', 'b', 'c', 'k', 'f', 'r', 'm', 'M', 'r', 's', 'b',
 'c', 's', 'h', 'm', 's', 's', 'd', 'h', 's', 'f', 'r', 'n', 'd', 's']

在这里,您将创建一个复杂的筛选器 ,并将此函数作为列表推导的条件语句传递。请注意,还会将成员值作为参数传递给函数。is_consonant()char

您可以将条件放在语句的末尾进行基本筛选,但是如果要更改成员值而不是将其过滤掉,该怎么办?在这种情况下,将条件放在表达式的开头附近很有用。您可以通过利用条件表达式来执行此操作:

new_list = [true_expr if conditional else false_expr for member in iterable]

通过将条件逻辑放在列表推导式的开头,可以使用条件逻辑从多个可能的输出选项中进行选择。例如,如果您有一个价格列表,那么您可能希望将负价格替换为正值,并保持正值不变:0

>>> original_prices = [1.25, -9.45, 10.22, 3.78, -5.92, 1.16]
>>> [price if price > 0 else 0 for price in original_prices]
[1.25, 0, 10.22, 3.78, 0, 1.16]

在这里,您的表达式是一个条件表达式 。这告诉 Python 输出 如果数字为正数,但如果数字为负数,则使用。如果这看起来势不可挡,那么将条件逻辑视为它自己的函数可能会有所帮助:price if price > 0 else 0price0

>>> def get_price(price):
...     return price if price > 0 else 0
...

>>> [get_price(price) for price in original_prices]
[1.25, 0, 10.22, 3.78, 0, 1.16]

现在,你的条件表达式包含在 中,你可以将其用作列表推导的一部分。get_price()

删除具有 set 和 dictionary 推导式的反导项

虽然 Python 中的列表推导是一种常用工具,但您也可以创建集合和字典推导式。集合推导几乎与 Python 中的列表推导完全相同。区别在于,设置推导式可确保输出不包含重复项。您可以使用大括号而不是括号来创建集合推导式:

>>> quote = "life, uh, finds a way"
>>> {char for char in quote if char in "aeiou"}
{'a', 'e', 'u', 'i'}

您的集合理解输出它在 中找到的所有唯一元音。与列表不同,集合不保证项目将按任何特定顺序保存。这就是为什么集合的第一个成员是 ,即使 中的第一个元音是 。quoteaquotei

字典推导是类似的,但需要定义一个键:

>>> {number: number * number for number in range(10)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

若要创建字典,请在表达式中使用大括号 () 和键值对 ()。{}number: number * number

使用 Walrus 运算符赋值

Python 3.8 引入了赋值表达式,也称为 walrus 运算符。若要了解如何使用它,请考虑以下示例。

假设您需要向将返回温度数据的 API 发出 10 个请求。您只想返回大于 100 华氏度的结果。假设每个请求将返回不同的数据。在这种情况下,公式无法让条件将数据分配给表达式可以访问的变量。expression for member in iterable if conditional

您需要表达式和条件中的温度,因此这是一个挑战。海象运算符 () 解决了这个问题。它允许您运行表达式,同时将输出值分配给变量。以下示例演示了如何实现此操作,用于生成虚假天气数据::=get_weather_data()

>>> import random
>>> def get_weather_data():
...     return random.randrange(90, 110)
...

>>> [temp for _ in range(20) if (temp := get_weather_data()) >= 100]
[107, 102, 109, 104, 107, 109, 108, 101, 104]

请注意,海象运算符需要位于您理解的条件部分。在 Python 中,您通常不需要在列表推导式中使用赋值表达式,但它是一个有用的工具,可以在必要时供您使用。

决定何时不使用列表推导式

列表推导式非常有用,可以帮助您编写易于阅读和调试的优雅代码,但它们并不是所有情况的正确选择。它们可能会使代码运行速度变慢或占用更多内存。如果您的代码性能较差或难以理解,那么最好选择替代方案。

注意嵌套推导式

您可以嵌套推导式,以在集合中创建列表、字典和集合的组合。例如,假设一个气候实验室正在跟踪 6 月第一周五个不同城市的高温。存储此数据的完美数据结构可以是嵌套在字典中的 Python 列表。您可以使用嵌套推导式创建数据:

>>> cities = ["Austin", "Tacoma", "Topeka", "Sacramento", "Charlotte"]
>>> {city: [0 for _ in range(7)] for city in cities}
{
    'Austin': [0, 0, 0, 0, 0, 0, 0],
    'Tacoma': [0, 0, 0, 0, 0, 0, 0],
    'Topeka': [0, 0, 0, 0, 0, 0, 0],
    'Sacramento': [0, 0, 0, 0, 0, 0, 0],
    'Charlotte': [0, 0, 0, 0, 0, 0, 0]
}

创建具有字典推导的外部字典。表达式是一个键值对,包含另一个推导式。此代码将快速生成 中每个城市的数据列表。cities

嵌套列表是创建矩阵的常用方法,您通常将其用于数学目的。请看下面的代码块:

>>> [[number for number in range(5)] for _ in range(6)]
[
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4]
]

外部列表推导式创建六行,而内部列表推导式用值填充每一行。[... for _ in range(6)][number for number in range(5)]

到目前为止,每个嵌套理解的目的都非常直观。但是,还有其他情况,例如扁平化列表,其中的逻辑可能会使您的代码更加混乱。以这个例子为例,它使用嵌套列表推导来展平矩阵:

matrix = [
...     [0, 0, 0],
...     [1, 1, 1],
...     [2, 2, 2],
... ]

>>> [number for row in matrix for number in row]
[0, 0, 0, 1, 1, 1, 2, 2, 2]

扁平化矩阵的代码很简洁,但要理解它的工作原理可能并不那么直观。另一方面,如果你使用循环来展平同一个矩阵,那么你的代码会更容易理解:for

>>> matrix = [
...     [0, 0, 0],
...     [1, 1, 1],
...     [2, 2, 2],
... ]
>>> flat = []
>>> for row in matrix:
...     for number in row:
...         flat.append(number)
...
>>> flat
[0, 0, 0, 1, 1, 1, 2, 2, 2]

现在,您可以看到代码一次遍历矩阵的一行,在转到下一行之前拉出该行中的所有元素。

为大型数据集选择生成器

Python 中的列表推导通过将整个输出列表加载到内存中来工作。对于小型甚至中型列表,这通常没问题。如果你想对前一千个整数的平方求和,那么列表推导将令人钦佩地解决这个问题:

>>> sum([number * number for number in range(1000)])
332833500

但是,如果您想将前十亿个整数的平方相加呢?如果您在计算机上尝试过,则计算机可能会变得无响应。这是因为 Python 正在尝试创建一个包含 10 亿个整数的列表,这消耗的内存比您的计算机想要的要多。如果您仍然尝试这样做,那么您的机器可能会变慢甚至崩溃。

当列表的大小出现问题时,在 Python 中使用生成器而不是列表推导通常会有所帮助。生成器不会在内存中创建单个大型数据结构,而是返回一个可迭代对象。代码可以根据需要多次请求可迭代对象中的下一个值,或者直到到达序列的末尾,同时一次只存储一个值。

如果用生成器将前十亿个平方相加,那么程序可能会运行一段时间,但不应导致计算机冻结。在下面的示例中,您将使用生成器:

>>> sum(number * number for number in range(1_000_000_000))
333333332833333333500000000

您可以判断这是一个生成器,因为表达式不在括号或大括号内。(可选)生成器可以位于括号内。

上面的例子仍然需要大量的工作,但它执行操作是懒惰的。由于延迟计算,代码仅在显式请求值时计算值。生成器生成一个值后,它可以将该值添加到运行总和中,然后丢弃该值并生成下一个值。当函数请求下一个值时,循环重新开始。此过程可减少内存占用。sum()

该函数还延迟运行,这意味着如果您选择在这种情况下使用它,内存将不是问题:map()

>>> sum(map(lambda number: number * number, range(1_000_000_000)))
333333332833333333500000000

您更喜欢生成器表达式还是 .map()

用于优化性能的配置文件

那么,哪种方法更快呢?您应该使用列表推导式还是它们的替代方案之一?与其坚持一条在所有情况下都适用的规则,不如问问自己,在特定情况下,性能是否重要。如果没有,那么通常最好选择任何导致最干净代码的方法!

如果处于性能很重要的方案中,则通常最好分析不同的方法并听取数据。timeit 库可用于计时运行代码块所需的时间。您可以使用来比较 、 循环和列表推导式 的运行时:timeitmap()for

>>> import random
>>> import timeit
>>> TAX_RATE = .08
>>> PRICES = [random.randrange(100) for _ in range(100_000)]
>>> def get_price(price):
...     return price * (1 + TAX_RATE)
...
>>> def get_prices_with_map():
...     return list(map(get_price, PRICES))
...
>>> def get_prices_with_comprehension():
...     return [get_price(price) for price in PRICES]
...
>>> def get_prices_with_loop():
...     prices = []
...     for price in PRICES:
...         prices.append(get_price(price))
...     return prices
...

>>> timeit.timeit(get_prices_with_map, number=100)
2.0554370979998566

>>> timeit.timeit(get_prices_with_comprehension, number=100)
2.3982384680002724

>>> timeit.timeit(get_prices_with_loop, number=100)
3.0531821520007725

在这里,您将定义三种方法,每种方法都使用不同的方法来创建列表。然后,您告诉每个函数运行 100 次,并返回运行这 100 次执行所花费的总时间。timeittimeit

正如您的代码所示,最大的区别在于基于循环的方法和 ,循环的执行时间延长了 50%。这是否重要取决于您的应用程序的需求。map()