推导式
前面学了控制流程,接下来瞅瞅 Python 中一种非常优雅的语法——推导式,它能用一行代码快速生成列表、字典、集合等数据。
1. 什么是 Python 推导式
什么是推导式。
推导式(comprehension)是 Python 提供的一种简洁语法,可以从一个可迭代对象中快速生成新的数据容器,比如列表、字典、集合等。其中列表是序列,字典和集合不是序列,但它们都可以用推导式快速生成。
用传统方式生成一个列表,通常需要写一个 for 循环。
# 传统方式:使用 for 循环生成 0 到 9 的平方数列表
square_list = []
for i in range(10):
square_list.append(i ** 2)
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(square_list)而使用列表推导式,只需要一行代码。
# 列表推导式
square_list = [i ** 2 for i in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(square_list)推导式的基本语法。
推导式通常由三部分组成:
- 表达式:用于生成新容器中的每个元素。
for循环:用于遍历原可迭代对象,也可以写多个for遍历多层数据。if条件(可选):用于过滤不符合条件的元素,也可以写多个if同时过滤。
[表达式 for 变量 in 可迭代对象 if 条件]if 放在最后时表示过滤条件,只有满足条件的元素才会被保留;if-else 放在表达式位置时,表示对每个元素做二选一处理。初学阶段先掌握一个 for 加一个 if 就够用,后面的例子会看到这两种写法。
四种推导式速览。
| 类型 | 符号 | 生成结果 | 特点 |
|---|---|---|---|
| 列表推导式 | [] | list | 有序、可重复、可修改 |
| 字典推导式 | {key: value} | dict | key 不可重复 |
| 集合推导式 | {} | set | 无序、自动去重 |
| 生成器表达式 | () | generator | 按需取值,节省内存 |
2. 列表推导式
列表推导式用于快速生成列表,是最常用的推导式。
2.1 基础语法
# 生成 1 到 5 的平方数列表
square_list = [i ** 2 for i in range(1, 6)]
# [1, 4, 9, 16, 25]
print(square_list)
# 等价于传统写法
square_list = []
for i in range(1, 6):
square_list.append(i ** 2)2.2 带条件过滤
# 过滤出 1 到 10 中的偶数
even_list = [i for i in range(1, 11) if i % 2 == 0]
# [2, 4, 6, 8, 10]
print(even_list)
# 过滤出长度大于 3 的字符串
name_list = ['itwxe', 'tom', 'jerry', 'wxe']
long_name_list = [name for name in name_list if len(name) > 3]
# ['itwxe', 'jerry']
print(long_name_list)
# 等价于传统写法
even_list = []
for i in range(1, 11):
if i % 2 == 0:
even_list.append(i)
long_name_list = []
for name in name_list:
if len(name) > 3:
long_name_list.append(name)2.3 多重循环
# 组合两个列表中的所有元素对(笛卡尔积)
list1 = ['a', 'b']
list2 = [1, 2]
result = [f'{x}{y}' for x in list1 for y in list2]
# ['a1', 'a2', 'b1', 'b2']
print(result)
# 等价于传统写法:两层 for 循环嵌套
result = []
for x in list1:
for y in list2:
result.append(f'{x}{y}')2.4 嵌套列表推导式
注意:2.3 是两个独立列表的所有组合(笛卡尔积),而展平是单个嵌套结构的内部展开,for 的书写顺序对应传统嵌套循环的顺序。
# 将二维列表展平为一维
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat_list = [num for row in matrix for num in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(flat_list)
# 等价于传统写法
flat_list = []
for row in matrix:
for num in row:
flat_list.append(num)2.5 带 if-else 的表达式
注意这里的 if-else 写在表达式位置,不是过滤条件,而是对每个元素做二选一处理。
# 将奇数保留,偶数替换为 0
num_list = [1, 2, 3, 4, 5]
result = [i if i % 2 == 1 else 0 for i in num_list]
# [1, 0, 3, 0, 5]
print(result)
if过滤 vsif-else表达式,两者很容易搞混。
| 写法 | 位置 | 作用 | 示例 |
|---|---|---|---|
for ... if | 末尾 | 过滤:不满足条件的丢弃 | [x for x in nums if x > 0] |
if-else 表达式 | 开头(for 左侧) | 映射:对每个元素二选一 | [x if x > 0 else 0 for x in nums] |
也就是说末尾的 if 只决定留不留这个元素;表达式位置的 if-else 则一个都不能少,每个元素都会保留,只是值可能被替换。
3. 字典推导式
字典推导式用于快速生成字典,语法和列表推导式类似,只是把 [] 换成 {},并且表达式需要是 key: value 形式。
3.1 基础语法
# 生成数字及其平方数的映射
square_dict = {i: i ** 2 for i in range(1, 6)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
print(square_dict)3.2 交换 key 和 value
user_dict = {'name': 'itwxe', 'age': 18}
reverse_dict = {value: key for key, value in user_dict.items()}
# {'itwxe': 'name', 18: 'age'}
print(reverse_dict)注意:交换后原来的 value 会变成 key,所以 value 必须是可以作为字典 key 的类型;如果有重复 value,后面的键值对会覆盖前面的。
3.3 带条件过滤
# 过滤出 value 大于 80 的键值对
score_dict = {'语文': 85, '数学': 92, '英语': 78}
high_score_dict = {key: value for key, value in score_dict.items() if value > 80}
# {'语文': 85, '数学': 92}
print(high_score_dict)3.4 从两个列表生成字典
keys = ['name', 'age', 'sex']
values = ['itwxe', 18, '男']
user_dict = {key: value for key, value in zip(keys, values)}
# {'name': 'itwxe', 'age': 18, 'sex': '男'}
print(user_dict)拓展:这里直接用 dict(zip(keys, values)) 也能得到同样结果。zip 把两个列表按位置一一配对,返回一个迭代器,如果两个列表长度不同,会以较短的为准(多余元素会被忽略)。初学阶段会用它就行。
4. 集合推导式
集合推导式和列表推导式类似,使用 {} 包裹,生成的是一个集合,会自动去重。集合是无序的,所以打印出来的顺序可能和示例注释不完全一样。
4.1 基础语法
# 生成 -5 到 4 的平方数集合
square_set = {i ** 2 for i in range(-5, 5)}
# 自动去重,相同平方数只保留一个
# {0, 1, 4, 9, 16, 25}
print(square_set)4.2 带条件过滤
# 从字符串中过滤出元音字母
msg = 'Hello World'
vowel_set = {char for char in msg if char in 'aeiouAEIOU'}
# {'e', 'o'}
print(vowel_set)5. 元组推导式
严格来说,Python 中并没有真正的「元组推导式」,使用 () 包裹的推导式生成的是生成器对象(generator),而不是元组。
5.1 生成器表达式
# 使用圆括号得到的是生成器对象
generator = (i ** 2 for i in range(5))
# <generator object <genexpr> at 0x...>
print(generator)
# <class 'generator'>
print(type(generator))5.2 转换为元组
# 通过 tuple() 函数转换为元组
generator = (i ** 2 for i in range(5))
tuple_result = tuple(generator)
# (0, 1, 4, 9, 16)
print(tuple_result)
# <class 'tuple'>
print(type(tuple_result))实际写代码时,也常直接写成 tuple(i ** 2 for i in range(5)),效果是一样的。
5.3 生成器的优势
生成器不会一次性把所有结果都生成并保存在内存中,而是按需生成,适合处理大数据量场景。
# 列表推导式会一次性生成所有元素
square_list = [i ** 2 for i in range(1000000)]
# 生成器表达式按需生成,更省内存
square_generator = (i ** 2 for i in range(1000000))5.4 使用 next() 逐个取值
生成器可以通过 next() 逐个消费元素,每次调用只产生下一个值。
gen = (i ** 2 for i in range(3))
# 0
print(next(gen))
# 1
print(next(gen))
# 4
print(next(gen))
# StopIteration 异常表示生成器已被耗尽
# print(next(gen))注意:生成器只能遍历一次,遍历完后不能再回头取值,这和列表不同。
5.5 生成器函数
生成器表达式本质上是一种简化的生成器。更灵活的方式是用 yield 关键字定义生成器函数,这在后续面向对象的文章会详细展开,先看一眼长什么样就行。
# 生成器表达式
gen = (i ** 2 for i in range(3))
# 等价的生成器函数
def gen_squares(n):
for i in range(n):
yield i ** 2
gen = gen_squares(3)
# [0, 1, 4]
print(list(gen))6. 推导式练习
最后来几个小练习巩固一下。
6.1 过滤出列表中的正数
num_list = [-3, -2, -1, 0, 1, 2, 3]
positive_list = [i for i in num_list if i > 0]
# [1, 2, 3]
print(positive_list)6.2 统计字符串中每个字符出现的次数
from collections import Counter
msg = 'itwxe'
char_count = Counter(msg)
# Counter({'i': 1, 't': 1, 'w': 1, 'x': 1, 'e': 1})
print(char_count)拓展:手动写也可以,不过 {char: msg.count(char) for char in msg} 存在性能问题——count() 每次都要遍历整个字符串,时间复杂度 O(n²)。短字符串没问题,长的还是推荐 Counter。
6.3 求两个列表的交集
list1 = [1, 2, 3, 4]
list2 = [3, 4, 5, 6]
common_set = {i for i in list1 if i in list2}
# {3, 4}
print(common_set)拓展:交集也可以直接写 set(list1) & set(list2),结果同样是 {3, 4};用推导式更容易看出筛选过程。
6.4 用字典推导式反转并过滤
# 将分数大于 80 的科目反转,key 变 value,value 变 key
score_dict = {'语文': 85, '数学': 92, '英语': 78}
reversed_dict = {value: key for key, value in score_dict.items() if value > 80}
# {85: '语文', 92: '数学'}
print(reversed_dict)6.5 用集合推导式提取唯一单词
sentence = 'hello world hello python world'
unique_words = {word for word in sentence.split()}
# {'hello', 'world', 'python'}
print(unique_words)推导式虽然简洁,但也不要过度嵌套,否则可读性会变差。初学阶段优先保证自己和别人能看懂,再追求一行写完。多写多练,找到简洁和可读性的平衡点~