推导式:列表推导、字典推导、集合推导

一句话概述

推导式(Comprehension)是 Python 最具特色的语法之一,它让你用一行代码就能从已有的可迭代对象中快速构建出列表、字典或集合。相比传统的 for 循环写法,推导式更简洁、执行速度更快、可读性更高。在 AI 开发中,推导式广泛用于数据预处理、特征变换、批量标签转换等高频操作。

💡 核心要点:① 列表推导式 [expr for x in iterable] 一行生成新列表 ② 带条件的推导式用 if 过滤元素 ③ 字典推导式 {k: v for ...} 快速构建映射表 ④ 集合推导式 {expr for ...} 自动去重

教学与演示

一、列表推导式:一行生成列表

是什么:列表推导式的基本语法是 [表达式 for 变量 in 可迭代对象]——它遍历可迭代对象中的每个元素,对每个元素执行表达式计算,将结果收集到一个新列表中并返回。本质上是 for 循环 + append() 的浓缩版。

大白话 就像批量生产——你有一个零件清单(可迭代对象),工人(for 循环)一个一个取零件,经过加工(表达式)后放入成品箱(新列表)。传统写法需要三步(建箱、加工、装箱),推导式一步到位。

为什么:AI 数据处理中频繁需要在列表之间做映射转换——所有像素值除以 255 归一化、所有标签转为 one-hot 编码、所有样本减去均值做标准化。用传统 for 循环写 3-5 行,推导式一行搞定,而且 Python 解释器对推导式有专门优化,执行速度通常比等价 for 循环快 20%~50%。

怎么做

import numpy as np

# 模拟 AI 数据预处理:将像素值从 [0, 255] 归一化到 [0, 1]
# 生成 8 个模拟的原始像素值
np.random.seed(10)
pixels = np.random.randint(0, 256, size=8)  # 8个像素值,范围[0, 255]
print(f"原始像素值:{pixels}")

# 传统 for 循环写法(3行)
normalized_old = []  # 第一步:建空列表
for p in pixels:  # 第二步:遍历
    normalized_old.append(p / 255.0)  # 第三步:计算并添加

# 列表推导式写法(1行!)
normalized = [p / 255.0 for p in pixels]  # 一行完成同样的逻辑
# 语法解读:[对每个元素做什么 for 元素 in 来源]

print(f"传统写法结果:{[round(x, 4) for x in normalized_old]}")
print(f"推导式结果  :{[round(x, 4) for x in normalized]}")

# 更复杂的变换:模拟对数据做标准化(减均值除以标准差)
print(f"\n--- 数据标准化(Z-score)---")
values = np.random.randint(10, 100, size=6)
print(f"原始值:{values}")
mean_val = np.mean(values)  # 计算均值
std_val = np.std(values)    # 计算标准差
# 列表推导式做标准化
standardized = [(v - mean_val) / std_val for v in values]
print(f"标准化后:{[round(x, 4) for x in standardized]}")

什么用:在 AI 图像处理中,[img/255.0 for img in batch] 批量归一化像素值。在 NLP 中,[len(text) for text in corpus] 快速统计所有文本长度。在特征工程中,[np.log(f) for f in features] 对偏态分布特征做对数变换。PyTorch 中 [tensor.to(device) for tensor in batch] 批量将张量转移到 GPU。

二、带条件的列表推导式

是什么:在基本列表推导式的末尾加上 if 条件,可以过滤掉不满足条件的元素。语法为 [表达式 for 变量 in 可迭代对象 if 条件]——只有满足 if 条件的元素才会参与表达式计算并被收集到结果列表中。

大白话 就像水果分拣线——传送带上各种水果(可迭代对象)经过,质检员只看苹果(if 条件),把苹果挑出来装盒(表达式),橘子直接放过去不要。

为什么:数据处理中经常需要「先过滤再变换」——只保留置信度 > 0.9 的预测结果、只取长度 > 3 的有效文本、只保留非零特征值。用传统写法需要嵌套 if + append,推导式的 if 后缀让过滤逻辑一目了然。

