什么时候在Python中使用列表推导式

前言

  我们都知道Python语言好、功能恐怖如斯!它允许我们编写优雅到几乎与普通英语一样易读的代码而闻名。该语言最独特的功能之一就是列表功能,它可以在一行代码中创建强大的功能。但是,很多Python开发者都在努力充分利用Python中列表的更高级功能。一些程序员甚至使用它们太多,可能导致代码效率降低且更难阅读

这篇文章将带我们了解Python列表的全部功能以及如何优雅的使用它们。还将了解使用它们所带来的权衡,以便确定何时其他方法更佳

1. 如何在Python中创建列表

  可以使用几种不同的方法在Python中创建列表。为了更好地理解在Python中使用列表的权衡,让我们首先看看如何使用这些方法创建列表

1.1 使用for循环

  循环的最常见类型for循环,可以使用for循环在三个步骤中创建元素列表:

  • 实例化一个空列表
  • 循环遍历元素的可迭代范围
  • 将每个元素追加到列表的末尾
>>> test = []
>>> for i in range(10):
...     test.append(i * i)
>>> test
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

在这里,实例化一个空列表test。然后,使用for循环来遍历range(10)。最后,将每个数字本身相乘,然后将结果追加到列表的末尾

1.2 使用map()函数

  map()提供了一种基于函数式编程的替代方法。传入一个函数和一个可迭代map()的对象,并将创建一个对象。该对象包含通过提供的功能运行每个可迭代元素所获得的输出

txns = [1, 2, 3, 4, 5]
tax_rate = .05
def get_price_with_tax(txn):
    return txn * (1 + TAX_RATE)
final_prices = map(get_price_with_tax, txns)
list(final_prices)
#[1.05, 2.1, 3.1500000000000004, 4.2, 5.25]

以上代码有一个txns和一个get_price_with_tax()。将这两个参数都传递给map(),并将结果对象存储在中final_prices。使用list将对象转换为列表list()

1.3 使用列表推导式

使用列表推导式这种优雅的方法,可以for只用一行代码就可以重写第一个示例中的循环:

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

我们不再需要创建一个空列表然后再将每个元素添加进去,只需按照以下格式同时定义列表及其内容即可:

new_list = [expression for member in iterable]

Python中的每个列表理解都包含三个元素:
  1. expression是成员本身,对方法的调用或返回值的任何其他有效表达式。在上面的示例中,表达式i * i是成员值的平方
  2. member是列表中可迭代的对象或值。在上面的示例中,成员值为i
  3. erable是一个列表,集合,序列,生成器或可以一次返回其元素的任何其他对象。在上面的示例中,iterable为range(10)

1.4 使用列表推导式的好处

在Python中使用列表推导式的一个主要好处是它可以在许多不同情况下使用。除了标准列表创建之外,列表推导式还可用于映射和过滤。不必为每种情况使用不同的方法

列表推导式也比循环更具声明性,这意味着它们更易于阅读和理解。循环要求集中精力创建列表。必须手动创建一个空列表,在元素上循环、追加

2. 如何增强理解力

2.1 使用条件逻辑

  尽管列表推导式的公式是准确的,但它也有些不完整。对理解公式的更完整的描述增加了对可选条件的支持。向列表推导式内添加条件逻辑的最常见方法是在表达式的末尾添加条件:

new_list = [expression for member in iterable (if conditional)]

条件可以帮助它们允许列表推导过滤掉不需要的值,这通常需要调用filter()


>>> test = 'hello world'
>>> result = [i for i in test if i in 'roe']
>>> result
['e', 'o', 'o', 'r']

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

条件语句放在语句的末尾可以完成简单的过滤,但如果要更改成员值而不是将其过滤掉该怎么办?在这种情况下,将条件放在表达式的开头很有用,看下它的表达式:

new_list = [expression (if conditional) for member in iterable]

这个公式可以使用条件逻辑从多个可能的输出选项中进行筛选

>>> test = [1, -1.02, 3, 4, 5, -1.16]
>>> result = [i if i > 0 else 0 for i in test]
>>> result
[1, 0, 3, 4, 5, 0]

表达式i包含一个条件语句if i > 0 else 0。这告诉Python来输出的值i,如果该数字为正数,但要改变i到0如果数字为负

2.2 使用集合跟字典

  尽管Python中的列表推导式是一种常用工具,但也可以创建集合和字典。区别在于,集合可确保输出不包含重复项。可以使用花括号而不是方括号来创建集合:

>>> test = "Hello world"
>>> result = {i for i in test if i in 'roe'}
>>> result
{'e', 'o', 'o', 'r'}

字典类似,但另外需要定义一个键:

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

要创建test字典,可以在表达式中使用花括号({})以及键值对(i: i * i)

2.3 使用Walrus运算符

Python3.8引入赋值表达式,也称为walrus运算符,举个示例

