Python数据结构与推导式:从构建到转化的高效编程路径
Python新手最容易产生的一个错觉就是把数据结构单纯当作“装东西的容器”把推导式当成“一种高级循环写法”。但真正跑过项目之后你会发现数据结构决定了你写代码的思考方式而推导式决定了你把一种结构转成另一种结构的速度。从列表到字典从字典到集合从集合到生成器如果全程只靠 for 循环和临时变量代码会越写越乱而一旦你掌握了推导式的底层逻辑很多所谓“复杂的数据处理”其实就是一行表达式的事。这篇文章会从构建和转化两条线展开先带你重新理解 Python 里最常用的列表、元组、字典、集合到底应该怎么选、怎么建、怎么互相转再把列表推导式、字典推导式、集合推导式和生成器表达式逐个拆开揉碎配合爬虫清洗、数据分析、量化交易里的真实场景告诉你哪些地方用推导式能极大提效哪些地方千万不能用。无论你是刚学 Python 的入门者还是已经在写业务脚本但总觉得代码不够利索的开发者这篇文章都值得花二十分钟刷一遍。1. 为什么说数据结构和推导式是Python的“任督二脉”1.1 数据结构是程序的骨架很多人学数据结构时第一反应是“这是算法课的考点和日常写脚本有什么关系”。我早期也有这种想法直到被一段维护成本极高的代码折磨之后才明白数据结构选的合不合理直接决定了代码能不能继续往下写。你可以把数据结构想象成不同功能的收纳工具。列表是一排开放的抽屉适合按顺序存放同类物品比如用户上传的文件路径、待处理的日志行元组是封好口的抽屉一旦放进去就不能改适合存坐标、RGB颜色、数据库查询返回的单行记录字典是带标签的档案柜每个标签对应一个值查起来快得惊人适合存用户信息、配置项、词频统计集合则是一张自动去重的核查表只关心“有没有”适合做交集、差集、白名单过滤。从算法复杂度的角度看列表和字典的差距非常明显。列表查找一个元素最坏情况下要遍历整个列表时间复杂度是 O(n)字典按键查找底层是哈希表平均复杂度是 O(1)。也就是说一万条数据时你可能感觉不到差异但到了百万、千万级别用列表硬找和用字典直接查差距可能就是几分钟和几毫秒的差别。这就是为什么所有数据结构相关教程都在强调“选型优先”。构建数据结构时很多新手习惯先建一个空列表然后 append建一个空字典然后赋值 key这当然没错。但只看这一步就够了吗远远不够。真正的核心能力是“根据业务需要用最合适的方式把数据组织起来”。比如统计一段文本中每个单词出现的次数你会想到用字典但如果你先学的是 C 语言习惯了数组可能第一反应是维护两个平行列表一个存单词、一个存次数这样不仅代码丑陋查找和更新也都很麻烦。Python 里defaultdict、Counter这些工具就是建立在字典之上的高级构建方式它们让“构建数据结构”这件事从手动挡变成了自动挡。1.2 推导式从“怎么建”到“怎么转”的高效表达理解了数据结构的骨架地位下一个问题就是如何快速构建和转化这些结构。推导式就是 Python 给我们的答案。举个例子给你一个数字列表想挑出所有偶数并乘以 2得到一个平方后的新列表。用传统循环你至少要写三行nums [1, 2, 3, 4, 5, 6] result [] for n in nums: if n % 2 0: result.append(n * 2)用列表推导式一行就完了result [n * 2 for n in nums if n % 2 0]这看起来只是语法糖但它背后代表了一种思维转变你不再关心“如何一步步填充列表”的过程而是直接声明“我要从 nums 里取出满足条件的元素映射成新值构成一个新列表”。这个过程本质上就是数据结构的构建和转化。为什么说推导式是“任督二脉”因为在实际业务里程序的大部分工作都是在处理数据从接口拿到 JSON 列表要转成字典方便查询从数据库读出一堆行要转成集合做去重从日志里提取关键字段要生成新的列表。这些操作如果用循环临时变量每个都要写好几行还得小心维护中间状态。而推导式把“遍历—筛选—映射—收集”四件事压缩成一个表达式代码量少出错概率也低读起来像自然语言。当然推导式不是银弹。它适合“纯净的映射和过滤”不适合带副作用或逻辑特别复杂的场景。这个后面我专门用一个章节讲清楚。1.3 适用人群与学习路线这篇文章的内容适合下面几类人第一刚入门 Python已经会写基本语法但刷题或写脚本时总觉得代码冗长想找到更高级的写法。第二做数据分析、爬虫、自动化脚本的工程师日常工作大量涉及数据结构转换想把代码写得既快又稳。第三准备面试或复习数据结构与算法的同学想系统地理解 Python 内置数据结构的底层能力和推导式的适用边界。学习路线我建议这样走先不看推导式把列表、元组、字典、集合的构建和常用方法练熟然后尝试用循环写数据转化理解每一步在干什么最后再用推导式改写体会表达方式的差异。这个顺序看起来很慢但能帮你真正建立“构建与转化”的直觉而不是只会复制粘贴语法。另外提一句环境准备热词里很多人搜 Python 安装、VSCode 环境配置建议直接装 Python 3.8 以上的版本用 VSCode 加 Python 插件或者 PyCharm 社区版都行。推导式相关的特性在 Python 3 里已经非常稳定不需要额外的包只要解释器版本别太老就可以。2. 动手构建Python核心数据结构2.1 列表与元组有序数据的构建和选择列表是 Python 里最灵活的数据结构可以存任意类型可以增删改查。它的构建方式主要有四种直接用方括号字面量、用list()转换、用.append()逐个添加、用列表推导式批量生成后面单独讲。# 字面量 fruits [apple, banana, cherry] # list() 转换 chars list(hello) # [h, e, l, l, o] # 先空后加 numbers [] numbers.append(10) numbers.append(20) # 批量生成列表推导式 squares [x**2 for x in range(10)]这里有一个很多人踩过的坑如果你想创建一个包含三个空列表的二维结构写出matrix [[]] * 3得到的三个列表其实是同一个对象的引用。你修改其中一个其他两个也会跟着变。我调试的时候遇到过这种诡异问题最后发现是*操作符复制的是引用而不是内容。正确的做法是matrix [[] for _ in range(3)]元组和列表最大的区别是不可变。很多人觉得元组只是“不能修改的列表”但其实它的真正价值在于“结构固定”。比如函数的多个返回值、一组 GPS 坐标、数据库一行记录用元组天然安全因为你不小心修改它时会立刻报错能把很多潜在 bug 挡在运行前。元组构建方式同样多样point (3, 5) rgb tuple([255, 128, 0])列表和元组之间可以互转list(tuple_obj)、tuple(list_obj)。这种转化在需要“临时修改”的时候特别有用。比如你拿到一个元组里的配置想在运行时动态调整其中一项就可以先转成列表改完再转回元组保持接口的不可变约定。2.2 字典与集合键值映射与唯一性的实现字典是 Python 里使用频率最高的映射结构。构建方式多得让人眼花缭乱# 字面量 user {name: 张三, age: 28} # dict() 关键字 user dict(name张三, age28) # fromkeys批量构建含默认值的字典 keys [apple, banana] default_dict dict.fromkeys(keys, 0) # {apple: 0, banana: 0} # zip 两个列表合成字典 names [张三, 李四] ages [28, 25] user_map dict(zip(names, ages))zip把两个序列对应位置打包成一组组元组再用dict()转成字典这本身就是一种非常经典的“构建与转化”技巧。做数据分析时经常遇到两个平行列表一个放字段名一个放字段值用这招合并成字典比手动循环干净得多。集合的构建有一个容易忽视的坑空集合不能用{}必须用set()因为{}是空字典。集合的字面量需要包含元素比如{1, 2, 3}。它和字典共用花括号语法所以学的时候要有意识地分清。# 集合构建 unique_nums {1, 2, 2, 3, 3, 3} # {1, 2, 3} # 从列表去重 unique_names set([张三, 李四, 张三]) # 空集合 empty_set set()集合的核心特性是元素唯一且可哈希。它天生适合去重、成员判断、集合运算。比如判断某个用户 ID 是否在 VIP 名单里用集合比用列表快得多。2.3 数据结构之间的互相转化构建是第一步转化才是真正拉开差距的地方。实际业务里数据很少一开始就是你要的形态。最常见的转化场景列表去重并保留原顺序。直接list(set(lst))会丢失顺序因为集合是无序的。要保持顺序可以用dict.fromkeys(lst)lst [3, 1, 2, 3, 1] unique_ordered list(dict.fromkeys(lst)) # [3, 1, 2]dict.fromkeys会保留插入顺序而且键唯一所以这个技巧在很多 Python 3.6 环境里都能稳定工作。列表转字典。如果列表本身是成对的元组直接用dict(lst)。如果两个单独列表用zip。pairs [(name, 张三), (age, 28)] user_dict dict(pairs) keys [name, age] values [张三, 28] user_dict2 dict(zip(keys, values))字典转列表。有时候我们只想取字典的键、值或全部键值对。user {name: 张三, age: 28} list_of_items list(user.items()) # [(name, 张三), (age, 28)] list_of_keys list(user.keys()) # [name, age] list_of_values list(user.values()) # [张三, 28]字典的键视图和值视图也可以直接传给集合构造函数用于集合运算。set(user.keys()) set(user.values())转化过程中有一类隐蔽问题字典的键必须是可哈希的所以列表、字典这类可变结构不能直接作为键或集合元素。如果你试图把一个列表放进集合解释器会报TypeError: unhashable type: list。遇到这种情况可以考虑把列表转成元组再存。这个细节在写缓存、状态标记、复杂键查询时特别重要。3. 推导式完全拆解语法、逻辑与性能3.1 列表推导式从循环到表达式的思维转换列表推导式的基本语法是[expression for item in iterable if condition]它等价于下面的循环result [] for item in iterable: if condition: result.append(expression)理解执行顺序很关键先执行后面的for再执行前面的expression中间可选的if用来过滤。你可以把expression想象成“对每个满足条件的 item 做映射”。我在教别人时常用一个类比列表推导式像一条流水线元素从一端进来经过传送带上的过滤和加工从另一端出来变成新列表。你不需要手动开一个空箱子也不需要手动把加工好的产品放进去流水线自动完成了装箱。实际应用时条件过滤经常是多条件的nums [10, 21, 32, 43, 54, 65] res [n // 2 for n in nums if n 30 and n % 2 0]注意and不能写成两个if连放的错误理解其实多个if也是等价的res [n // 2 for n in nums if n 30 if n % 2 0]两者效果相同但可读性上更推荐用and合并成一个条件。列表推导式还有一个隐藏优势它会优先在底层用 C 循环执行比手动 for 循环的 Python 解释器逐行执行要快一些。当然这个快不是数量级差距但当你处理几十万元素时确实能感受到流畅度的提升。3.2 字典推导式与集合推导式一个表达式搞定映射与去重字典推导式的语法和列表推导式类似只是用花括号并且表达式部分是键: 值{key_expression: value_expression for item in iterable}非常典型的例子是把一个字典的键和值反转original {a: 1, b: 2, c: 3} reversed_dict {v: k for k, v in original.items()} # {1: a, 2: b, 3: c}这个操作在查询映射关系反转时极其方便。比如你原本用城市名查邮编现在需要根据邮编查城市反转一下就是新字典。字典推导式还可以从一个列表批量构建带计算值的字典。比如统计词频words [apple, banana, apple, cherry] word_count {word: words.count(word) for word in set(words)}不过这里有个性能隐患words.count(word)对每个词都会扫描整个列表整体是 O(n^2)。如果数据量大更推荐用Counter。我们在讲推导式的“适用边界”时也要记得它并不是所有场景的最优解。集合推导式语法类似但生成的是去重后的集合nums [1, 2, 2, 3, 4, 4, 5] squared_set {x**2 for x in nums} # {1, 4, 9, 16, 25}它特别适合把列表转成集合进行去重或集合运算同时还允许你在转化过程中做一些映射。比如从一堆文件名中提取扩展名files [a.py, b.txt, c.py, d.jpg] extensions {f.split(.)[-1] for f in files} # {py, txt, jpg}注意集合推导式的结果不保证顺序所以如果后续需要稳定顺序请再转成列表排序。3.3 生成器表达式当数据量大到内存扛不住时生成器表达式和列表推导式非常相似区别只是把方括号换成圆括号gen (x**2 for x in range(10))它不会立刻生成全部元素而是在你迭代时逐个产出这就是“惰性求值”。处理超大文件或无限数据流时列表推导式可能直接吃光内存而生成器表达式只占用几乎固定的内存。我举个例子假设你有一个 1GB 的日志文件需要求所有行数字之和。直接用列表推导式把所有行转成数字再求和会一次性构建一个百万级元素的列表内存占用巨大用生成器表达式配合sum()每一行只被处理一次内存几乎不变total sum(int(line.strip()) for line in open(huge.log) if line.strip().isdigit())注意生成器表达式外面必须用sum()这样的函数消费它否则它自己不会主动执行。生成器表达式的一个限制是它只能被迭代一次。你第一次用for遍历它第二次再遍历时会得到空结果。这个话题经常有新手踩坑。我的建议是如果你需要多次遍历相同的数据那就老老实实用列表如果只是做一次性的统计、筛选、传参生成器表达式更合适。3.4 推导式中的条件过滤与嵌套循环推导式同样支持嵌套循环。语法顺序和普通循环是一致的先写外层 for再写内层 for[(x, y) for x in range(3) for y in range(3)] # [(0,0), (0,1), (0,2), (1,0), ...]它等价于result [] for x in range(3): for y in range(3): result.append((x, y))嵌套推导式最常见的用途是展平二维列表matrix [[1, 2], [3, 4], [5, 6]] flat [num for row in matrix for num in row] # [1, 2, 3, 4, 5, 6]这里容易搞混顺序for row in matrix是外层for num in row是内层。写的时候记住“左读顺序就是循环嵌套顺序”。我个人的经验是嵌套超过两层就不建议用推导式了可读性会急剧下降。你可以拆成普通函数或多个步骤。比如下面这个三层嵌套我能看懂但维护时绝对会头疼[(a,b,c) for a in range(5) for b in range(a) for c in range(b) if c 0]这种代码在一个月后回看要花很长时间才能反应出它的逻辑。项目里最重要的不是炫技而是让同事和自己都能快速理解。条件在推导式里的位置也很有意思。比如(x for x in lst if condition)中的if是过滤内层而(f(x) if condition else g(x) for x in lst)中的if-else是映射表达式的一部分。很多人容易混淆记住if在for后面是过滤if-else在for前面是选择映射。例子nums [3, -1, 2, -5] # 过滤负数只保留正数 positive [n for n in nums if n 0] # 映射正数不变负数转成绝对值 abs_map [n if n 0 else -n for n in nums]这个区分在实际代码中经常用到特别是处理清洗逻辑时。4. 实战用推导式完成常见的数据构建与转化4.1 爬虫数据清洗过滤空值、去除重复、提取字段爬虫最麻烦的不是发请求而是清洗抓回来的数据。你从网页、接口拿到一堆列表里面掺杂着None、空字符串、缺失字段、重复数据。这时候推导式是你的救命工具。假设爬虫返回的是很多字典每个字典表示一条商品信息但有些商品缺少标题有些价格是空字符串raw_products [ {title: iPhone 15, price: 5999}, {title: , price: 2999}, {title: MacBook, price: None}, {title: iPhone 15, price: 5999}, {title: None, price: 1999}, ]先过滤掉标题为空或价格为空的条目valid_products [ item for item in raw_products if item.get(title) and item.get(price) ]再去重。因为字典本身不可哈希不能直接放进集合去重。可以先用 “标题价格”的元组作为去重键构建一个临时字典再取回原值unique_products list({(p[title], p[price]): p for p in valid_products}.values())这里字典推导式把元组键映射到原始字典后出现的重复键会覆盖先出现的最终.values()就得到去重后的列表。这一行代码同时完成了“构建映射”和“转化回列表”两件事。再比如我们需要从原始数据中只提取价格字段并转成浮点数列表方便后续统计分析prices [float(p[price]) for p in valid_products if p[price].isdigit()]这种写法比手动循环加append清爽太多而且清洗逻辑集中在一行别人读起来也能一眼看懂规则。4.2 数据分析场景用字典推导式构建统计聚合数据分析中经常需要按某个字段做聚合。比如一份销售数据包含城市和销售额sales [ {city: 北京, amount: 1200}, {city: 上海, amount: 980}, {city: 北京, amount: 1500}, {city: 广州, amount: 700}, ]如果你试图用一个字典推导式直接完成聚合求和会失败因为推导式对每个元素只执行一次映射不适合做累加。正确的方式是先初始化结构再循环累加total {} for s in sales: total[s[city]] total.get(s[city], 0) s[amount]这里dict.get(key, 0)是构建聚合结果的关键技巧键不存在时返回 0避免KeyError。但是我们仍然可以用字典推导式做一些初始化工作比如把每个城市的初始金额设为 0cities [s[city] for s in sales] amount_total dict.fromkeys(set(cities), 0) for s in sales: amount_total[s[city]] s[amount]虽然这个初始化不是必须的但在数据量大、后续需要多次更新时先建好结构能避免反复判断键是否存在。如果数据源是两列平行列表比如城市列表和金额列表可以直接用字典推导式来构建映射cities [北京, 上海, 北京, 广州] amounts [1200, 980, 1500, 700] # 去重后的城市列表作为键 city_amount {c: amounts[cities.index(c)] for c in set(cities)}不过要注意cities.index(c)返回第一次出现的索引遇到重复城市会取第一个金额所以这种写法只适合“城市只出现一次”的场景。如果要做聚合还是前面循环累加更稳妥。数据分析里另一个常用操作是用Counter快速统计频率。Counter本质上就是字典的子类但它底层是 C 优化过的from collections import Counter word_counts Counter([apple, banana, apple, cherry])我可以把它和字典推导式结合比如只保留出现次数大于 1 的词hot_words {word: count for word, count in word_counts.items() if count 1}这种“先用 Counter 构建再用字典推导式筛选转化”的组合在日常数据处理里非常实用。4.3 量化交易场景快速比对股票代码集合与交集运算量化交易里经常要处理股票代码列表。比如一个策略筛选出看涨股票另一个策略筛选出低波动股票最后要取两者的交集作为最终买入池。用列表和循环当然能实现但用集合运算明显更清晰。先定义两个策略的候选列表strategy_a [600519, 000858, 000333, 002415] strategy_b [600519, 000333, 300750, 601318]如果你需要把列表转换成集合直接用set()就行但有时候你需要先做条件筛选再转集合这时集合推导式就派上用场了set_a {code for code in strategy_a if code.startswith(60)} set_b {code for code in strategy_b if not code.startswith(00)}然后直接做交集和差集buy_list set_a set_b # 两个策略都看好的股票 only_a set_a - set_b # 只在策略A中出现的股票集合运算不仅写起来方便底层哈希表实现让它在数据量非常大时依然很快。这在回测、盘中选股场景里很有价值因为每次交易信号更新都要求毫秒级响应。如果股票代码需要映射到股票名称可以用字典推导式从一个“代码-名称”列表构建映射stock_info [ (600519, 贵州茅台), (000858, 五粮液), (000333, 美的集团), ] name_map {code: name for code, name in stock_info}然后结合买入池快速找出对应的名称buy_names [name_map[code] for code in buy_list if code in name_map]这段代码里集合交集已经帮我们把范围缩小了再用列表推导式去查找名称整个过程清晰又高效。你可以看到数据结构的“构建”和“转化”在量化场景里是无处不在的。4.4 从脚本到exe推导式在打包场景的注意点热词里很多人搜“python 打包成exe”说明不少小伙伴写脚本是要发给同事用的。打包时推导式这种写法本身没有兼容性问题但有几个小坑值得提一下。第一如果打包环境里的 Python 版本比你开发环境低某些推导式特性可能不可用。比如 Python 3.7 才保证字典保持插入顺序Python 3.8 才支持海象运算符配合推导式所以尽量统一版本。第二调试时不要过度依赖一条很长的推导式。打包后的程序一旦报错错误信息往往指向整行定位问题很麻烦。我写代码时会故意把复杂的业务逻辑拆成几步保留中间变量方便打包后排查逻辑错误。第三生成器表达式在打包成 exe 后处理大数据时要格外注意文件句柄的释放比如open()配合生成器后要在外部手动关闭文件否则可能造成文件占用。这些经验不是推导式本身的问题而是工程化时容易忽略的细节。掌握推导式能让你写出更 Pythonic 的代码但好代码还要兼顾可调试性和可维护性。5. 常见问题、踩坑记录与性能优化5.1 推导式容易踩的坑学推导式时有几个问题几乎每个人都遇到过。第一个坑是嵌套顺序搞反。比如二维列表展平很多人会写成[num for num in row for row in matrix]结果就是NameError因为执行到第一个num in row时row还没定义。正确顺序是外层循环在前。写之前先在脑子里过一遍 for 循环的嵌套顺序再套进推导式里。第二个坑是字典推导式键冲突。如果两个元素生成相同的键后面的值会覆盖前面的值而且不会有任何提示。比如上面去重时这可能是你想要的但如果你本想保留多个值就会悄悄丢数据。所以使用前一定要确认键的唯一性。第三个坑是变量作用域。Python 3 的推导式有独立的局部作用域不会把循环变量泄漏到外部这点比 Python 2 好很多。但如果你在推导式里调用了一个修改外部列表的函数就属于副作用代码会变得很难预测。比如data [] result [data.append(x) for x in range(5)]这会把data变成[0,1,2,3,4]同时result变成[None,None,None,None,None]因为list.append()返回None。这种写法几乎永远是反模式不要这样写。第四个坑是过度嵌套。推导式本身是简洁工具嵌套超过两层后代码可读性严重下降甚至不如普通循环清晰。遇到这种情况我建议拆开成多个推导式或函数性能不会差多少但可维护性会好很多。5.2 什么时候不要用推导式我见过不少人把推导式用得很嗨结果代码里出现了“一行超人”别人根本看不懂。以下场景我强烈建议放弃推导式需要副作用时。比如循环里要print、要更新多个外部变量、要写入文件这些操作不应该作为推导式的 expression。推导式设计初衷是“纯净的映射和过滤”不是在列表生成过程中偷偷做额外操作。这个时候用普通循环反而更清晰。逻辑复杂时。如果条件判断有多层 if 嵌套或者表达式非常长比如超过 80 个字符或者需要写一个辅助函数才能让表达式变得可读那就别硬用推导式。你可以先写辅助函数在推导式里调用它这样既保持简洁又不损失逻辑表达。需要异常处理时。如果循环体内有 try/except推导式无能为力。比如解析字符串成数字可能会遇到无法解析的值你需要跳过或设默认值。这种场景建议用循环或map 处理函数。大数据量且需要复用数据时。如果只想遍历一次用生成器表达式如果需要多次访问必须用列表否则生成器第二次遍历就是空的。这个前面讲过但很多新手还是会踩。5.3 性能对比推导式、map/filter与显式循环很多人关心推导式到底快不快。我做了一个简单测试构建一个包含 100 万个整数的列表分别用三种方式过滤偶数并平方显式 for 循环map配合filter列表推导式在常见 Python 3.10 环境下结果大致如下不同机器略有差异但趋势一致方式耗时相对代码可读性推荐程度显式 for 循环最高基准容易理解适合复杂逻辑mapfilter略低于 for 循环中等适合函数式编程风格列表推导式比 for 快 20%~30%高最推荐列表推导式快的原因是它底层用 C 循环优化了 append 操作减少了每次迭代时 Python 字节码的开销。map和filter本身也是 C 实现但由于需要额外函数调用有时不一定比推导式快。要说明的是性能不是选择写法的唯一标准。一个清晰、容易维护的 for 循环远胜过一个花哨但没人看得懂的推导式。我的判断标准很简单如果推导式一行能说明白逻辑且不超过 80 字符就用它否则就拆开。5.4 实用建议让推导式可读性更高的几个习惯最后分享几个我自己写推导式的习惯都是日常踩坑总结出来的。给变量起有意义的名字。不要吝啬变量名长度。比如[p[price] for p in products if p[price]]里的p可以换成prod虽然多写点字但读起来舒服很多。把条件抽出来定义成函数。如果过滤条件比较复杂比如“价格大于 100 且库存大于 0 且标题不含促销词”可以定义一个is_valid_product()函数然后在推导式里调用它。这样推导式依旧一行但所有业务逻辑都放在函数里便于测试和维护。一行太长就换行。Python 的推导式允许在方括号内换行。把 for、if、expression 分开写配合缩进能极大提升可读性result [ compute(item) for item in items if condition1(item) if condition2(item) ]不要只盯着列表推导式。很多时候生成器表达式、字典推导式、集合推导式才是真正合适的选择。你需要根据“输出是什么”来决定用哪种结构。另外一个技巧是把推导式当作数据管道的一段。比如清洗数据时可以先用列表推导式过滤再用字典推导式映射再用集合推导式去重。每一步只做一件事组合起来就是清晰的数据流。这比在一个大循环里既过滤又映射又去重要容易理解得多。我个人的体会是推导式学起来不难难的是在合适的场景选择合适的结构。写代码时习惯先问自己我现在的输入是什么输出应该是什么中间需要保留哪些临时信息这三个问题想明白后推导式的写法往往是水到渠成。最后再分享一个小技巧遇到不确定的推导式行为直接在 REPL 里写一个最小样例跑一遍比空想快得多。这个习惯帮我避免了好几次线上 bug希望你也能用上。