怎么做

import numpy as np

# 模拟 AI 模型推理结果过滤:只保留高置信度的预测
np.random.seed(7)
# 10个样本的预测概率(模拟 softmax 输出各类别概率)
probabilities = np.random.uniform(0.1, 1.0, size=10)
probabilities = np.round(probabilities, 4)
print(f"全部预测概率:{probabilities}")

# 带条件的列表推导式:只保留概率 > 0.7 的高置信度预测
# 语法:[表达式 for 变量 in 来源 if 条件]
high_conf = [p for p in probabilities if p > 0.7]
print(f"高置信度(>0.7):{high_conf}")
print(f"高置信度占比:{len(high_conf)}/{len(probabilities)}")

# 进阶:过滤 + 变换同时进行
# 只保留 > 0.5 的预测,并将其转为百分比字符串
print(f"\n--- 过滤 + 格式转换 ---")
results = [f"{p*100:.1f}%" for p in probabilities if p > 0.5]
print(f"转换为百分比(>0.5):{results}")

# 模拟数据清洗:列出所有超出合理范围的特征值
print(f"\n--- 异常值检测 ---")
features = np.array([1.2, 3.5, 99.7, 2.1, -50.3, 4.2, 0.0, 200.1])
print(f"原始特征:{features}")
# 找出异常值(<0 或 >50 认为异常)
anomalies = [f for f in features if f < 0 or f > 50]
print(f"异常值(<0 或 >50):{anomalies}")

什么用:在 AI 模型输出后处理中,[pred for pred in predictions if pred.confidence > 0.9] 过滤低置信度结果。在数据清洗中,[x for x in data if not np.isnan(x)] 剔除缺失值。在特征选择中,[f for f in features if np.var(f) > 0.01] 过滤方差过低的无效特征。在 NLP 停用词过滤中,[w for w in words if w not in stop_words] 去除停用词。

三、字典推导式:快速建表

是什么:字典推导式的语法为 {键表达式: 值表达式 for 变量 in 可迭代对象 if 条件}——遍历过程中为每个元素生成一个键值对,最终汇聚成一个字典。if 条件 部分可选,用于过滤。

大白话 就像自动编号机——给你一叠文件(可迭代对象),机器自动给每份文件打上编号(键)和分类标签(值),最后产出一本目录册(字典)。

为什么:AI 开发中经常需要快速构建映射关系——类别名到索引的映射、特征名到特征值的映射、样本 ID 到预测结果的映射。字典推导式是构建这类映射表的最快方式。

怎么做

import numpy as np

# 场景1:构建类别名到索引的映射表(AI分类任务必需)
class_names = ["猫", "狗", "鸟", "鱼", "兔"]  # 分类任务中的类别名
# 字典推导式:{类别名: 索引}
class_to_idx = {name: idx for idx, name in enumerate(class_names)}
print("类别 → 索引映射表:")
print(class_to_idx)
# 反向映射:索引导类别名
idx_to_class = {idx: name for name, idx in class_to_idx.items()}
print(f"\n索引 → 类别映射表:")
print(idx_to_class)

# 场景2:快速统计元素频次(模拟词频统计)
print(f"\n--- 模拟词汇频次统计 ---")
words = ["AI", "Python", "AI", "深度学习", "Python", "AI", "数据"]
# 字典推导式 + 列表的 count 方法
word_freq = {word: words.count(word) for word in set(words)}
print("词频统计:", word_freq)

# 场景3:带条件的字典推导式
# 只保留频次 > 1 的高频词
high_freq = {word: count for word, count in word_freq.items() if count > 1}
print("高频词(出现>1次):", high_freq)

