Skip to content
This page has been auto-translated and may contain errors.View in English

字典

列表让你通过位置查找内容。但很多时候你希望通过名称查找。不是"给我第 3 项",而是"给我小明的分数"。字典将数据存储为键值对:你通过键来查找值,而不是位置。

当列表的位置索引没有意义时,字典就是合适的结构。字典将任意键映射到值,以 O(1) 时间提供命名查找。排行榜、JSON 响应、配置文件:这些都自然地表达为键值映射。

dict 是一个基于哈希表的键值存储,平均查找、插入和删除均为 O(1)。键必须是可哈希的;值可以是任何对象。从 Python 3.7 起,字典保留插入顺序。dict 是 Python 命名空间、对象 __dict__ 属性和关键字参数的基础。

创建字典

花括号,每个键和值之间用冒号分隔,键值对之间用逗号分隔。键几乎总是字符串。值可以是任何东西:数字、字符串、其他列表,甚至其他字典。

字典字面量使用花括号和 key: value 语法。键可以是任何不可变(可哈希)类型:字符串、整数、元组。值可以是任何 Python 对象。字典保留插入顺序,因此遍历时,你会按添加顺序获取项。

字典字面量从左到右求值。键必须是可哈希的:strinttuple 可以;listdict 不行。值没有限制。从 Python 3.7 起保证插入顺序(自 3.6 起作为紧凑哈希表实现)。字面量中重复的键会静默使用最后一个值。

python
player = {
    "name":  "小明",
    "score": 87,
    "level": 5,
    "alive": True,
}

访问值

使用方括号加键来获取值。如果键不存在,Python 会抛出 KeyError。当你不确定键是否存在时,使用 .get():它返回 None 而不是崩溃,或者你指定的默认值。

方括号访问在键缺失时会抛出 KeyError.get(key) 在键缺失时返回 None.get(key, default) 改为返回默认值。每当键的存在不确定时,就使用 .get();它比用 try/except 包装访问更安全、更易读。

d[key] 调用 __getitem__,它对键进行哈希并探查表:平均 O(1)。键缺失时抛出 KeyError.get(key, default=None) 执行相同的探查,但在键缺失时返回默认值而不是抛异常。key in d 检查(调用 __contains__)是 O(1) 的,是访问前进行守卫的惯用方式。

python
player = {"name": "小明", "score": 87}

player["name"]    # "小明"
player["score"]   # 87
player["lives"]   # KeyError (key doesn't exist)
python
player.get("score")          # 87
player.get("lives")          # None (no error, returns None by default)
player.get("lives", 3)       # 3   (use this default if key is absent)

每当键可能缺失时,.get() 更安全:

python
count = inventory.get("arrows", 0)   # 0 if "arrows" isn't in the dict

添加和更新

用方括号给键赋值。如果键已存在,值被替换。如果还不存在,则创建一个新条目。使用 .update() 一次性合并整个其他字典。

对键赋值会调用 __setitem__:平均 O(1),创建或替换。.update() 接受另一个字典或键值对的可迭代对象,并对每个条目调用 __setitem__,覆盖现有的键。

d[key] = value 调用 __setitem__,它对键进行哈希并在表中插入或覆盖:平均 O(1)。.update(other) 等同于重复调用 __setitem__| 运算符(Python 3.9+)在不修改原字典的情况下合并字典并返回一个新字典;|= 就地修改。

python
player = {"name": "小明", "score": 87}

player["score"] = 92        # update existing
player["level"] = 5         # add new key
python
extras = {"level": 5, "alive": True}
player.update(extras)   # adds/overwrites with keys from extras

移除项

有四种方式可以移除条目。.pop() 移除一个键并把值返回给你。带默认值的 .pop() 在键可能不存在时是安全的。del 移除一个键且没有返回值。.clear() 清空整个字典。

.pop(key) 在键缺失时抛出 KeyError.pop(key, default) 改为返回默认值,使它成为安全的移除方法。del d[key] 调用 __delitem__,在键缺失时抛出 KeyError.clear() 移除所有条目,但保留字典对象本身。

