列表推导式:简洁高效更具 Python 风格的列表创建方法
我们在《Python 中的列表和元组》中已经详细介绍了列表(list)的基本特性和使用方法,本文将着重介绍一种 Python 中用于创建 list 的简洁高效的语法形式:列表推导式。
Python 之所以广受欢迎,有一个重要的原因是 Python 代码风格优雅,容易编写,并且几乎和普通英语一样易读(这当然是针对大多数母语为英语的开发者)。列表推导式就是 Python 中体现这一因素的语言特性。
列表推导式允许你使用一行代码就可以实现用于创建 list 的复杂逻辑。看起来是件美妙的事情,然而,就像“劲酒虽好,可不要贪杯哦”(呃,这不是广告哦),过度或不当地使用列表推导式,反而会降低代码的可读性,甚至导致程序效率的降低。
话不多说,我们现在开始步入正题。
【Python 中如何创建 list】
Python 中有几种不同的方法可以创建 list。为更好地理解使用列表推导式时的权衡取舍,我们先看一下如何使用这些方法创建 list。
1,使用 for 循环
for 循环是 Python 中最常用的循环类型。我们可以使用 for 循环来创建一个 list,只需三步:
初始化一个空的 list 对象
循环遍历一个包含元素的 iterable 或 range
将各元素追加到 list 尾部
比如,我们可以使用以下三行代码创建一个包含 10 个平方数的 list。
>>> squares = []
>>> for i in range(10):
... squares.append(i * i)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
2,使用 map() 对象
map() 提供了一个基于函数式编程的用于创建 list 的可选方法。你向其中传递一个函数 f 和一个 iterable,map() 将创建一个对象,该对象包含了 iterable 中的每个元素经由函数 f 计算后的全部输出结果。
这里给出一个使用 map() 的例子。假设我们有一批交易业务,办理这些业务需要缴纳一定的税额。我们需要计算这些业务的最终费用(业务费用 + 税费)。
>>> txns = [1.09, 23.56, 57.84, 4.56, 6.78]
>>> TAX_RATE = .08
>>> def get_price_with_tax(txn):
... return txn * (1 + TAX_RATE)
...
>>> final_price = map(get_price_with_tax, txns)
>>> final_price
<map object at 0x0000021AF4B421C8>
>>> list(final_price)
[1.1772000000000002, 25.4448, 62.467200000000005, 4.9248, 7.322400000000001]
我们在 txns 中存储业务费用,并定义了一个计算单项业务最终费用的函数 get_price_with_tax,将这个函数和 txns 作为参数传递给 map() 函数。map() 的计算结果 final_price 是一个 map 对象,我们可以使用 list() 将其转换为 list 对象。
3,使用列表推导式
>>> squares = [i*i for i in range(10)]
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
new_list = [expression for member in iterable]
expression
expression 的结果就是新 list 中的元素。expression 可以是一个函数调用,或者其他任何合法的可返回一个值的表达式。
member
member 代表 iterable 中的每个对象或值。
iterable
iterable 可以是一个 list、集合(set)、序列(sequence)、生成器(generator)或其他任何每次访问就返回一个元素的对象。
>>> txns = [1.09, 23.56, 57.84, 4.56, 6.78]
>>> TAX_RATE = .08
>>> def get_price_with_tax(txn):
... return txn * (1 + TAX_RATE)
...
>>> final_prices = [get_price_with_tax(i) for i in txns]
>>> final_prices
[1.1772000000000002, 25.4448, 62.467200000000005, 4.9248, 7.322400000000001]
【功能丰富的列表推导式】
理解列表推导式的功能范围有助于理解其带来的全部价值。我们还将了解到 Python 3.8 中列表推导式的一些新变化。
1,使用条件逻辑
我们已经知道了使用列表推导式的基本语法:
new_list = [expression for member in iterable]
这个语法是正确但不完整的。更完整的语法格式支持可选的条件语句(conditional),常见的形式为将条件逻辑追加到上边格式的尾部。
new_list = [expression for member in iterable (if conditional)]
条件语句允许列表推导式选择性地保留符合要求的值,过滤掉那些不想要的值,这在很多时候是有用的。这和 filter() 提供的功能类似。
看个例子。
>>> sentence = 'the rocket came back from mars'
>>> vowels = [i for i in sentence if i in 'aeiou']
>>> vowels
['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
...
>>> consonants = [i for i in sentence if is_consonant(i)]
>>> consonants
['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']
这个稍复杂些的例子用于提取 sentence 中的小写辅音字母。我们将过滤函数 is_consonant() 放到了列表推导式的条件语句中,并为其传递了参数 i。
条件语句不仅能过滤掉 list 中的非法元素,还支持对这些元素进行修改,使其合乎要求。这时,我们需要将条件语句前置到列表推导式的 expression 之后。
new_list = [expression (if conditional) for member in iterable]
此语法格式允许我们从条件逻辑的多个分支中提取数据到 list 中。
比如,有一组数据,我们需要将其中的正数翻倍,将其中的负数取绝对值。
>>> original_datas = [1.25, -9.45, 10.22, 3.78, -5.92, 1.16]
>>> prices = [i*2 if i>0 else -i for i in original_datas]
>>> prices
[2.5, 9.45, 20.44, 7.56, 5.92, 2.32]
请注意:我们在语法格式中标注的 (if conditional) 指的是一个合法的条件判断语句,不仅仅只包含一条 if 语句。
2,使用 set 和 dict 推导式
set 推导式输出结果中不含重复元素,且不保证元素的输出顺序
set 推导式使用大括号 ({}) 来定义
>>> quote = "life, uh, finds a way"
>>> unique_vowels = {i for i in quote if i in 'aeiou'}
>>> unique_vowels
{'e', 'u', 'a', 'i'}
>>> squares = {i: i * i for i in range(10)}
>>> squares
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
while True:
data = f.read(1024)
if not data:
break
use(data)
while data := f.read(1024):
use(data)
>>> import random
>>> def get_weather_data():
... return random.randrange(90, 110)
>>> hot_temps = [temp for _ in range(20) if (temp := get_weather_data()) >= 100]
>>> hot_temps
[107, 102, 109, 104, 107, 109, 108, 101, 104]
【哪些场景不适合使用列表推导式】
列表推导式很有用,可以帮助我们编写易于阅读和调试的优雅代码,但它们不是所有情况下的正确选择。它们可能会使代码运行得更慢或占用更多内存。如果你的代码性能较差或较难理解,那么最好选择一个替代方案。
1,谨慎使用嵌套的推导式
推导式可以嵌套使用,从而创建一个混合着 list、dict 或 set 的集合。
比如,某个气候实验室正在跟踪 5 个城市六月份首周的最高气温。存储气温数据的数据结构可以是一个嵌套了列表推导式的字典推导式。
>>> cities = ['Austin', 'Tacoma', 'Topeka', 'Sacramento', 'Charlotte']
>>> temps = {city: [0 for _ in range(7)] for city in cities}
>>> temps
{
'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]
}
temps 是一个由字典推导式生成的外层数据集合,字典推导式的 expression 部分为一个 key-value 键值对,其中的 value 部分是一个列表推导式。
嵌套的 list 也是一种创建矩阵的常见方法。比如下边这段代码创建了一个 6 行 5 列的矩阵:
>>> matrix = [[i for i in range(5)] for _ in range(6)]
>>> matrix
[
[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]
]
外层的列表推导式用于生成矩阵的行,内层的列表推导式用于生成矩阵的列。
上边这两个例子中,嵌套推导式的用途很简单。而有些场景下使用嵌套的推导式会使你的代码看起来非常令人困惑。
比如,下边的代码用于扁平化一个矩阵:
>>> matrix = [
... [0, 0, 0],
... [1, 1, 1],
... [2, 2, 2],
... ]
>>> flat = [num for row in matrix for num in row]
>>> flat
[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 num in row:
... flat.append(num)
...
>>> flat
[0, 0, 0, 1, 1, 1, 2, 2, 2]
外层 for 循环读取每一行,内层 for 循环读取每行中的每个元素。
单行嵌套的列表推导式固然更具 Python 风格,但比起风格更重要的是,你应该编写可以让其他人容易理解和修改的代码。你需要在编码风格和代码可读性方面做好平衡。
2,对于大数据集,请使用生成器
Python 中的列表推导式会将整个输出结果加载到内存中。对于小的或中等规模的 list,这没多大问题。如果你想计算前 1000 个数的平方和,列表推导式可以轻松胜任。
>>> sum([i * i for i in range(1000)])
332833500
但假如你想计算 10 亿个整数的平方和呢?如果你尝试在你的计算机上以上边的方式来执行这个运算,你可能会发现你的电脑变得没反应了。这是因为,Python 正在尝试创建一个包含 10 亿个整数的 list,而这可能耗尽你电脑的内存。
当 list 的大小成为瓶颈时,我们通常使用生成器来替代列表推导式。生成器不会在内存中创建一个很大块的数据结构,它会返回一个 iterable。你可以使用代码从这个 iterable 中逐次取出下一个值,不限次数或者直到取完全部数据,而每次只需要存储一个值即可。
如果你打算使用生成器计算前 10 亿个整数的平方和,你的程序需要花一段时间来完成这个计算,但它不会导致你的电脑僵死。
下边是使用生成器的示例代码:
>>> sum(i * i for i in range(1000000000))
333333332833333333500000000
从代码中可以看出这是个生成器,因为表达式使用的是圆括号,而非方括号或大括号。
上边这段代码仍需要做很多工作,但它执行的是惰性操作。由于是惰性求值,生成器只在被显式请求时才会计算这些平方值。当生成器返回一个值(比如,567*567),它会将此值加到正在运行的 sum 上,然后丢弃这个值,接着生成下一个值(568*568)。当 sum 函数向生成器请求下一个值时,这个过程重新开始。而此过程仅使用很少的内存。
map() 同样执行惰性操作,这意味着你可以在此场景中使用 map() 而无需担心内存问题。
>>> sum(map(lambda i: i*i, range(1000000000)))
333333332833333333500000000
你可以自行决定使用生成器还是 map()。
3,性能分析方法
>>> import random
>>> import timeit
>>> TAX_RATE = .08
>>> 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
【结语】
在这篇文章中,我们了解了如何使用列表推导式来实现复杂的任务而不增加代码的复杂度。
现在你应该可以:
使用更具声明性的列表推导式来简化循环和 map() 调用
使用条件逻辑增强列表推导式
创建 set 和 dict 列表推导式
在代码可读性和程序性能间做出权衡,选择合适的方法
感谢阅读全文,您的关注、分享和“在看”是对我最大的鼓励!