# 场景4:构建特征名到随机权重的映射(模拟神经网络参数初始化)
print(f"\n--- 特征权重初始化 ---")
feature_names = ["亮度", "对比度", "饱和度", "锐度", "色温"]
np.random.seed(3)
# 每个特征随机初始化一个权重(模拟神经网络第一层)
weights = {name: round(np.random.uniform(-0.5, 0.5), 4) for name in feature_names}
print("特征权重表:", weights)

什么用:在 AI 分类任务中,{label: idx for idx, label in enumerate(classes)} 构建标签映射是每个项目的第一步。在 NLP 中,{word: vec for word, vec in zip(vocab, embeddings)} 构建词到向量的查找表。在模型配置中,{name: value for name, value in config if value is not None} 过滤空配置项。在结果汇总中,{sample_id: pred for sample_id, pred in zip(ids, predictions)} 构建 ID 到预测的映射。

四、集合推导式:自动去重的列表

是什么:集合推导式的语法为 {表达式 for 变量 in 可迭代对象 if 条件}(注意用花括号 {} 但花括号内只有单个表达式,没有冒号)——遍历并计算每个元素,结果自动去重后放入一个集合。

大白话 就像投票箱——大家往里面投选票(for 循环处理每个元素),但自动把重复的票合并掉了(集合去重特性)。最终你看到的是一组不重复的结果。

为什么:在 AI 数据处理中,经常需要获取唯一值——数据集中有哪些类别、语料库中有哪些不重复的词汇、特征有哪些不同的取值。用列表推导式还要再套 set(),而集合推导式一步到位。

怎么做

import numpy as np

# 场景1:快速获取数据中的唯一类别(去重)
# 模拟分类模型的一批预测结果(含重复类别)
predictions = ["猫", "狗", "猫", "鸟", "狗", "猫", "鱼", "鸟", "狗"]
# 集合推导式:自动去重,得到所有不重复的预测类别
unique_classes = {pred for pred in predictions}  # 花括号 + 单个表达式 = 集合推导式
print("所有预测中出现过的类别:", unique_classes)
print(f"类别数:{len(unique_classes)}(原列表{len(predictions)}条)")

# 场景2:提取数据集中所有不重复的特征值
print(f"\n--- 获取不重复的特征值 ---")
np.random.seed(5)
# 模拟 20 个样本的离散特征取值(如学历等级)
levels = np.random.choice(["小学", "初中", "高中", "本科", "硕士", "博士"], size=20)
print(f"全部样本特征:{levels}")
# 集合推导式获取所有不重复的取值
unique_levels = {lv for lv in levels}
print(f"不重复的特征值:{sorted(unique_levels)}")

# 场景3:带条件的集合推导式
# 提取所有长度 > 1 的标签
print(f"\n--- 带过滤的集合推导式 ---")
tags = ["AI", "ML", "AI", "Deep", "ML", "CV", "NLP", "CV", "DL"]
# 只保留长度 > 2 的标签,且去重
long_tags = {t for t in tags if len(t) > 2}
print("原始标签:", tags)
print("长度>2的不重复标签:", long_tags)

# 场景4:列表推导式 vs 集合推导式的对比
print(f"\n--- 对比:列表 vs 集合推导式 ---")
data = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]  # 含大量重复
list_result = [x * x for x in data]       # 列表推导式:保留所有元素
set_result = {x * x for x in data}        # 集合推导式:自动去重
print(f"列表推导式(保留重复):{list_result}")
print(f"集合推导式(自动去重):{set_result}")

什么用:在 AI 数据探索(EDA)中,{label for label in df['category']} 快速了解数据集有哪些类别。在 NLP 构建词汇表时,{word for doc in corpus for word in doc} 收集语料库中所有不重复词汇。在异常检测中,{ip for ip in logs if is_suspicious(ip)} 收集所有可疑 IP 并自动去重。在特征工程中,快速了解一个离散特征有哪些可能的取值。

概念关系图谱