.pop(key, default) 是单次哈希探查:平均 O(1)。del d[key] 调用 __delitem__,同样的探查,在键缺失时抛异常。移除后,哈希表可能会缩小以释放内存。.clear() 重置表大小。在循环中遍历字典并同时修改它会抛 RuntimeError;先构建一个要移除的键的列表。

python
player = {"name": "小明", "score": 87, "level": 5}

player.pop("level")            # removes "level" and returns 5
player.pop("lives", None)      # safe pop, returns None if key absent
del player["score"]            # removes "score", no return value
player.clear()                 # removes everything

带默认值的 .pop() 是移除一个可能不存在的键的最安全方式。

遍历

三种视图让你能遍历字典的不同部分。仅遍历字典会给你键。.values() 给值。.items() 同时给出两者,这是你最常用的:将每对解包到两个名字中,以获得简洁、易读的循环。

.keys().values().items() 返回视图对象,不是列表。视图动态反映字典的当前状态:如果你修改字典,视图会立即更新。.items() 对于大多数循环最有用,因为元组解包 for k, v in d.items() 读起来很清晰。

.keys().values().items() 返回 dict_keysdict_valuesdict_items 视图对象。视图是惰性的:它们不复制数据,并在底层字典变化时更新。dict_keys 支持集合代数(&|-),因为键是唯一且可哈希的。在遍历期间修改字典会抛 RuntimeError;如果需要,使用 list(d.items()) 来快照。

python
player = {"name": "小明", "score": 87, "level": 5}

for key in player:               # iterate keys (most common)
    print(key)

for key in player.keys():        # same, explicit keys view
    print(key)

for value in player.values():    # just the values
    print(value)

for key, value in player.items():   # both, most useful
    print(f"{key}: {value}")

.items() 是你最常用的。将每对解包到两个名字中使循环可读性强。

检查成员

in 检查键是否存在于字典中。它不检查值,只检查键。要检查某项是否不存在,使用 not in

innot in 调用 __contains__,对字典而言是 O(1)。它只检查键。要检查值,你需要使用 in d.values(),但那是 O(n) 的,因为值没有被索引。

key in d 调用 dict.__contains__,它对键进行哈希并探查表:平均 O(1)。value in d.values() 遍历值视图:O(n)。这种不对称性是优先用字典键进行查找,而非扫描值的核心原因。

python
player = {"name": "小明", "score": 87}

"name"  in player      # True
"lives" in player      # False
"lives" not in player  # True

in 只检查键。要检查值,使用 in player.values(),虽然这种情况很少需要。

嵌套字典

值本身也可以是字典。这就是你如何表示多层级结构化数据:一个有 stats 部分的玩家,一个有子部分的配置文件。两组方括号访问嵌套值:第一个选择外层键,第二个选择内层键。

嵌套字典是值本身就是字典的字典。用链式下标访问。修改内部字典会影响外部字典,因为外部字典持有对同一对象的引用。尽可能保持嵌套浅:深度嵌套很快变得难以阅读和导航。

嵌套字典存储对象引用,而非副本。外层字典的浅拷贝(d.copy())不会复制内部字典;对内部字典的修改在原字典和拷贝中都可见。对于深层嵌套结构,copy.deepcopy() 创建完全独立的副本。链式 __getitem__ 调用每次都是 O(1),所以访问深度没有渐近成本。

python
users = {
    "小明": {"score": 87, "level": 5},
    "小红": {"score": 74, "level": 3},
}

users["小明"]["score"]   # 87
users["小红"]["level"]   # 3

用链式方括号访问。对于深度嵌套结构,这可能变得笨重,所以尽可能保持嵌套浅。

setdefault

.setdefault() 如果键存在则读取它,如果不存在则将其设置为默认值,然后返回该值。当你需要一个键存在但又不想在它已经存在时覆盖它时,这很有用。

.setdefault(key, default) 是一个原子的读取或创建:如果键存在,返回其当前值而不做任何修改;如果不存在,插入默认值并返回它。常见用例是在不单独进行存在性检查的情况下构建分组结构。

