你写 Python 写到稍微真实一点的程序,很快就会遇到这种场面:一本书有书名、作者、价格和库存,一个学生有姓名、班级和成绩,一次请求有路径、状态码和耗时。把这些数据塞进列表当然也能做,但你得记住“第 0 个位置是书名,第 1 个位置是作者”,代码一长,自己都容易拿错。
字典解决的正是“我不想记位置,我想按名字取数据”这个问题。它把一个键和一个值配成一组。你拿出键,Python 就去找与它对应的值:
book = {
"title": "Python 入门手册",
"author": "林晓",
"price": 59.0,
"in_stock": True,
}
print(book["title"])
print(book["price"])Python 入门手册
59.0刚学字典时,真正容易卡住的往往不是花括号怎么写,而是这些细节:为什么列表不能当键?book["isbn"] 和 book.get("isbn") 到底该选谁?复制一份字典以后,为什么里面的列表还会一起变化?这一章会围绕这些实际问题,把字典从“会写”讲到“用得稳”。

字典通过键定位相应的值,每组键和值保持清楚的一一对应。
列表按连续的位置编号保存元素,字典则按你提供的键保存值。下面两个对象虽然都装着三项数据,取数据时的思路完全不同:
book_list = ["Python 入门手册", 59.0, 12]
book_dict = {"title": "Python 入门手册", "price": 59.0, "stock": 12}
print(book_list[1]) # 记住 1 表示价格
print(book_dict["price"]) # 直接说我要价格字典里的每一项叫一个键值对。在 "price": 59.0 中,"price" 是键,59.0 是值。一个字典里的键不能重复,因为同一把键不能同时指向两份不同的数据;值可以重复,也几乎可以是任何 Python 对象。
course = {
"name": "Python 基础",
"teacher": "周老师",
"tags": ["编程", "入门"],
"progress": {"done": 8, "total": 20},
}这里的值既有字符串,也有列表和另一个字典。字典不是要求所有值同一种类型的“表格列”,它更像一份有字段名的记录。
判断该不该用字典,可以先问一句:程序是不是经常要根据某个名称、编号或标识找到对应信息?如果答案是“是”,字典通常很合适。如果数据只需要按先后位置处理,列表往往更自然。
你如果在字典字面量里写了两个相等的键,最终只会留下后面的值:
prices = {
"notebook": 12.5,
"pen": 3.0,
"notebook": 10.0,
}
print(prices){'notebook': 10.0, 'pen': 3.0}这不会报“键重复”的错误,所以配置项、字段名较多时要格外留意。重复键有时是有意覆盖,更多时候只是拼写和复制导致的疏忽。
最常见的写法是把键值对放进一对花括号。代码短、内容固定时,这种写法最清楚:
user = {
"id": 1007,
"name": "小雨",
"city": "杭州",
}如果数据要在程序运行中一点点收集,可以先建空字典,再逐项放进去:
profile = {}
profile["name"] = input("姓名:")
profile["city"] = input("城市:")
profile["skills"] = []注意,空字典是 {},空集合才写 set()。这是 Python 初学者很常混淆的一对写法。
dict() 接收键值对dict() 可以接收一个“每项恰好包含两个元素”的可迭代对象,第一个元素当键,第二个元素当值:
pairs = [("host", "localhost"), ("port", 8000), ("debug", True)]
config = dict(pairs)
print(config)当键和值原本分开保存时,可以先用 zip() 配对:
fields = ["name", "score", "passed"]
values = ["陈晨", 92, True]
student = dict(zip(fields, values))
print(student)zip() 会在较短的输入结束时停止。如果字段有三个、值只有两个,第三个字段会悄悄缺失。对外部数据做转换时,最好先检查长度:
if len(fields) != len(values):
raise ValueError("字段数量和值的数量不一致")键是简单标识符时,还可以写成关键字参数:
server = dict(host="127.0.0.1", port=8000, debug=False)这种写法的键会自动变成字符串,但它不能直接表示 "user-name"、1、("x", "y") 这类键。遇到这些键,还是用字面量或键值对序列。
字典查找之所以快,是因为 Python 会根据键的哈希值定位候选位置,再用相等比较确认是不是那把键。你不需要背实现细节,但需要记住一个约束:字典键在存放期间,参与哈希和相等判断的内容必须稳定。
满足这个约束的对象叫可哈希对象。字符串、整数、浮点数、布尔值通常都可以当键:
lookup = {
"admin": "管理员",
404: "页面不存在",
(31.23, 121.47): "上海附近",
}元组能不能当键,要看它里面的每个元素。只含可哈希元素的元组可以;只要里面藏着列表等不可哈希对象,整个元组就不能当键:
ok = {(2026, 8, 13): "发布日期"}
# TypeError: unhashable type: 'list'
bad = {("tags", ["python", "dict"]): "示例"}
稳定且可哈希的对象才能当键,列表和字典会变化,不能直接作为键。
列表、字典和普通集合会原地改变,通常不能当键。冻结集合 frozenset 不可变,其中的元素也可哈希时,它可以当键:
permissions = {
frozenset({"read", "write"}): "编辑者",
frozenset({"read"}): "访客",
}你可以用 hash() 做一次快速试探。能返回整数,说明这个对象提供了哈希;抛出 TypeError,说明它不能直接当普通字典的键:
print(hash("order-1001"))
print(hash((10, 20)))
# print(hash([10, 20])) # TypeError“键必须不可变”是方便初学者记忆的近似说法,更准确的标准是“键必须可哈希”。元组本身不可变,但元组里的列表仍会变,所以这种元组不可哈希。反过来,用户自定义对象能不能当键,还取决于类如何定义相等与哈希行为。
Python 中,如果两个键比较相等,并且哈希也相同,字典会把它们视为同一个键。True == 1,1 == 1.0,因此下面的字典最后只有一项:
flags = {}
flags[True] = "布尔值"
flags[1] = "整数"
flags[1.0] = "浮点数"
print(flags)
print(len(flags)){True: '浮点数'}
1这类混用很容易让人误判数据量。实际项目里,同一类业务键尽量保持同一种类型,例如订单编号统一用字符串,别一会儿用 1001,一会儿用 "1001"。
方括号取值表达的是一种很明确的态度:这个键按程序设计就应该存在。
order = {"id": "A1024", "amount": 268.0, "paid": False}
print(order["id"])
print(order["amount"])键不存在时,方括号会抛出 KeyError:
print(order["coupon"])KeyError: 'coupon'先别急着把所有方括号都换成 get()。KeyError 有时正是在提醒你:数据不符合程序约定,字段名可能拼错了,上游可能漏传了必填字段。让错误尽早出现,通常比带着坏数据继续运行更容易排查。
get() 适合本来就可能缺少的键优惠券、备注、头像这些字段可能没有,用 get() 会更自然:
order = {"id": "A1024", "amount": 268.0}
coupon = order.get("coupon")
shipping_fee = order.get("shipping_fee", 0)
print(coupon) # None
print(shipping_fee) # 0get(key, default) 找到键就返回原值,找不到就返回默认值,而且不会把这个默认值写进字典。
这里有个很隐蔽的坑:不传第二个参数时,缺少键会返回 None;可如果字典真的保存了 None,你只看返回结果就分不清“键不存在”还是“键存在但值是 None”。
profile = {"nickname": None}
print(profile.get("nickname")) # None
print(profile.get("avatar")) # None需要区分这两种状态时,用 in,或者准备一个绝不可能出现在业务数据里的哨兵对象:
missing = object()
value = profile.get("avatar", missing)
if value is missing:
print("没有 avatar 这个键")
else:
print(f"avatar 的值是 {value!r}")
键不存在时,方括号查找会触发键错误,带默认值查找可以平稳返回默认值。
in 默认检查键,不检查值settings = {"theme": "dark", "language": "zh-CN"}
print("theme" in settings) # True
print("dark" in settings) # False
print("dark" in settings.values()) # True写 key in d 就是在查键,通常不用多写成 key in d.keys()。如果你真要查值,明确写 value in d.values(),同时也要想一想:值反查是不是很频繁?如果是,可能应该另外建立“值到键”的索引字典。
先判断这个字段是不是必填。订单编号、用户名这类必填字段直接用方括号,缺少时让 KeyError 暴露数据问题。
如果字段允许缺省,而且缺省时有自然值,就使用 get(),例如 cart.get("discount", 0)。
如果“键不存在”和“值恰好是 None”代表不同状态,用 in 或唯一哨兵判断,不要只靠 get() 的返回值猜。
字典是可变对象。方括号放在赋值号左边时,键不存在就是新增,键存在就是修改:
cart = {"apple": 2, "milk": 1}
cart["bread"] = 1 # 新增
cart["apple"] = 4 # 修改
print(cart)这套规则很方便,但也意味着拼错键名不会报错,而是创建一个新键:
user = {"email": "lin@example.com"}
user["emali"] = "new@example.com" # 拼错了,却能正常执行
print(user)当你只允许修改已有字段时,可以先检查:
field = "email"
if field not in user:
raise KeyError(f"不允许创建未知字段:{field}")
user[field] = "new@example.com"del、pop() 与 popitem() 的差别只想删除,不关心旧值,可以用 del。键不存在时它会抛 KeyError:
session = {"user_id": 17, "token": "abc", "temporary": True}
del session["temporary"]既要删除又要拿到旧值,用 pop():
token = session.pop("token")
print(token)键可能不存在时,给 pop() 一个默认值:
note = session.pop("note", None)popitem() 不接收键,它会删除并返回最后插入的键值对。字典为空时会抛 KeyError:
tasks = {"download": "完成", "parse": "完成", "save": "等待"}
name, state = tasks.popitem()
print(name, state)
print(tasks)
字典支持添加、修改和删除,pop() 会在移除键值对的同时返回对应值。
如果要一次清空,使用 clear():
cache = {"page:1": "...", "page:2": "..."}
cache.clear()
print(cache) # {}del d[key] 和 d.pop(key) 在键不存在时都会报错;d.pop(key, default) 才会在缺少键时返回默认值。不要因为两者都能删除,就忽略了是否需要旧值、是否允许键缺失这两个条件。
下面的实验台把当前字典、执行结果和操作历史放在一起。先选一个键和值,再分别试试方括号读取、get()、增改、删除、pop()、setdefault() 和 update()。尤其可以故意输入一个不存在的键,观察严格读取和带默认值读取给出的不同反馈。
setdefault():取值和初始化一次完成分组时经常要做三件事:先看这个组有没有列表,没有就创建空列表,然后追加新元素。
groups = {}
city = "成都"
name = "小安"
if city not in groups:
groups[city] = []
groups[city].append(name)setdefault(key, default) 可以把“检查并初始化”合成一次操作:
groups = {}
groups.setdefault("成都", []).append("小安")
groups.setdefault("成都", []).append("小林")
groups.setdefault("西安", []).append("阿哲")
print(groups){'成都': ['小安', '小林'], '西安': ['阿哲']}键已存在时,setdefault() 返回已有值,不会覆盖它;键不存在时,它插入默认值,再把这个值返回。
config = {"timeout": 10}
first = config.setdefault("timeout", 30)
second = config.setdefault("retries", 3)
print(first) # 10
print(second) # 3
print(config) # {'timeout': 10, 'retries': 3}get() 和 setdefault() 最容易被混在一起。get() 只读,不会改变字典;setdefault() 可能新增键。只是想显示一个缺省值时用 get(),确实希望以后都复用这份容器时再用 setdefault()。
有一组新配置要覆盖旧配置时,不用逐个赋值,update() 会把传入的键值对写进原字典:
config = {
"host": "127.0.0.1",
"port": 8000,
"debug": False,
}
config.update({"port": 9000, "debug": True})
print(config)冲突键由后来的值覆盖,新键会追加进去。update() 也能接收键值对序列和关键字参数:
config.update([("timeout", 15), ("retries", 2)])
config.update(debug=False)一个很常见的错误是把返回值重新赋给变量:
config = {"debug": False}
result = config.update({"debug": True})
print(result) # None
print(config) # {'debug': True}update() 原地修改并返回 None。如果写成 config = config.update(...),config 就会变成 None。
| 创建新字典,|= 修改左边字典Python 3.9 起,两个字典可以用 | 合并:
defaults = {"theme": "light", "page_size": 20, "debug": False}
user_options = {"theme": "dark", "debug": True}
effective = defaults | user_options
print(effective)
print(defaults)effective 是新字典,冲突键由右侧 user_options 获胜,defaults 不变。顺序不能随便交换,因为 a | b 和 b | a 在冲突时结果可能不同。
|= 会修改左侧字典:
defaults |= user_options
print(defaults)还可以用 ** 解包创建新字典,这种写法在 Python 3.9 之前也常见:
effective = {**defaults, **user_options}普通字典合并只处理最外层。右侧的嵌套字典会整体覆盖左侧对应值:
base = {
"database": {"host": "db.local", "port": 5432},
"debug": False,
}
override = {
"database": {"host": "db.example.com"},
}
merged = base | override
print(merged){'database': {'host': 'db.example.com'}, 'debug': False}原来的 port 不会自动留下来。若业务需要“深层合并”,要明确写合并规则,例如:
merged = base | override
merged["database"] = base["database"] | override["database"]
字典推导式把筛选和变换写成构造流程,合并时还要留意同名键的覆盖结果。
通用深层合并还要处理列表、类型冲突和删除标记,并没有唯一正确的默认答案,所以 Python 的普通字典合并不会擅自替你决定。
现代 Python 的字典会保留插入顺序。遍历时,键值对按加入字典的先后出现:
stages = {}
stages["collect"] = "收集数据"
stages["clean"] = "清洗数据"
stages["report"] = "生成报告"
for stage in stages:
print(stage)collect
clean
report这项行为从 Python 3.7 起是语言层面的保证。更新已有键只改变值,不改变它的位置;删除后重新插入,会把它放到末尾:
scores = {"小林": 80, "小周": 91, "小陈": 86}
scores["小林"] = 88
print(list(scores))
del scores["小周"]
scores["小周"] = 93
print(list(scores))['小林', '小周', '小陈']
['小林', '小陈', '小周']保留插入顺序不等于自动按键或按值排序。要按键排序,请明确使用 sorted():
scores = {"小林": 88, "小周": 93, "小陈": 86}
for name in sorted(scores):
print(name, scores[name])按分数排序时,把规则写进 key:
for name, score in sorted(scores.items(), key=lambda item: item[1], reverse=True):
print(name, score)字典相等只看键值对是否相同,不看插入顺序:
a = {"x": 1, "y": 2}
b = {"y": 2, "x": 1}
print(a == b) # True如果想从最后插入的键开始遍历,Python 3.8 起可以使用 reversed(d):
for stage in reversed(stages):
print(stage)直接遍历字典,拿到的是键:
stock = {"苹果": 8, "牛奶": 3, "面包": 0}
for product in stock:
print(product)只需要值时用 values():
total_units = 0
for quantity in stock.values():
total_units += quantity
print(total_units)键和值都需要时,用 items() 最清楚:
for product, quantity in stock.items():
print(f"{product}:{quantity} 件")items() 每次产生一个 (键, 值) 二元组,for product, quantity 顺手把它拆成两个变量。
keys()、values()、items() 返回的是动态视图这三个方法返回的不是独立列表,而是原字典的视图。字典变了,已经拿到的视图也能看到变化:
prices = {"咖啡": 18, "茶": 12}
names = prices.keys()
print(list(names))
prices["果汁"] = 15
print(list(names))['咖啡', '茶']
['咖啡', '茶', '果汁']需要固定快照时,主动转成列表、元组或集合:
snapshot = list(prices.items())
prices["水"] = 3
print(snapshot) # 仍是创建快照时的内容键视图很像集合,可以做集合运算。下面找出默认配置和用户配置共同拥有的键:
defaults = {"theme": "light", "language": "zh-CN", "page_size": 20}
user_options = {"theme": "dark", "page_size": 50, "sidebar": False}
common_keys = defaults.keys() & user_options.keys()
print(common_keys)值视图不按集合处理,因为值可能重复,也可能不可哈希。不要依赖两个 values() 视图直接比较来判断值是否相同;根据目的转成列表或计数结构再比较。
下面的代码一边遍历,一边删除键,通常会抛出 RuntimeError: dictionary changed size during iteration:
stock = {"苹果": 8, "牛奶": 0, "面包": 0}
# 错误示范
for product, quantity in stock.items():
if quantity == 0:
del stock[product]需要删除时,先遍历一个快照,或者先收集待删除的键:
for product, quantity in list(stock.items()):
if quantity == 0:
del stock[product]另一种写法是用推导式创建新字典:
stock = {
product: quantity
for product, quantity in stock.items()
if quantity > 0
}下面的观察器会同时显示原字典、keys()、values()、items() 视图以及一份列表快照。尝试更新旧键、增加新键,再删除并重新插入一个键,你会直接看到哪些结果随原字典变化,哪些结果保持原样,以及插入顺序怎样改变。