概念核心含义与AI的关系关联概念
列表推导式[expr for x in seq] 一行生成列表批量数据变换、归一化、特征工程for 循环、append
字典推导式{k: v for ...} 快速构建映射表标签映射、词向量查找表、配置管理dict、item
集合推导式{expr for ...} 自动去重获取唯一类别、词汇表构建set、去重
条件过滤if 后缀过滤不满足条件的元素数据清洗、置信度筛选、异常检测if、filter
嵌套推导式推导式中包含另一个推导式二维数据展开、特征交叉嵌套循环
生成器表达式(expr for x in seq) 惰性求值大数据集流式处理、节省内存推导式、yield

重点答疑

Q1: 列表推导式和 for 循环 + append 有什么本质区别?

语法层面:推导式是一行表达式,for+append 是多行语句。性能层面:推导式在 CPython 中以 C 语言级别执行,避免了每次迭代查找 append 方法、压栈出栈的开销,通常快 20%~50%。可读性层面:对于简单的「映射+收集」操作,推导式更清晰;但如果循环体内逻辑复杂(多步操作、副作用、异常处理),for 循环更合适。原则是「简单映射用推导式,复杂逻辑用 for」。

Q2: 什么时候用集合推导式而不是列表推导式再转 set()?

两者的结果等价,性能也差别不大。集合推导式的优势在于语义更直接——你的意图就是「获取不重复的集合」,一行代码就能表达,读到的人也一眼明白。set([x for x in data if cond]) 先建完整列表再转集合,中间列表会临时占用内存;而 {x for x in data if cond} 直接构建集合,内存上略优。当数据量很大时,集合推导式略好。

Q3: 推导式可以嵌套吗?会不会降低可读性?

语法上可以嵌套,比如 [y for x in matrix for y in x] 展开二维矩阵。但嵌套推导式很容易变得难以阅读,尤其是在涉及多个 for 和 if 时。PEP 8 建议:如果推导式超过一行(约 79 个字符),应该拆分为 for 循环。一个折中:单层推导式大胆用,两层嵌套视情况,三层及以上必须用 for 循环。代码是写给人看的,推导式的首要目的是提升可读性,而非炫技。

章节单词汇总

英文音标术语/释义
comprehension/ˌkɑːmprɪˈhenʃən/推导式;Python 中快速构建集合的语法
derive/dɪˈraɪv/推导;从一个序列变换出另一个序列
filter/ˈfɪltər/过滤;用 if 条件筛掉不需要的元素
map/mæp/映射;将函数或表达式应用于每个元素
flatten/ˈflætn/展平;将嵌套结构变为一维序列
concise/kənˈsaɪs/简洁的;推导式的核心优势
generator/ˈdʒenəreɪtər/生成器;惰性求值的推导式变体
duplicate/ˈduːplɪkeɪt/重复;集合推导式自动去除重复元素

面试练习

Q1 [单选] [x*2 for x in range(5)] 的结果是?

  • A. [0, 2, 4, 6, 8]
  • B. [0, 1, 4, 9, 16]
  • C. [2, 4, 6, 8, 10]
  • D. [0, 2, 4, 6, 8, 10]
解答:range(5) 产生 0、1、2、3、4,每个乘以 2 得到 0、2、4、6、8。注意表达式是 x*2 不是 x**2(平方)。

