简介:面向Python入门与进阶学习者,这份完整教学课件聚焦内置数据结构字典(dict),以PPT形式系统梳理字典的核心知识:从dict()、d={}、dict(**kwargs)、dict.fromkeys等定义初始化方式,到d[key]与get/setdefault的取值差异;从update合并更新、pop/popitem/clear/del的删除操作,到for循环遍历keys、values、items的多种写法,并特别指出遍历时移除元素的正确与错误做法,避免初学者常见的运行时异常。课件还讲解了可哈希对象才能作为key的底层要求,并扩展到defaultdict、OrderedDict等collections模块常用扩展,帮助读者在真实项目中灵活选择合适的数据结构,覆盖实际开发中的高频场景与易错点。压缩包仅含1个pptx文件,大小约2.02MB,内容紧凑、编排清晰,既适合教师直接用于课堂教学,也适合自学者按节次系统性复习。课件中每个方法均配有简洁语法示例,方便对照练习,目前已有127人学习阅读,是快速掌握Python字典机制、提升代码效率的实用参考资料。
1. 为什么 Python 字典是写业务代码时最先该掌握的容器
Python 里最容易被低估的容器就是字典(dict)。很多人把它当成“能存键值对的那个东西”,但实际上一旦进入数据处理、接口对接、配置管理这类日常开发,字典的使用频率会远超列表。它不光是 Python 语法的一部分,更是 Python 这门语言在设计上最核心的数据结构之一——函数的关键字参数、类的__dict__、json模块的解析结果,底层全都有字典的身影。
这一节要讨论的不是“字典怎么用”这种入门问题,而是把字典当成一个工程工具来拆解:它为什么快、怎么存、怎么取、什么时候不该用它、以及在真实业务里怎么写出不踩坑的字典操作代码。无论你是刚学 Python 的新手,还是写过几年 Python 想要把基础打得更扎实的开发者,这一节的内容都值得从头过一遍,因为很多看起来不起眼的细节——比如视图对象的实时性、键的哈希要求、合并运算符的行为差异——恰恰是线上 bug 最容易藏身的地方。
2. Python 字典的构造方式与键值对的底层约束
2.1 四种常见创建方式,哪种效率最高
字典的创建方式并不只有字面量一种,不同场景选不同写法,可读性和执行效率会有明显差别。最常见的四种方式如下:
# 方式一: 字面量创建 d1 = {"name": "Tom", "age": 25} # 方式二: dict 构造器 + 关键字参数 d2 = dict(name="Tom", age=25) # 方式三: dict 构造器 + 可迭代对象 d3 = dict([("name", "Tom"), ("age", 25)]) # 方式四: fromkeys 批量创建 d4 = dict.fromkeys(["name", "age", "city"], "unknown") print(d1, d2, d3, d4, sep="\n")方式一在 Python 3.6+ 中会走专门的字节码LOAD_CONST路径,创建速度最快,而且代码最直观。方式二适合键名符合标识符规则的情况,但键名一旦带有连字符或中文字符就不能用。方式三用于从已有的键值对列表或元组序列转换,在解析外部数据时常用。方式四用来批量初始化键,所有键共享同一个初始值对象,注意如果初始值是可变对象(比如空列表),所有键会指向同一个列表,改动一个就全都变了。
2.2 键的哈希约束:为什么列表不能做键
字典在存储键值对时使用的是哈希表结构。查找时先对键做哈希运算,再根据哈希值定位到存储桶,因此键必须满足两个条件:可哈希(hashable)且相等性判断稳定。Python 中不可变类型(str、int、tuple、frozenset)默认可哈希,可变类型(list、dict、set)不可哈希。
# 反例: 列表不能作为字典的键 try: d = {["a", "b"]: 1} except TypeError as e: print("Error:", e) # 正例: 元组可以 d = {("a", "b"): 100}这里很多人会忽略一个细节:元组里如果包含了列表,这个元组依然不可哈希。哈希算法基于对象内容计算,内容又引用了可变对象,无法保证哈希值稳定。在实际开发中,一旦遇到TypeError: unhashable type,优先检查键是不是 list 或 dict,而不是急着加try/except——这类错误应该暴露出来。
2.3 常用构造与校验的边界情况参数表
| 操作 | 语法 | 典型返回值 | 注意事项 |
|---|---|---|---|
| 创建空字典 | {}或dict() | {} | {}是 dict,set()才是集合 |
| 带默认值创建 | dict.fromkeys(seq, val) | 新字典 | 可变默认值共享引用 |
| 由键值对列表转换 | dict(pairs) | 新字典 | pairs 内每个元素需为二元序列 |
| 键值互换 | {v: k for k, v in d.items()} | 新字典 | 原值不可哈希时报错 |
| 安全取数 | d.get("key", default) | 值或 default | 键不存在不抛异常 |
| 带默认写建 | d.setdefault("key", []) | 已存在的值 | 比 get 后再赋值少一次查询 |
3. 字典的核心操作:增删改查、视图对象与合并策略
3.1 增删改查的工程写法,避免 KeyError 的 3 个习惯
字典的基础操作很多人都会,但写业务代码时最容易踩的坑是频繁触发KeyError。三个实用的习惯能显著减少这类问题。
第一个习惯是使用setdefault替代先判断再赋值。统计单词出现次数的场景最典型:
word_count = {} words = ["apple", "banana", "apple", "orange", "banana", "apple"] for word in words: word_count[word] = word_count.get(word, 0) + 1 print(word_count)这里用get比if word not in word_count少一次哈希查找。第二个习惯是使用defaultdict处理嵌套结构,第三个是删除时用pop而不是先判断再del:
# pop 删除不存在的键时返回默认值,不会抛异常 data = {"a": 1, "b": 2} removed = data.pop("c", None) print(data, removed) # 连用多个 get 可以安全访问嵌套字典 nested = {"user": {"address": {"city": "北京"}}} city = nested.get("user", {}).get("address", {}).get("city", "未知") print(city)3.2 视图对象的动态特性:keys、values、items 不是快照
从 Python 3 开始,dict.keys()、dict.values()、dict.items()返回的是视图对象(view),而不是列表。视图的最大特点是动态的——字典变化时视图也会自动跟着变。
d = {"x": 1, "y": 2} view = d.keys() print(view) # dict_keys(['x', 'y']) d["z"] = 3 print(view) # dict_keys(['x', 'y', 'z']) —— 视图实时更新这个特性在内存检查、断言测试时很有用,但也带来性能陷阱:如果循环遍历视图的同时修改字典大小,会抛出RuntimeError: dictionary changed size during iteration。正确做法是先转成列表再遍历。
3.3 字典合并的三种方式:update、解包与 | 运算符
合并字典在 Python 3.9 之前主要靠update方法和**解包,3.9 之后引入了|运算符,代码更简洁但使用场景有差别。
d1 = {"a": 1, "b": 2} d2 = {"b": 3, "c": 4} # 方法一: update 原地修改 d1.update(d2) print("update:", d1) # 方法二: 解包创建新字典 d3 = {**d1, **d2} print("unpack:", d3) # 方法三: | 运算符(Python 3.9+) d4 = d1 | d2 print("pipe:", d4)三种方式的区别在于:update修改原字典,返回None;解包和|都生成新字典,但|的可读性更好。另一个细节是键冲突时后者覆盖前者,覆盖顺序是从右往左,即右侧字典的值胜出。如果做配置合并且需要保留左侧值,就必须手动写循环或使用{**d2, **d1}调换顺序。
3.4 嵌套字典的安全写入与缺失键处理
处理嵌套字典时常见的需求是“多级键不存在则创建”,常见做法是循环判断,更 Pythonic 的写法是配合defaultdict递归构造:
from collections import defaultdict def recursive_dict(): return defaultdict(recursive_dict) config = recursive_dict() config["server"]["host"] = "127.0.0.1" config["server"]["port"] = 8080 print(config) print(dict(config))这里recursive_dict作为defaultdict的默认工厂,每次访问不存在的键时自动创建下一层defaultdict,省去了逐层setdefault的重复代码。但要注意,defaultdict在转换为普通字典时需要手动递归转换,否则序列化到 JSON 时会报错。深层嵌套的数据用这种方式构建非常顺手,但在逻辑复杂度上会使代码的隐式行为增加,使用时建议在函数内部封装。
4. 字典在数据统计、结构转换与性能边界上的真实表现
4.1 用字典完成分组统计与品种计数的完整示例
字典最典型的应用是分组统计和计数。下面是从数据库查询结果中按类别分组的常见写法:
from collections import defaultdict records = [ {"name": "商品A", "category": "电子", "price": 1999}, {"name": "商品B", "category": "食品", "price": 50}, {"name": "商品C", "category": "电子", "price": 3299}, {"name": "商品D", "category": "图书", "price": 89}, {"name": "商品E", "category": "食品", "price": 120}, ] grouped = defaultdict(list) for rec in records: grouped[rec["category"]].append(rec["name"]) print(dict(grouped))配合sum、min、max还可以继续做聚合运算。另一个高频场景是把两个列表快速映射为字典:dict(zip(keys, values))。需要注意长度不同时zip会截断到短列表的长度,如果需要完整映射,用zip_longest。这类题目在 Python 字典题目练习中非常常见,掌握了核心思路后,无论题目包装成什么样,本质上都是在考察键的构造和值的聚合逻辑。
4.2 字典 vs 列表 vs 字典树:查询复杂度与适用场景对比
字典使用哈希表实现,平均时间复杂度为 O(1) 的插入和查询;列表的按值查找是 O(n);字典树(Trie)的前缀查询是 O(m),m 为键的长度。这是三个不同维度的数据结构,放在一起比较时容易混淆。
| 操作场景 | 字典 dict | 列表 list | 字典树 Trie |
|---|---|---|---|
| 单键精确查找 | O(1) 平均 | O(n) | O(m) |
| 前缀匹配 | 不支持 | 不支持 | O(m) |
| 有序遍历 | 不保证顺序 | 支持 | 按字典序 |
| 内存占用 | 较低 | 最低 | 较高,节点指针开销 |
| 适用场景 | 键值映射 | 顺序存储 | 字符串前缀查询、输入提示 |
由此可以得到一个明确结论:如果业务只需要精确查找键值对,不要用字典树;反之,如果需要做前缀联想、自动补全、敏感词匹配这类需求,单纯依赖字典无法实现,需要引入额外的数据结构。相关热搜中的“字典树”和“vba 字典”虽然名字里都带“字典”,但前者是树形结构,后者是 Visual Basic for Applications 中的Scripting.Dictionary对象,与 Python dict 的线程安全性和方法集都不同,不要混为一谈。
4.3 哈希碰撞与 Python 字典的扩容机制对性能的影响
Python 字典的高效性建立在哈希函数分布均匀的假设上,但极端情况下大量键映射到同一存储桶,查找会退化为 O(n)。Python 的字符串哈希引入了随机盐值(PYTHONHASHSEED),每次进程启动时哈希种子不同,这是为了防御哈希碰撞攻击,所以不要假设字典遍历顺序在多次运行之间保持一致——虽然 Python 3.7 起字典保序是语言规范,但保序不等于哈希值稳定。
扩容方面,Python 字典的负载因子约为 2/3,当存储桶占用超过这个比例时会触发扩容,而扩容涉及重新计算所有键的存储位置,此时单次插入性能会短暂下降。在需要提前插入大量数据时,直接构造完整字典比多次逐步插入更高效,因为减少了扩容次数。
# 批量构造 vs 循环插入 import time keys = [f"key_{i}" for i in range(200_000)] values = range(200_000) # 方式一: 整体构造 start = time.perf_counter() d = dict(zip(keys, values)) print("构造函数耗时:", time.perf_counter() - start) # 方式二: 逐步插入 d2 = {} start = time.perf_counter() for k, v in zip(keys, values): d2[k] = v print("循环插入耗时:", time.perf_counter() - start)运行结果的差异在十余万量级时可能不明显,但在百万级数据时能拉开差距。如果场景中键值对数量巨大且内存敏感,可以考虑改用sqlite3或磁盘索引,避免将所有内容装载进内存。这里的核心思想是:不是所有数据都适合放字典。
4.4 字典到 JSON 的相互转换与中文编码处理
json模块与字典的互转是接口开发中最常见的操作。json.dumps默认会把中文转成\uXXXX形式,这在调试时极不方便,设置ensure_ascii=False即可显示原始中文。时间对象、自定义对象不能直接序列化,需要编写默认转换函数:
import json from datetime import datetime data = {"name": "测试", "time": datetime.now(), "count": 42} def default_serializer(obj): if isinstance(obj, datetime): return obj.strftime("%Y-%m-%d %H:%M:%S") raise TypeError(f"Type {type(obj)} not serializable") json_str = json.dumps(data, ensure_ascii=False, default=default_serializer) print(json_str)反序列化时值得注意的一个参数是object_hook,它允许在解析完成前对每个子字典做处理,常用于把 ISO 格式的时间字符串自动转换为datetime对象。这在处理复杂的嵌套接口返回值时能省掉大量遍历代码。
5. 字典排序、键值互换与自定义对象的哈希设计
5.1 按值排序以及多字段排序的正确姿势
字典本身是无序的(虽然保序),需要排序时sorted函数配合key参数即可。按值排序有两种常见写法:
scores = {"Alice": 88, "Bob": 75, "Charlie": 95, "David": 75} # 按值升序 sorted_asc = dict(sorted(scores.items(), key=lambda item: item[1])) # 按值降序 sorted_desc = dict(sorted(scores.items(), key=lambda item: item[1], reverse=True)) # 先按值降序,再按键名排序 sorted_multi = dict(sorted(scores.items(), key=lambda item: (-item[1], item[0]))) print(sorted_asc) print(sorted_desc) print(sorted_multi)最后一个例子中-item[1]配合reverse=False实现了“值大优先、同名按字母序”的效果。排序后转回dict是因为sorted返回列表,列表转字典在 Python 3.7+ 可以保持插入顺序。需要注意的是,如果值里有None,sorted会因无法比较而报错,需要先过滤或填充默认值。
5.2 键值互换时如何解决值重复导致的丢失问题
简单使用{v: k for k, v in d.items()}做键值互换存在一个缺陷:值重复时后面的键覆盖前面的键。如果需要保留全部关系,应该把相同值对应的键收集到列表:
d = {"a": 1, "b": 2, "c": 1, "d": 2} # 简单互换会丢失 lossy = {v: k for k, v in d.items()} print(lossy) # {1: 'c', 2: 'd'} # 使用 setdefault 保留全部 inverted = {} for k, v in d.items(): inverted.setdefault(v, []).append(k) print(inverted) # {1: ['a', 'c'], 2: ['b', 'd']}如果原字典的键和值都保证唯一,简单互换写法没有问题;一旦数据来自外部接口或数据库,重复值几乎必然出现,使用第二种写法更稳妥。此外,值本身必须可哈希才能作为新字典的键,若值包含列表或字典,需要先做转换,否则报错。
5.3 自定义对象作为字典键:重写 __hash__ 和 __eq__ 的边界
内置类型做键安全可靠,但业务中偶尔需要把自定义对象作为字典键使用。此时必须同时重写__hash__和__eq__,因为两个对象a == b为真时,hash(a)必须等于hash(b),否则字典会出现逻辑上相同的键被存储两次。
class Person: def __init__(self, id_card, name): self.id_card = id_card self.name = name def __hash__(self): return hash(self.id_card) def __eq__(self, other): if not isinstance(other, Person): return NotImplemented return self.id_card == other.id_card def __repr__(self): return f"Person({self.name})" p1 = Person("110101", "张三") p2 = Person("110101", "李四") d = {p1: "用户记录"} print(d[p2]) # 输出: 用户记录这里p1和p2虽然是两个对象,但身份证号相同,字典把它们视为同一个键。设计__hash__时只应纳入不可变属性,如果哈希涉及可变属性,对象存进字典后再修改该属性,字典将无法正常查找这个键。
5.4 用__missing__扩展字典默认行为实现缓存
自定义字典子类可以通过__missing__魔术方法控制在键不存在时的行为。defaultdict内部就是基于这个机制实现的。手动实现可以做出更精细的控制,比如带过期时间的缓存字典:
import time class CacheDict(dict): def __init__(self, expire_seconds): super().__init__() self.expire_seconds = expire_seconds self._timestamps = {} def __missing__(self, key): return None def set(self, key, value): self[key] = value self._timestamps[key] = time.time() def get(self, key, default=None): if key in self: if time.time() - self._timestamps[key] < self.expire_seconds: return super().get(key) else: # 过期则删除 super().__delitem__(key) del self._timestamps[key] return default cache = CacheDict(expire_seconds=3) cache.set("weather", "晴") print(cache.get("weather")) time.sleep(4) print(cache.get("weather"))这种设计适合本地轻量缓存,不需要引入 Redis 等外部组件。但要注意它不具备线程安全性,多线程场景需要加锁。真正高并发的场景下应该直接用functools.lru_cache或cachetools库,它们已经处理了淘汰策略和线程安全。
5.5 字典推导式的条件过滤与运行效率验证
字典推导式在数据清洗中非常高效,可以一行完成过滤和映射。下面的示例从原始数据中筛选价格高于 100 的商品并保留分类信息:
products = { "商品A": {"price": 89, "stock": 20}, "商品B": {"price": 259, "stock": 5}, "商品C": {"price": 1500, "stock": 2}, "商品D": {"price": 49, "stock": 100}, } filtered = { name: info for name, info in products.items() if info["price"] > 100 and info["stock"] > 0 } print(filtered)推导式的性能优势来自 Python 解释器对其做了单独的循环优化,通常比等价的for循环赋值快 10% 到 20%。但推导式的嵌套循环过多时可读性急剧下降,最多嵌套两层。超过两层,函数封装更合适。验证性能时可以用timeit模块做对比,趋势是三层以上循环中推导式的性能优势基本被语法解析开销抵消。
合理使用字段过滤的推导式可以让代码保持声明式风格,在业务逻辑中一眼看出过滤条件。字典本身内容一旦复杂,建议配合类型注解与TypedDict使用,让 IDE 的静态检查和代码提示发挥作用,这是大项目里容易忽略的实践细节。
本文还有配套的精品资源,点击获取