键视图、值视图和键值对视图会随原字典变化,并可逐项遍历。
字典推导式的形状和列表推导式很像,只是最前面要写出 键: 值:
squares = {number: number ** 2 for number in range(1, 6)}
print(squares){1: 1, 2: 4, 3: 9, 4: 16, 5: 25}它很适合做“转换”和“筛选”。例如,把及格学生的名字统一处理,并保留整数分数:
raw_scores = {
" 小林 ": 88.5,
"小周": 53.0,
" 小陈": 91.5,
}
passed = {
name.strip(): round(score)
for name, score in raw_scores.items()
if score >= 60
}
print(passed)推导式不一定越短越好。如果键和值的计算都很复杂,或者过程里需要记录错误、打印日志,普通循环更容易读:
normalized = {}
for row in rows:
student_id = row["student_id"].strip()
if not student_id:
continue
normalized[student_id] = {
"name": row["name"].strip(),
"score": float(row["score"]),
}如果多个输入计算出了相同的键,后来的值会覆盖前面的值:
names = ["Python", "PYTHON", "python"]
normalized = {name.lower(): name for name in names}
print(normalized){'python': 'python'}这可能正是你想要的“最后一条记录优先”,也可能意味着数据被悄悄丢掉。只要键由清洗、截断、取整等操作生成,就要想想不同输入会不会撞成同一个键。
真实数据常常不止一层。一个班级可以用学号索引学生记录,每个学生记录里又有课程分数字典:
students = {
"S001": {
"name": "小林",
"city": "成都",
"scores": {"语文": 86, "数学": 92},
},
"S002": {
"name": "小周",
"city": "西安",
"scores": {"语文": 91, "数学":
读嵌套表达式时,不要一口气盯着所有括号。按层拆开:
student = students["S001"]
scores = student["scores"]
math_score = scores["数学"]
print(math_score)确认每一步拿到的是字典、列表还是普通值以后,再缩成一行也不迟:
math_score = students["S001"]["scores"]["数学"]字典里也常放列表,比如一个用户对应多个标签:
users = {
"u001": {"name": "小林", "tags": ["摄影", "徒步"]},
"u002": {"name": "小周", "tags": ["音乐"]},
}
users["u001"]["tags"].append("编程")很多人会写出这样的代码:
city = response.get("user").get("address").get("city")只要 user 或 address 不存在,前一个 get() 就返回 None,接着对 None 调用 .get(),程序仍然会报错。初学阶段,逐层判断更直白:
user = response.get("user")
if user is None:
city = None
else:
address = user.get("address")
city = None if address is None else address.get("city")如果数据约定缺省的 user 和 address 都可以视为空字典,也能写成:
city = response.get("user", {}).get("address", {}).get("city")这段短写法有前提:已有键的值必须真的是字典。若 "user": None,它仍会失败。所以别为了少写几行,掩盖数据类型不稳定的问题。
嵌套很深时,先考虑是不是一个字典承担了太多职责。频繁访问的数据可以单独建立索引,固定结构也可以封装成类或其他更明确的数据模型。字典很灵活,但灵活不等于所有层次都要塞进同一个对象。
这是字典里最值得提前避开的坑之一。把一个字典赋给另一个变量,并没有复制内容,只是给同一个对象多加了一个名字:
original = {"theme": "light", "font_size": 16}
alias = original
alias["theme"] = "dark"
print(original["theme"]) # dark
print(alias is original) # True想让外层字典分开,可以使用 copy():
original = {"theme": "light", "font_size": 16}
copied = original.copy()
copied["theme"] = "dark"
print(original["theme"]) # light
print(copied["theme"]) # dark但 dict.copy() 是浅复制。它只新建外层字典,里面的可变对象仍然是共享的:
original = {
"name": "小林",
"tags": ["Python", "数据分析"],
}
copied = original.copy()
copied["tags"].append("自动化")
print(original["tags"])
print(copied["tags"])两边都会出现 "自动化",因为两个外层字典里的 tags 指向同一个列表。
如果所有嵌套对象也要分开,可以根据数据结构手动复制需要修改的部分:
copied = original.copy()
copied["tags"] = original["tags"].copy()
copied["tags"].append("自动化")或者在确实需要完整独立副本时使用 copy.deepcopy():
from copy import deepcopy
copied = deepcopy(original)深复制会递归复制嵌套对象,成本更高,而且有些本该共享的对象并不需要复制。实际写程序时,明确复制哪一层通常更容易控制。
fromkeys() 配可变默认值会让所有键共享它下面这段看起来像给每个班级准备一份空名单:
groups = dict.fromkeys(["一班", "二班", "三班"], [])
groups["一班"].append("小林")
print(groups)结果三个班级都会出现 "小林"。fromkeys() 把同一个列表对象放到了每个键下面。需要各自独立的列表时,用字典推导式:
groups = {class_name: [] for class_name in ["一班", "二班", "三班"]}
groups["一班"].append("小林")
print(groups)字典真正好用的地方,不是保存一份孤零零的个人信息,而是把大量数据按某个规则组织起来。下面三个模式几乎会反复出现。

字典既能累计数量、把数据分组,也能通过唯一标识快速定位记录。
假设有一串搜索词,要统计每个词出现几次:
queries = ["字典", "列表", "字典", "循环", "字典", "列表"]
counts = {}
for query in queries:
counts[query] = counts.get(query, 0) + 1
print(counts)get(query, 0) 让第一次出现的词从 0 开始,之后每遇到一次就加 1。数据量大、还要取最常见项时,标准库的 Counter 更省事:
from collections import Counter
counts = Counter(queries)
print(counts.most_common(2))先掌握普通字典写法很有价值,因为你会看清“元素作键、次数作值”的模型;熟悉以后再用 Counter,就知道它替你省掉了哪一步。
要按城市给用户分组,可以让值变成列表:
users = [
{"name": "小林", "city": "成都"},
{"name": "小周", "city": "西安"},
{"name": "小安", "city": "成都"},
]
by_city = {}
for user in users:
city = user["city"
分组非常频繁时,可以用 defaultdict(list):
from collections import defaultdict
by_city = defaultdict(list)
for user in users:
by_city[user["city"]].append(user)普通字典配 setdefault() 更直观,defaultdict 则把“缺少键时创建列表”的规则写在容器上。两者选一个即可,不需要为了显得高级而同时套用。
接口和文件常给你“记录列表”,但业务代码需要按编号反复查找。每次从头扫描列表既麻烦又容易散落重复逻辑,可以先建索引:
products = [
{"sku": "P100", "name": "键盘", "price": 299},
{"sku": "P101", "name": "鼠标", "price": 129},
{"sku": "P102", "name": "显示器", "price": 1599},
]
by_sku
建立索引前要确认键真的唯一。如果两条商品记录用了相同 sku,后者会覆盖前者。希望把重复当错误时,边构建边检查:
by_sku = {}
for product in products:
sku = product["sku"]
if sku in by_sku:
raise ValueError(f"重复的商品编号:{sku}")
by_sku[sku] = product同一批记录可以按“出现次数”“类别分组”或“唯一编号索引”组织。下面的模拟器允许你切换三种模式并逐步观察字典怎样生长。重点看键和值的角色怎样随任务改变,而不只是最后打印出的花括号。
下面把创建、查找、更新、删除、遍历和统计放进同一个场景。我们收到几条库存变更记录,需要更新仓库数据,同时记录无法识别的商品。
inventory = {
"P100": {"name": "键盘", "stock": 8, "price": 299},
"P101": {"name": "鼠标", "stock": 15, "price": 129},
"P102": {"name": "显示器", "stock": 3, "price": 1599},
我们不直接写 inventory[change["sku"]],因为外部变更记录里可能出现未知编号。先用 get() 探测,再决定怎么处理:
从每条变更里读取必填的 sku 和 delta。这两个字段缺少就说明输入格式错误,因此使用方括号,让错误直接暴露。
用 inventory.get(sku) 查商品。商品编号来自外部,找不到是业务中允许出现的情况,不必让程序因 KeyError 终止。
找不到的编号追加到 unknown_skus;找到的商品则修改嵌套字典里的 stock。若库存将变成负数,先记录错误,不执行更新。
完整代码如下:
unknown_skus = []
rejected = []
for change in changes:
sku = change["sku"]
delta = change["delta"]
product = inventory.get(sku)
if product is None:
unknown_skus.append(sku)
continue
new_stock = product["stock"] + delta
if new_stock
这段代码里,字典不是孤立的语法练习。每一种读取方式都在表达业务约定:输入记录的 sku 必须存在,所以用方括号;sku 在库存里可能找不到,所以用 get();删除下架商品还要拿到名称,所以用 pop()。把“允许什么、缺少时怎么办”写清楚,代码自然就稳了。
KeyError 时先看实际键最常见的原因不是字典坏了,而是键拼错、大小写不同或含有空格:
row = {"student_id": "S001", "score": 88}
print(row.keys())
print(repr("student_id "))外部 CSV 字段可能是 "student_id ",用户输入可能是 " S001"。清洗应当发生在明确的边界处,别在程序各处临时猜测:
cleaned = {key.strip(): value for key, value in row.items()}in 查到了,取值却失败检查和取值如果用了不同形式的键,就会出现这种错觉:
data = {1001: "已支付"}
print(1001 in data) # True
# print(data["1001"]) # KeyError整数 1001 和字符串 "1001" 是两把不同的键。业务编号最好在进入系统时就统一类型。
依次检查三件事:是不是只做了 b = a;是不是只浅复制了外层;嵌套列表或字典是不是仍由两边共享。用 is 可以检查对象身份:
print(copy_a is original)
print(copy_a["tags"] is original["tags"])先检查冲突键由哪一侧覆盖,再看它是不是嵌套字典。base | override 是右侧优先的浅层合并,不会递归保留嵌套字段。
RuntimeError看看循环体里有没有增加或删除键。要改变字典大小,遍历 list(d) 或 list(d.items()) 的快照,或者先收集要改动的键,循环结束后统一处理。
调试字典时,最有效的动作通常很朴素:打印 type(key)、repr(key)、list(d.keys()),再确认这一行是在读取、写入还是删除。键的类型、空格和数据约定一旦对上,大多数问题都会很快露出来。
有一份用户配置:
settings = {
"nickname": None,
"notifications": False,
}请写代码分别判断:nickname 键存在但值为 None;language 键完全不存在。不能只比较 get() 的结果。
下面的代码为什么不稳?请改成不会在遍历时改变原字典大小的版本。
stock = {"苹果": 3, "牛奶": 0, "面包": 0, "咖啡": 5}
for name, quantity in stock.items():
if quantity == 0:
del stock[name]给定记录列表,请按 id 建立索引;如果出现重复 id,抛出 ValueError,不要让后者悄悄覆盖前者。
records = [
{"id": "A01", "name": "小林"},
{"id": "A02", "name": "小周"},
{"id": "A01", "name": "小安"},
]学完这一章,你不需要把所有方法背成表格。先记住四个判断:键是否应该存在、缺少时要不要写入默认值、操作要不要修改原字典、嵌套值是不是共享对象。遇到具体任务时沿着这四个问题往下走,方括号、get()、setdefault()、update()、复制和遍历的选择就不会乱。
全部处理完成后,用字典推导式筛出零库存商品,再从库存字典中删除。删除过程遍历的是独立列表,避免遍历原字典时改变大小。