Q2 [单选] [x for x in range(10) if x % 2 == 0] 的结果是?

  • A. [0, 2, 4, 6, 8]
  • B. [1, 3, 5, 7, 9]
  • C. [2, 4, 6, 8, 10]
  • D. [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
解答:if x % 2 == 0 过滤出偶数。range(10) 中偶数为 0、2、4、6、8(注意 0 是偶数)。

Q3 [单选] 字典推导式 {x: x**2 for x in range(3)} 的结果是?

  • A. {0: 0, 1: 1, 2: 4, 3: 9}
  • B. {0: 0, 1: 1, 2: 4}
  • C. {1: 1, 2: 4, 3: 9}
  • D. [0, 1, 4]
解答:range(3) 产生 0、1、2,键为 x 本身,值为 x 的平方。所以是 {0:0, 1:1, 2:4}。注意结果是字典(花括号+冒号),不是列表。

Q4 [单选] 以下集合字面量 {1, 2, 1, 3, 2, 1} 的结果是?

  • A. [1, 2, 3]
  • B. {1, 2, 3}
  • C. {1, 1, 2, 2, 3}
  • D. 报错
解答:集合字面量用花括号包裹、逗号分隔元素。Python 会自动去重,所以 {1, 2, 1, 3, 2, 1} 等价于 {1, 2, 3}。注意:{[1, 2, 3]} 才会报错——因为列表不可哈希,不能放入集合。

Q5 [多选] 关于列表推导式,以下哪些说法是正确的?

  • A. 可以包含一个或多个 for 子句
  • B. 可以包含一个或多个 if 子句
  • C. 执行速度通常快于等价的 for 循环
  • D. 推导式中可以使用 pass 语句
解答:A 正确,[x+y for x in a for y in b] 是合法的嵌套。B 正确,[x for x in data if x > 0 if x < 100] 多个 if 相当于 and。C 正确,CPython 对推导式有专门优化。D 错误,推导式是表达式,不能包含 pass 语句。

Q6 [单选] 以下代码输出什么?{x for x in 'hello world' if x != ' '}

  • A. {'h', 'e', 'l', 'o', 'w', 'r', 'd'}
  • B. {'h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd'}
  • C. {'h', 'e', 'l', 'o', 'w', 'r', 'd'}
  • D. ['h', 'e', 'l', 'o', 'w', 'r', 'd']
解答:集合推导式遍历 'hello world' 的每个字符,if x != ' ' 过滤掉空格。遍历到的字符为 h、e、l、l、o、w、o、r、l、d(共 10 个非空格字符),集合自动去重后得到 7 个不重复字符:h、e、l、o、w、r、d。

Q7 [单选] {x: len(x) for x in ['cat', 'dog', 'bird']} 的结果是?

  • A. {'cat': 3, 'dog': 3, 'bird': 4}
  • B. {0: 3, 1: 3, 2: 4}
  • C. ['cat', 'dog', 'bird']
  • D. {3, 3, 4}
解答:x 作为键,len(x) 作为值。'cat' 长度 3、'dog' 长度 3、'bird' 长度 4。注意字典推导式用冒号分隔键和值。

Q8 [多选] 以下哪些是合法的推导式写法?

  • A. [i+j for i in [1,2] for j in [10,20]]
  • B. {k: v for k, v in [('a',1), ('b',2)]}
  • C. {x for x in range(10) if x > 5}
  • D. {x: x**2 for x in range(5) for x in range(3)}
解答:A(列表推导式嵌套 for,笛卡尔积)、B(字典推导式元组解包)、C(集合推导式带条件)都合法。D 不合法——同一个循环变量 x 在嵌套 for 中被重复使用,语法上虽然可以,但逻辑混乱,实际上会覆盖并只使用内层 x 的值。

Q9 [单选] [x for x in range(20) if x % 3 == 0] 包含几个元素?

  • A. 6
  • B. 7
  • C. 5
  • D. 10
解答:0 到 19 中被 3 整除的有:0、3、6、9、12、15、18,共 7 个。注意 0 也是 3 的倍数(0/3=0 余 0),所以包含 0。

Q10 [单选] 与 [x*2 for x in range(5) if x > 2] 等价的 for 循环是?

  • A. for x in range(5): if x > 2: result.append(x*2)
  • B. 需要先创建空列表再在循环中 append
  • C. 直接用 range(5) 数组乘以 2
  • D. 用 while 循环逐个处理
解答:推导式 [x*2 for x in range(5) if x > 2] 的等价写法为:result = []; for x in range(5): if x > 2: result.append(x*2)。A 的写法缺少 result = [] 初始化,实际运行会报 NameError。所以最准确的答案是 B——需要先创建空列表。