.setdefault(key, default) 是单次哈希探查:平均 O(1)。如果键缺失,插入并返回 default。如果存在,返回现有值并忽略 default(检查后从不求值)。对于常见的"将项分组到列表中"模式,这是在追加前检查 key in d 的标准替代方案。

python
inventory = {}

inventory.setdefault("arrows", 0)    # sets "arrows": 0, returns 0
inventory.setdefault("arrows", 10)   # "arrows" already exists, no change, returns 0

它对于构建分组结构而无需先检查键是否存在很有用:

python
groups = {}

for name, team in players:
    groups.setdefault(team, []).append(name)

collections.defaultdict 和 Counter

标准库有两个 dict 子类,可以自动处理常见模式。defaultdict 为缺失的键创建一个默认值,所以你永远不会得到 KeyErrorCounter 计算序列中每项出现的频率,并以字典形式给你结果。

defaultdict 接受一个为新键产生默认值的可调用对象,消除了对 .setdefault() 的需求。Counter 是一个用于频率计数的专门字典,带有 .most_common() 方法。两者都是 dict 子类,所以所有标准 dict 操作都适用于它们。

defaultdict.__missing__ 调用工厂并存储结果,使其在常见情况下线程安全。Counterdict 的子类,添加了 .most_common(n)(通过 heapq 实现为 O(n log n))、.subtract() 和用于合并计数的算术运算符。两者都在 collections 中;导入将在模块章节中介绍。

collections 导入

defaultdictCounter 需要从标准库导入。导入将在模块章节中介绍。

python
from collections import defaultdict

groups = defaultdict(list)
for name, team in players:
    groups[team].append(name)   # no KeyError if team is new
python
from collections import Counter

words  = ["cat", "dog", "cat", "bird", "cat", "dog"]
counts = Counter(words)
# Counter({'cat': 3, 'dog': 2, 'bird': 1})

counts.most_common(2)   # [('cat', 3), ('dog', 2)]

Counter 节省了大量"在循环中计数"的样板代码。

实战

构建一个分数追踪器并打印包含所有条目的摘要:

python
scores = {"小明": 87, "小红": 74, "小华": 92, "小刚": 55}

total   = sum(scores.values())
average = total / len(scores)

print(f"Players:  {len(scores)}")
print(f"Average:  {average:.1f}")
print(f"Highest:  {max(scores.values())}")
print(f"Lowest:   {min(scores.values())}")
print()

for name, score in scores.items():
    print(f"  {name}: {score}")

在循环中构建每个文件结果的字典,然后汇总所有条目:

python
job_results = {}
files       = ["report_jan.csv", "report_feb.csv", "report_mar.csv"]

for filename in files:
    size = len(filename) * 100   # placeholder for real file size
    if size < 2000:
        status = "ok"
    else:
        status = "large"
    job_results[filename] = {"size": size, "status": status}

ok_count    = 0
large_count = 0

for result in job_results.values():
    if result["status"] == "ok":
        ok_count += 1
    else:
        large_count += 1

print(f"Processed {len(job_results)} file(s): {ok_count} ok, {large_count} large")

通过遍历必填字段来验证嵌套请求字典,然后就地规范化特征重要性字典:

python
request = {
    "method":  "POST",
    "path":    "/users",
    "headers": {"Content-Type": "application/json"},
    "body":    {"username": "小明", "email": "[email protected]"},
}

body   = request["body"]
errors = []

for field in ["username", "email"]:
    if not body.get(field):
        errors.append(f"Missing required field: {field}")

if "email" in body and "@" not in body["email"]:
    errors.append("Invalid email format")

print(f"Method: {request['method']} {request['path']}")
if errors:
    print(f"Errors: {errors}")
else:
    print("Validation passed")

# 将特征重要性值规范化为总和为 1
feature_importance = {"age": 0.34, "income": 0.28, "region": 0.15, "purchases": 0.23}
total = sum(feature_importance.values())

for key in feature_importance:
    feature_importance[key] = round(feature_importance[key] / total, 3)

print(f"Normalised: {feature_importance}")