ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python Dict与Set完全指南:从基础操作到底层哈希原理与实战

Python Dict与Set完全指南:从基础操作到底层哈希原理与实战 我在不少项目里都发现大家写Python代码卡壳很多时候不是因为语法不会而是对Dict和Set这两个内置数据结构的理解太浅。尤其是字典——Python里的字典远不止一个能存键值对的容器它同时是很多高性能写法的基石集合Set呢看起来就是一组不重复的元素但把它用在去重、关系判断、图论建模上时效率高得惊人。这篇内容我会结合自己在开发里的经验带你把Dict和Set从“会用”提升到“用好”先从字典的创建和基本操作讲起再深入视图、默认值、嵌套、排序这些进阶场景接着把集合的去重和集合运算讲透最后再看哈希表的底层原理并且用几个真实案例说明到底该在什么场景用它们。不管是刚装好Python准备入门的新手还是想再加固基础的中级开发者都能从里面找到有实际操作价值的东西。1. 从零开始用 Dict创建、增删改查与最常见的坑1.1 字典的底层设计思路键值对与哈希索引Dict字典是 Python 里使用频率最高的数据结构之一。它的本质是一张映射表把“键”对应到“值”比如把用户名对应到用户资料、把商品编号对应到库存数量。最直观的生活类比是通讯录你按拼音首字母翻到那个姓就能直接看到电话号码不需要从头到尾把整本通讯录翻一遍。这个“直接定位”的能力靠的是哈希表。Python 在把键存进字典时会先算这个键的哈希值再用哈希值决定存放位置。取的时候同样先算哈希值然后直接去那个位置拿结果。所以无论字典里有多少条数据取一个键的时间基本上都是恒定的——这就是O(1)的来源。这一点和列表完全不同列表查找一个元素得从头遍历数据越大越慢。Python 3.7 开始字典还保留了键的插入顺序。也就是说你按什么顺序put进去keys()和items()遍历时就按什么顺序出来。这个特性已经写进了语言规范意味着你不仅能拿字典当映射表还能拿它当一个自带顺序的“键值对列表”用。user {name: Alice, age: 30, city: Shanghai} print(user[name]) # Alice print(user[age]) # 301.2 五种创建字典的方式按场景选创建字典最常用的方式就是字面量花括号里写键值对冒号分隔键和值逗号分隔多个条目。# 方式一字面量 d1 {name: Alice, age: 30} # 方式二dict() 构造函数 d2 dict(nameAlice, age30) # 方式三dict.fromkeys批量初始化同一个默认值 d3 dict.fromkeys([a, b, c], 0) # 方式四zip 两个序列一个做键一个做值 keys [name, age, city] values [Alice, 30, Shanghai] d4 dict(zip(keys, values)) # 方式五字典推导式 d5 {k: len(k) for k in [apple, banana, cherry]}这五种方式各有各的适用场景。字面量最直觉适合写固定配置dict()适合在接口调用里传关键字参数fromkeys在做“给一组键统一初始化”时非常省事比如给一批股票代码都赋初始价格 0zip适合把两个已经对齐的列表合并成字典推导式则是在需要做“键值转换”时最优雅的写法。注意空花括号{}创建的是空字典不是空集合。想要空集合必须用set()。这个坑我见不少新人踩过。1.3 增删改查的完整实操新手照着敲就行字典的基本操作可以总结成一张表实际操作时几乎天天用到操作代码注意事项新增或修改d[key] value键不存在则新增存在则覆盖读取d[key]键不存在直接抛KeyError安全读取d.get(key, default)键不存在返回默认值不抛异常删除del d[key]键不存在抛KeyError安全删除d.pop(key, default)键不存在返回默认值弹出末尾d.popitem()Python 3.7 按 LIFO 弹出最后插入的键值对清空d.clear()全部删除判断存在key in d判断键是否存在判断的是键不是值比较容易被忽略的一点in判断字典成员时判断的是“键”不是“值”。这是新手经常弄混的地方。你要是想判断某个“值”是否存在得配合values()视图来写但这样就是O(n)遍历了和列表查找一样慢。scores {math: 90, chinese: 85, english: 92} # 新增 scores[physics] 88 # 修改 scores[math] 95 # 安全读取 grade scores.get(history, no record) print(grade) # no record # 删除 scores.pop(chinese, None) # 判断 if math in scores: print(数学成绩存在)1.4 遍历字典的三种姿势与动态视图遍历字典最常见的有三种写法info {name: Alice, age: 30, city: Shanghai} # 遍历键 for key in info: print(key) # 遍历值 for value in info.values(): print(value) # 遍历键值对最推荐 for key, value in info.items(): print(f{key}: {value})keys()、values()、items()返回的都是“视图对象”。视图对象有一个特点它是动态的字典一旦变化视图也会跟着变。这个特性在做“边遍历边统计”时有用但也容易出问题——后面我会专门讲遍历时修改字典的坑。另外一个实用技巧是如果你需要同时拿到“索引位置”和“键值对”可以直接套enumeratefor idx, (key, value) in enumerate(info.items()): print(idx, key, value)2. Dict 进阶默认值、计数、合并与排序一条龙讲透2.1 get 和 setdefault 怎么选别再乱用了get和setdefault表面上看都能提供默认值但作用时机完全不同。get是“查一下没有就返回默认值但不修改原字典”setdefault是“查一下没有就把默认值写进字典再返回这个值”。stock {} # get不修改原字典 price stock.get(AAPL, 0) print(price) # 0 print(stock) # {} # setdefault键不存在时插入默认值 price stock.setdefault(AAPL, 0) print(price) # 0 print(stock) # {AAPL: 0}实际用的时候setdefault非常适合“初始化桶”这种场景。比如你要把一个列表按首字母分组words [apple, banana, avocado, blueberry, cherry] grouped {} for word in words: grouped.setdefault(word[0], []).append(word) print(grouped) # {a: [apple, avocado], b: [banana, blueberry], c: [cherry]}如果不使用setdefault你每回都得先判断键存不存在代码会多出好几行if。但如果你频繁做这种“没有就建空列表”的操作更推荐后面讲的defaultdict那才是真正的解法。2.2 defaultdict 自动兜底省掉一摞 ifcollections.defaultdict是内置dict的子类它重写了__missing__方法。当你要访问一个不存在的键时它不会抛KeyError而是调用你在创建时传入的工厂函数自动生成一个默认值。from collections import defaultdict # 默认值是 0 counter defaultdict(int) counter[apple] 1 print(counter[apple]) # 1 # 默认值是空列表 d_llist defaultdict(list) d_llist[group_a].append(1) print(d_llist[group_a]) # [1] # 默认值是空集合 d_set defaultdict(set) d_set[users].add(u1) print(d_set[users]) # {u1}defaultdict用起来是真的舒服。我在处理日志、做数据清洗的时候经常要按某个字段分组纯手写if key not in d: d[key] []非常啰嗦换成defaultdict(list)之后一行代码就解决。但要注意defaultdict的默认值生成只发生在d[missing_key]访问时用d.get(missing_key)仍然不会触发默认值创建。所以如果你用了defaultdict尽量别混用get否则会得到和预期不一样的结果。2.3 用 Counter 一行完成词频统计很多同学不知道collections.Counter其实它是专门为“计数”设计的字典子类。最常见的用法是统计列表里各元素出现次数。from collections import Counter words [apple, banana, apple, cherry, banana, apple] cnt Counter(words) print(cnt) # Counter({apple: 3, banana: 2, cherry: 1}) # 取出现最多的前两个 print(cnt.most_common(2)) # [(apple, 3), (banana, 2)] # 提取所有不重复元素 print(set(cnt.keys())) # {apple, banana, cherry}Counter和普通字典最大的区别是它支持加减运算、交集并集等操作看着就像做数学题一样c1 Counter({a: 1, b: 2}) c2 Counter({b: 1, c: 3}) print(c1 c2) # Counter({c: 3, b: 3, a: 1}) print(c1 c2) # Counter({b: 1})取交集取较小计数如果你在写量化交易策略、词频分析、用户行为统计这类代码Counter是比手写dictif高好几个档次的工具。在Counter内部它其实就是一个以元素为键、以出现次数为值的字典。2.4 字典合并update、双星号、竖线三种方式对比合并字典是日常开发里很常见的需求。Python 提供了好几种方式不同版本还有不同推荐偏好。d1 {a: 1, b: 2} d2 {b: 3, c: 4} # 方式一update原地修改返回 None d1.update(d2) print(d1) # {a: 1, b: 3, c: 4} # 方式二双星号解包生成新字典 d3 {a: 1, b: 2} merged {**d3, **d2} print(merged) # {a: 1, b: 3, c: 4} # 方式三Python 3.9 的竖线操作符 merged d3 | d2 print(merged) # {a: 1, b: 3, c: 4}需要注意一个细节多个字典合并时后面的字典会覆盖前面字典里相同的键。这在你合并配置项时非常有用——比如先加载默认配置再用用户配置覆盖默认值后面的优先级更高。update是原地修改不返回新字典{**d1, **d2}和d1 | d2都是生成新字典。如果你不想污染原数据务必用后两种。2.5 字典排序按值排序和按键排序的正确写法字典本身是无序的这里的“无序”不是指插入顺序而是指没有“排序”方法。但你可以用sorted()配合items()对键值对排序得到一个列表再根据需要转回字典。scores {math: 90, chinese: 85, english: 92} # 按值升序 sorted_by_value sorted(scores.items(), keylambda item: item[1]) print(sorted_by_value) # [(chinese, 85), (math, 90), (english, 92)] # 按值降序 sorted_by_value_desc sorted(scores.items(), keylambda item: item[1], reverseTrue) print(sorted_by_value_desc) # [(english, 92), (math, 90), (chinese, 85)] # 按键排序 sorted_by_key sorted(scores.items()) print(sorted_by_key) # [(chinese, 85), (english, 92), (math, 90)]这里要注意sorted()返回的是列表里面的每个元素是元组。如果你希望排序结果仍然是一个“字典”需要再套一层dict()sorted_dict dict(sorted(scores.items(), keylambda item: item[1], reverseTrue)) print(sorted_dict) # {english: 92, math: 90, chinese: 85}因为 Python 3.7 的字典保持插入顺序所以转成新字典后遍历顺序就是排序后的顺序。这个特性让“字典按值排序”变成了很多排行榜功能的简洁实现。2.6 嵌套字典JSON 数据结构的核心Python 里的 JSON 解析结果全部是嵌套字典加列表的组合。你从接口拿到的数据基本长这样data { user: { name: Alice, tags: [developer, python], settings: {theme: dark, lang: zh} } }访问嵌套值时最简单的就是一层层用方括号取name data[user][name] theme data[user][settings][theme] print(name, theme) # Alice dark要是层级很深或者中间某一层可能没有就要小心KeyError。比较稳妥的做法是逐层get或用defaultdict。不过更推荐你写一个递归取值的小工具在处理不规则嵌套结构时非常省力。def deep_get(d, path, defaultNone): for key in path.split(.): if isinstance(d, dict): d d.get(key) if d is None: return default else: return default return d print(deep_get(data, user.settings.theme)) # dark print(deep_get(data, user.missing.field)) # None这也是我在处理接口返回数据时一定会封装的一个函数毕竟真实接口的返回结构永远比你预期的更复杂。3. Set 使用全攻略从去重到集合运算一套完整打法3.1 Set 到底是什么能解决什么问题Set集合在 Python 里是一组“无序、不重复”的元素。无序意味着你不能通过下标访问不重复意味着同一个元素只能出现一次。它的底层也是哈希表所以查找元素的速度和字典一样都是O(1)。很多人对集合的印象停留在“去重”但它的能力远不止这些。集合天生支持数学里的交集、并集、差集、对称差集运算。这意味着你可以用它做“共同好友”“标签匹配”“权限比对”这类关系判断写起来比循环 列表嵌套干净得多。s {1, 2, 3} print(type(s)) # class set空集合的创建要注意不能用{}那会创建空字典。必须用set()。3.2 创建与基本操作add、update、remove、discard集合常用操作如下表操作代码说明创建s {1, 2, 3}或s set([1, 2, 3])传入可迭代对象添加单个s.add(element)元素已存在则不操作批量添加s.update([4, 5, 6])接受任何可迭代对象删除元素s.remove(element)不存在会抛KeyError安全删除s.discard(element)不存在不报错随机弹出s.pop()弹出并返回一个任意元素清空s.clear()清空所有元素remove和discard是新手最容易混淆的一对。如果你在写“清理已处理任务”这类逻辑不存在时不算错误用discard更稳如果你明确知道元素一定存在删掉就是业务逻辑的一部分用remove更合适它能及时暴露问题。tags {python, data, ai} tags.add(ml) tags.update([nlp, cv]) tags.remove(data) tags.discard(not_exist) # 不会报错 print(tags) # {python, ai, ml, nlp, cv}3.3 集合运算与关系判断文氏图里那套东西的 Python 实现这一部分是Set真正的杀手锏。用运算符可以直接实现数学里的集合运算a {python, sql, java} b {sql, golang, rust} # 并集 print(a | b) # {python, sql, java, golang, rust} # 交集 print(a b) # {sql} # 差集在 a 但不在 b print(a - b) # {python, java} # 对称差集去掉共同部分 print(a ^ b) # {python, java, golang, rust}除了运算符集合对象本身也提供了对应的方法union()、intersection()、difference()、symmetric_difference()。它们和运算符的区别是方法可以接受列表、元组等其他可迭代对象运算符要求两侧都得是集合。关系判断也是集合的高频用法x {a, b} y {a, b, c} print(x.issubset(y)) # Truex 是 y 的子集 print(y.issuperset(x)) # Truey 是 x 的超集 print(x.isdisjoint({c})) # True两个集合没有交集实际业务里“看 A 是不是 B 的子集”在权限校验、功能开关、配置检查时都很常见。比如检查用户权限集合是否包含所有必需权限一行required.issubset(user_perms)就搞定。3.4 不可哈希元素为什么 list 进不了 set集合的底层是哈希表所以放进集合的元素必须可哈希。可变容器如list、dict、set都是不可哈希的直接放进去会抛TypeError: unhashable type: list。而不可变的tuple、frozenset则可以。# 会报错 # {[1, 2], 3} # 正确写法用元组 {(a, 1), (b, 2)}如果你需要“一个可哈希的集合”Python 提供了frozenset它是不可变的可以做为字典的键或者放进另一个集合。fs frozenset([1, 2, 3]) print(fs) # frozenset({1, 2, 3})这里还有个隐藏的坑1和True在哈希表里是同一个键。因为True在 Python 中等价于整数1。所以{1, True, 0, False}实际上只有两个元素1和0。我在一次统计用户“开启状态”时遇到过这个问题排查了很久才发现。4. 深入底层哈希表和性能差异到底为什么快4.1 Dict 和 Set 的底子都是哈希表要真正用好Dict和Set必须理解它们快在哪。Python 的字典和集合底层都是一个数组数组的每个位置是一个桶。put元素时先调hash()计算键的哈希值再经过一定规则映射到数组下标把数据放进去get元素时又算一遍哈希值直接去对应下标取。如果两个不同的键算出的下标冲突了Python 会采用“开放寻址法”继续探测下一个空位。这也是为什么哈希表的平均复杂度是O(1)但在极端情况下可能退化到O(n)。好在这套机制 CPython 内部已经做得很成熟日常开发基本不用操心冲突问题。4.2 列表和集合的成员判断性能对比用实际数据感受一下在一个有 100 万元素的列表里判断某个元素是否存在最坏情况要遍历完整个列表而在同等大小的集合里基本上一次哈希定位就能完成。我用一个脚本实测过差距import time data_list list(range(1_000_000)) data_set set(data_list) target 999_999 # 列表成员判断 start time.perf_counter() print(target in data_list) print(list 耗时:, time.perf_counter() - start) # 集合成员判断 start time.perf_counter() print(target in data_set) print(set 耗时:, time.perf_counter() - start)在我的机器上list的成员判断耗时在几毫秒到十几毫秒之间set基本在纳秒级别。数据量越大差距越明显。这就是为什么写爬虫时 URL 去重、写词频统计时元素去重都优先考虑set而不是list if not in。4.3 空间换时间为什么不要在大数据量下滥用但要注意哈希表的“快”是有代价的。字典和集合的内存开销远大于列表。因为数组需要预留大量空位来减少哈希冲突负载因子到一定程度还会触发扩容。举例说一个包含 10 万元素的set内存开销可能是一个同规模list的好几倍。所以原则是查找频繁、去重频繁的场景用Set或Dict如果你只是要顺序遍历、按下标访问、存有序小批量数据那么list可能更合适。经验之谈数据量小几百到几千时list和set的差距可以忽略怎么顺手怎么来数据量达到十万以上并且涉及频繁成员判断就必须上set。这是我拆过很多性能问题后得出的结论。4.4 自定义对象做键hash 和 eq 必须一起考虑想让自定义类的实例放进set或作为字典的键不光要定义__hash__还要定义__eq__。Python 在检查“两个键是否相同”时会先比较哈希值再调用__eq__确认相等。如果你只重写了__eq__没重写__hash__这个对象会变得不可哈希直接报错。更隐蔽的问题是如果把一个可变对象放进了set之后又修改了它的属性导致哈希值变化那么这个对象在集合里就会“找不到”了。这会让集合和字典出现逻辑错乱。所以我建议自定义类做键时尽量用不可变字段参与__hash__计算或者干脆用元祖、字符串这类内置不可变类型做键。class User: def __init__(self, uid, name): self.uid uid self.name name def __hash__(self): return hash(self.uid) def __eq__(self, other): return isinstance(other, User) and self.uid other.uid u1 User(1, Alice) u2 User(1, Bob) # uid 相同 users {u1} print(u2 in users) # True因为 __eq__ 只比较 uid5. 真实场景案例邻接矩阵、URL 去重和递归记忆化5.1 用 Dict Set 构建图的邻接表图论题里最常用的数据结构是邻接表。Python 里实现邻接表最自然的组合是“字典加集合”键是节点值是不可达的邻居集合。graph { A: {B, C}, B: {A, D, E}, C: {A, F}, D: {B}, E: {B, F}, F: {C, E} }用set存邻居有两个好处一是加边时天然去重不用判断是不是已经有了二是判断两个节点是否相邻时是O(1)操作。在 BFS 或 DFS 遍历时用一个set来记录已访问节点同样能极大简化代码。def bfs(graph, start): visited {start} queue [start] order [] while queue: node queue.pop(0) order.append(node) for neighbor in graph[node]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) return order如果你处理的是加权图那邻居set就不够了得改成dict邻居节点做键权重做值。这就是“字典套字典”的经典结构。weighted_graph { A: {B: 5, C: 2}, B: {A: 5, D: 1}, C: {A: 2, D: 8}, D: {B: 1, C: 8} }5.2 爬虫场景的 URL 去重写爬虫时最头疼的问题之一就是重复爬取。一个 URL 可能被多个页面反复引用不去重的话爬虫会变成死循环还会给服务器带来不必要的压力。set在这里是天然的“已访问集合”。visited set() todo [https://example.com/page/1] while todo: url todo.pop() if url in visited: continue visited.add(url) # 假装这里是抓取逻辑 # new_urls fetch_links(url) # todo.extend(new_urls)如果 URL 数量大得内存快撑不住可以考虑用bloom filter布隆过滤器这种概率结构做补充它用少量内存换取“可能有重复”的误判但这是后话。单机百万量级的 URL纯set其实完全扛得住。5.3 李白打酒递归记忆化里的 Dict 用法“李白打酒”是 Python 社区里很经典的一道递归题描述是李白提着酒壶出门遇到酒店就把酒量翻倍遇到花就喝掉一斗。一共遇到店 5 次、花 10 次最后一次遇到的是花且壶中刚好没酒问初始有多少斗酒。直接暴力递归容易重复计算。用字典做记忆化缓存就能把指数级计算压缩到一次遍历完成。这里的Dict充当了“动态规划表”。from functools import lru_cache # 参数含义店、花、当前酒量 lru_cache(maxsizeNone) def solve(shop, flower, wine): # 如果酒量小于0剪枝 if wine 0: return 0 # 最后一次必须是花且壶中没酒 if shop 0 and flower 0: return 1 if wine 0 else 0 # 如果酒量大于剩余花数永远喝不完剪枝 if wine flower: return 0 res 0 if shop 0: res solve(shop - 1, flower, wine * 2) if flower 0: res solve(shop, flower - 1, wine - 1) return res # 店5次、花10次、最后遇到花且酒刚好为0 print(solve(5, 10, 2))这里lru_cache底层就是一个字典键是(shop, flower, wine)三元组值是计算结果。你如果不想用装饰器完全可以手写一个字典做缓存原理完全一样。这个例子特别能说明字典不只是存业务数据还可以存算法中间状态是动态规划和记忆化搜索的基石。5.4 量化交易与数据统计中的 Dict/Set 组合我做过一些量化策略的回测发现Dict和Set在数据清洗阶段特别能派上用场。比如统计一段时间内不同交易品种的涨跌天数from collections import defaultdict, Counter daily_returns [ {symbol: AAPL, change: 0.02}, {symbol: TSLA, change: -0.03}, {symbol: AAPL, change: 0.01}, {symbol: MSFT, change: 0.005}, ] # 统计每个品种的上涨天数 up_days Counter() # 记录每个品种的每日收益率序列 history defaultdict(list) for item in daily_returns: symbol item[symbol] change item[change] history[symbol].append(change) if change 0: up_days[symbol] 1 # 去重得到当前所有交易品种 symbols {item[symbol] for item in daily_returns} print(symbols) print(dict(up_days))看一个统计任务里Counter负责计次defaultdict负责收集序列set负责去重。三者配合代码非常干净。这也是“结构化数据”处理的标准打法。6. 新手常见问题与排错手册我踩过的坑都在这里6.1 KeyError 的三种解法KeyError是字典新手最常遇到的报错原因就是访问了不存在的键。解法有三种d.get(key, default)只读场景不需要往字典里插入默认值。d.setdefault(key, default)读场景但需要初始化桶减少多余if。collections.defaultdict整体默认值统一适合批量初始化。我个人的习惯是如果只读一次用get如果需要初始化结构用defaultdictsetdefault偶尔用主要场景是“字典已经存在不想改类型”时。6.2 遍历时修改字典RuntimeError 的根源在迭代字典的同时往字典里添加、删除键会触发RuntimeError: dictionary changed size during iteration。这是 Python 保护迭代器稳定性的机制。d {a: 1, b: 2} # 会报错 # for k in d: # if k a: # d[c] 3如果要在遍历过程中修改可以先把键转成列表d {a: 1, b: 2} for k in list(d.keys()): if k a: d[c] 3 print(d) # {a: 1, b: 2, c: 3}更好的方案是重建字典比如用字典推导式过滤键这样既不报错语义也清晰。6.3 unhashable type: list 到底错在哪这个报错几乎每个用set的人都见过。根源是list是可变的不能做键也不能进集合。解决方法是改成tupledata [1, 2] # s {data} # TypeError s {tuple(data)} print(s) # {(1, 2)}延伸一下如果你需要以“二维坐标”为键做状态记录(x, y)元组就是最常见的方案。在写图论、棋盘类算法题时这一招是标配。6.4 空花括号到底是字典还是集合这是高频混淆点{}是空字典set()才是空集合。用type({})看一下就知道是class dict。如果你要建一个空的“集合”然后往里面add元素却写成了{}后面所有add都会报AttributeError: dict object has no attribute add。这个错误信息一度是 Python 社区新手提问榜前列。6.5 字典排序后丢了字典类型dict.items()套sorted()得到的是列表不是字典。如果你希望排序后还能用.get()、.keys()等方法记得再用dict()包一下scores {math: 90, chinese: 85} sorted_scores sorted(scores.items(), keylambda x: x[1]) # 此时是列表不是字典 result dict(sorted_scores)6.6 JSON 解析后键的类型不是字符串从json.loads()得到的字典键默认全是字符串。但如果你手写字典时键用了整数比如{1: a}序列化成 JSON 再解析回来键就变成1了。这种“类型漂移”在接口对接时很容易埋雷。import json d {1: a, 2: b} s json.dumps(d) print(s) # {1: a, 2: b} r json.loads(s) print(type(list(r.keys())[0])) # class str所以做接口联调时最好明确约定键的类型或者解析完统一做一次类型转换避免后续逻辑因为“键是字符串还是数字”而摸不着头脑。6.7 布尔值和数字在 Set 里的隐藏坑最后再说一个隐蔽的坑True 1False 0这导致布尔值和整数在某些场景下是同一个哈希键。s {1, True, 0, False} print(len(s)) # 2如果你在统计状态、开关量去重时混用了布尔和数字结果可能和你预期的不一致。稳妥的写法是统一状态类型不要一会儿用1表示开一会儿用True表示开。踩过这么多坑之后我最深的体会是Dict和Set的表面用法花不了多少时间就能学会真正的价值在于“你愿意在合适的场景想起它们”。遇到要快速查找、去重、统计、做关系判断的需求时先想set和dict能不能一行解决而不是立刻上循环。把这两个结构用熟代码的简洁程度和执行效率都会往上走一大截。
返回列表