假设我们需要向API发出多个请求,这些请求将返回年龄数据。我只想返回年龄大于50的结果。假设每个请求将返回不同的数据。在这种情况下,无法使用Python中的列表理解来解决这个问题。该公式expression for member in iterable (if conditional)无法为条件式提供将数据分配给表达式可以访问的变量的方法

但是,Walrus解决了这个问题。它可以在运行表达式的同时将输出值分配给变量。以下示例显示了如何使用此方法get_age_data()来生成年龄数据:

>>> import random
>>> def get_weather_data():
...     return random.randrange(30, 60)
>>> age_temps = [temp for _ in range(20) if (temp := get_age_data()) >= 50]
>>> age_temps
[51, 55, 57, 59, 50...]

在Python中,通常不需要在列表理解内使用赋值表达式,但是在必要时,它是很有用的

3. 何时不使用Python中的列表推导式

  列表推导式很有用,可以帮助我们编写易于阅读和调试的优美代码,但并不是在所有情况下都可以选择它。因为可能使我们的代码运行速度更慢或消耗更多的内存。如果代码性能较差或难以阅读的话,最好选择一个替代方案

3.1 选择大型数据集的生成器

  Python中的列表推导式通过将整个输出列表加载到内存中来工作。对于小型甚至中型列表,通常都可以。如果要对前一千个整数的平方求和,则列表理解将很好地解决此问题:

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

  但是,如果想对十亿的整数平方求和呢?如果在计算机上尝试过此操作,则可能会注意到计算机无响应。这是因为Python试图创建一个具有十亿个整数的列表,该列表消耗的内存比计算机所需的更多。计算机可能没有生成庞大列表并将其存储在内存中所需的资源。如果仍然尝试这样做,计算机可能会变慢甚至崩溃

  如果要用一个生成器来求和前十亿平方,那么程序可能会运行一段时间,但不会导致计算机死机。下面的示例使用一个生成器

>>> sum(i * i for i in range(1000000000))
333333332833333333500000000

可以说这是一个生成器,因为表达式没有被方括号或花括号包围。生成器可以用括号括起来

上面的示例仍然需要大量工作,但是它懒惰地执行操作。由于延迟计算,因此仅在明确要求时才计算值。生成器生成一个值(例如100 * 100)后,它可以将该值加到运行总和中,然后丢弃该值并生成下一个值(101 * 101)。当求和函数要求下一个值时,循环重新开始。此过程使内存占用量较小

map() 也懒惰的工作,这意味着如果在这种情况下选择使用内存,内存将不是问题:

>>> sum(map(lambda i: i*i, range(1000000000)))
333333332833333333500000000

是否选择生成器表达式或取决于map()

3.2 配置文件以优化性能

  那么,到底哪种方法更快?应该使用列表推导式还是其他方法?与其坚持在所有情况下都适用的单一规则,不如看看在特定情况下效果是否更有用

如果在性能很重要的情况下,通常最好是分析不同的方法并检查数据。timeit是一个有用的库,用于计算要花多长时间运行代码块。可以用timeit来比较map()、for循环和列表推导式的运行时间:

>>> import random
>>> import timeit
>>> tax_rate = .05
>>> txns = [random.randrange(100) for _ in range(100000)]
>>> def get_price(txn):
...     return txn * (1 + tax_rate)
...
>>> def get_prices_with_map():
...     return list(map(get_price, txns))
...
>>> def get_prices_with_comprehension():
...     return [get_price(txn) for txn in txns]
...
>>> def get_prices_with_loop():
...     prices = []
...     for txn in txns:
...         prices.append(get_price(txn))
...     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

以上代码定义了三种方法,每种方法都使用不同的方法来创建列表。然后,告诉timeit每个功能运行100次。timeit返回运行这100次执行所花费的总时间。

我们可以通过测试结果发现,最大的区别在于基于循环的方法和map(),循环执行时间延长了50%。这是否重要取决于应用程序需求

4. 结论

通过以上示例,我们知道了如何在Python中使用列表推导式来完成复杂的任务,而又不会使我们的代码过于复杂

我们可以:

  • map()通过声明式列表简化循环和调用
  • 使用条件逻辑增强理解能力
  • 使用条件逻辑增强理解能力
  • 确定什么时候代码清晰或性能要求使用替代方法

当必须选择列表创建方法时,大家可以尝试多种实现方式,并考虑在特定情况下最容易阅读和理解的内容。如果性能很重要,那么可以使用性能分析工具测试一下去分析数据,不要凭直觉跟预感去猜测
在这里插入图片描述

5. 致谢

  好了,到这里又到了跟大家说再见的时候了。我只是一个会写爬虫的段子手而已,一个希望有朝一日能够实现财富自由,能够早日荣归故里的游子罢了。希望我的文章能带给您知识,带给您帮助,带给您欢笑!同时也谢谢您能抽出宝贵的时间阅读,创作不易,如果您喜欢的话,点个赞再走吧。您的支持是我创作的动力,希望今后能带给大家更多优质的文章!

猜你喜欢

转载自blog.csdn.net/qiulin_wu/article/details/106193882