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

字典

docs.scrimba.com

列表让你按位置查找内容。但通常你想按名称查找。不是"给我第 3 项",而是"给我李明的分数"。字典将数据存储为键值对:你按键而不是位置查找值。

当列表的位置索引没有意义时,字典就是正确的数据结构。字典将任意键映射到值,让你能在 O(1) 时间内进行命名查找。排行榜、JSON 响应、配置文件:都自然地表示为键值映射。

dict 是一个键值存储,查找、插入和删除的平均时间复杂度为 O(1)(O(1) 表示无论字典有多大成本都保持不变,因为键通过哈希找到,将键转换为一个直接指向其插槽的数字)。键必须是可哈希的,意味着它们的内容永远不会改变,所以那个数字保持稳定:strinttuple 符合条件,list 则不符合。值可以是任何对象。自 Python 3.7 起,字典按插入顺序保持键,这在序列化或显示时很重要。同样的结构驱动了 Python 表面下的很多东西,包括你自己对象的属性,所以值得深入学习字典。列表章节涵盖了基于位置的表亲。

创建字典

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

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

字典字面量从左到右求值。键必须是可哈希的(它们的内容不能改变,所以 Python 为它们编制的数字保持不变):strinttuple 符合条件,listdict 不符合。值没有此类限制。自 Python 3.7 以来,插入顺序是语言保证的,所以迭代和序列化是可重现的。一个隐藏的陷阱:单个字面量中的重复键不会引发错误,它保留最后一个值并丢弃前面的值。

python
player = {
    "name": "张三",
    "score": 87,
    "level": 5,
    "alive": True,
}
Juno创建字典 大括号,每个键和它的值之间加冒号,对之间加逗号。键通常是字符串,值可以是任何东西,甚至是另一个字典。无论你按什么顺序添加内容,你都按那个顺序获取它们,这比我预期的用处更大。
Juno创建字典 大括号和 key: value 对。键可以是任何不可变类型(strinttuple),值可以是任何对象。迭代遵循插入顺序,所以当顺序重要时字典很合适。
Juno创建字典 键必须是可哈希的,所以 strinttuple 可以,listdict 不可以。插入顺序自 3.7 起得到保证,所以你可以依赖它进行输出。小心重复键字面量:它无声地取最后一个值,没有警告。

访问值

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

方括号访问在键不存在时引发 KeyError.get(key) 在未找到时返回 None.get(key, default) 返回默认值。只要键的存在性不确定,使用 .get();它比用 try/except 包装访问更安全,更易读。

方括号访问通过哈希键并读取匹配的插槽进行查找,平均时间为 O(1)(无论大小如何成本都不变)。未找到时引发 KeyError.get(key, default=None) 执行相同的查找,但在未找到时返回默认值而不是引发,所以当键的存在性不确定时这是正确的调用。在进行方括号访问前进行保护时,key in d 也是 O(1);当你只需要是或否时选择 in,当你想要值或一步式回退时选择 .get()

python
player = {"name": "张三", "score": 87}

player["name"]    # "张三"
player["score"]   # 87
player["lives"]   # KeyError (键不存在)
python
player.get("score")          # 87
player.get("lives")          # None (无错误,默认返回 None)
player.get("lives", 3)       # 3   (如果键不存在使用此默认值)

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

python
count = inventory.get("arrows", 0)   # 如果字典中没有 "arrows",返回 0
Juno访问值d["key"] 给你值,但如果那个键不在那里会引发 KeyError。当你不确定时,选择 .get():它返回 None 而不是崩溃,或者返回你传入的默认值。我现在将所有东西都默认化,我的程序停止因缺失键而崩溃。
Juno访问值 方括号访问在未找到时引发 KeyError.get(key) 返回 None,而 .get(key, default) 返回你传入的任何东西。当键可能不存在时使用 .get():比用 try/except 包装访问更整洁。
Juno访问值d[key].get() 都进行一次 O(1) 查找,区别只在于未找到时发生什么:引发异常对比返回默认值。当你想要是否答案时使用 key in d,当你想要值或一步式回退时使用 .get()

添加和更新

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

分配到键是平均 O(1) 的操作,同时完成两项工作:如果键是新的则创建条目,如果已存在则替换值。.update() 接受另一个字典或键值对的可迭代对象,并为每个条目应用相同的分配,在任何共享键上覆盖。

d[key] = value 哈希键并在一个平均 O(1) 步骤中插入或覆盖,所以没有单独的"添加"和"更新":分配两者都做。.update(other) 是在 other 中的每个条目上运行相同操作,在任何共享键上覆盖。要在不改变原始字典的情况下合并,| 操作符(Python 3.9+)返回一个新字典并让两个输入保持不变,而 |=.update() 一样原地改变。当函数不应修改调用者仍持有的字典时,选择 |

python
player = {"name": "张三", "score": 87}

player["score"] = 92        # 更新现有的
player["level"] = 5         # 添加新键
python
extras = {"level": 5, "alive": True}
player.update(extras)   # 使用 extras 中的键添加/覆盖
Juno添加和更新 使用方括号分配到键:如果已经在那里,值被替换;如果不在,新条目出现。一个语法处理两种情况,无需事先检查。使用 .update() 一次性合并整个其他字典。
Juno添加和更新d[key] = value 一步创建或替换,所以没有不同的添加对比更新。.update(other) 合并整个字典,覆盖任何共享键。两者每键都是 O(1)。
Juno添加和更新 分配在一个 O(1) 步骤中插入或覆盖,而 .update() 对每个条目都这样做。当你不想动原始字典时,| 返回一个新合并的字典,而 |= 原地改变。在函数内使用 | 所以你不会无声地改变调用者仍持有的字典。

移除项

四种移除条目的方式。.pop() 移除键并返回值。带默认值的 .pop() 在键可能不存在时很安全。del 移除键,无返回值。.clear() 清空整个字典。

.pop(key) 在未找到时引发 KeyError.pop(key, default) 返回默认值,使其成为安全的移除方法。del d[key] 移除键无返回值,在未找到时引发 KeyError.clear() 移除所有条目但保留字典对象本身。

.pop(key, default) 在一个平均 O(1) 查找中移除并返回,默认值使其对未找到的情况安全;没有它,缺失键会引发 KeyErrordel d[key] 移除无返回值并在未找到时引发。.clear() 清空字典但保留同一对象,所以任何其他指向它的名称看到的都是空字典。需要记住的失败模式:在迭代字典时改变它会在循环中间引发 RuntimeError。修复方法是先用 for key in list(d): 对计划移除的键做快照,然后从原始字典中移除。

python
player = {"name": "张三", "score": 87, "level": 5}

player.pop("level")            # 移除 "level" 并返回 5
player.pop("lives", None)      # 安全的 pop,如果键不存在返回 None
del player["score"]            # 移除 "score",无返回值
player.clear()                 # 移除所有内容

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

Juno移除项.pop(key) 移除键并返回它的值,而 .pop(key, None) 在键可能不存在时保持冷静。del d[key] 移除不返回任何东西,.clear() 清空整个东西。.pop() 上的默认值救了我免遭很多 KeyError 惊喜。
Juno移除项.pop(key) 在未找到时引发,.pop(key, default) 不引发,使其成为安全选择。del d[key] 移除不返回且在未找到时引发;.clear() 清空字典但保留对象本身。
Juno移除项.pop(key, default) 是你的未找到安全移除返回;del 和方括号pop在缺失键时引发。咬人的那个:在迭代时移除会抛出 RuntimeError。先用 for key in list(d): 做快照,然后从原始字典中删除。

迭代

三个视图让你循环字典的不同部分。直接迭代字典给你键。.values() 给值。.items() 一次给出两个,这是你最常用的:将每对解包成两个名称以获得清晰、可读的循环。

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

.keys().values().items() 返回视图对象(字典的活动窗口,不是单独的列表)。视图不复制任何东西并反映字典的当前状态,所以如果字典改变,视图也随之改变。因为键是唯一的且可哈希的,键视图支持集合代数:d.keys() & other.keys() 找共享键,- 找差异,这对比较两个配置很方便。相同的在迭代时改变的陷阱也适用于这里;如果你必须在循环时改变字典,用 list(d.items()) 迭代快照。

python
player = {"name": "张三", "score": 87, "level": 5}

for key in player:               # 迭代键(最常见)
    print(key)

for key in player.keys():        # 相同,显式键视图
    print(key)

for value in player.values():    # 值
    print(value)

for key, value in player.items():   # 两者,最有用
    print(f"{key}: {value}")

.items() 是你最常用的。将每对解包成两个名称使循环可读。

Juno迭代 直接循环字典你得到它的键。.values() 给值,.items() 一次给两个,这是你最常选择的。将每对解包成两个名称像 for key, value in player.items() 使我的循环更容易读得多。
Juno迭代.keys().values().items() 返回活动视图,不是列表,所以它们跟踪字典的变化。.items()for k, v in d.items() 读起来最整洁,是你最常写的循环。
Juno迭代 三个视图是活动窗口,不是副本,所以它们反映字典的变化。键视图做集合代数,所以 d.keys() & other.keys() 在一行中比较两个字典。和移除同样的规则:不要在迭代时改变,如果必须做快照 list(d.items())

检查成员

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

对于字典,innot in 是 O(1) 的,它们只检查键。要检查值,你会使用 in d.values(),但那是 O(n),因为值没有索引。

key in d 通过哈希键并读取一个插槽找到键:平均 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(),虽然很少需要。

Juno检查成员in 告诉你一个键是否在字典中,只有一个键,永远不是值。无论字典多大它都保持快速。将其翻转为 not in 检查某物是否不存在。
Juno检查成员innot in 只检查键,O(1) 的。检查值需要 value in d.values(),O(n) 因为值没有索引。所以围绕按键查找事物进行设计。
Juno检查成员key in d 是 O(1),value in d.values() 是 O(n)。那个差距是为什么要按你搜索的东西为字典编制键的原因。如果你持续扫描值,你的字典方向搞反了;改为添加反向映射。

嵌套字典

值本身可以是字典。这是表示具有多个级别的结构化数据的方式:具有统计部分的玩家、具有子部分的配置文件。两组方括号访问嵌套值:第一个选择外部键,第二个选择内部键。

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

嵌套字典持有引用到内部字典,不是它们的副本(引用是指向一个共享对象的指针,所以两个名称可以指向同一内部字典)。这是复制咬人的地方:d.copy() 是一个浅副本,它复制外部字典但内部字典仍然共享,所以通过副本改变一个在原始字典中也改变。当你需要副本完全独立时,copy.deepcopy() 遍历整个树并复制每个级别。在你向可能改变它的代码交付配置前选择它。每个方括号步骤是它自己的 O(1) 查找,所以深度在速度上无成本,只在可读性上。

python
users = {
    "alice": {"score": 87, "level": 5},
    "bob": {"score": 74, "level": 3},
}

users["alice"]["score"]   # 87
users["bob"]["level"]     # 3

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

Juno嵌套字典 值可以是整个它自己的字典,这是你如何持有结构化数据如具有统计部分的玩家的方式。两组方括号探入:第一个选择外部键,第二个从里面选择。方便,但我尽量不嵌套太深否则读起来会很麻烦。
Juno嵌套字典 嵌套字典是值是字典的字典;用链式方括号探入。外部字典持有对内部字典的引用,所以改变内部字典显示在它被引用的任何地方。保持嵌套浅层否则导航变得困难。
Juno嵌套字典 外部字典存储对内部字典的引用,所以 .copy() 是浅的:副本共享那些内部字典而改变泄漏跨两者。当你需要真正的独立性时使用 copy.deepcopy()。深度在速度上无成本,在可读性上有成本,所以保持浅层。

setdefault

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

.setdefault(key, default) 是一个一次调用的读或创建:如果键存在,返回它的当前值不改变任何东西;如果不存在,插入默认值并返回它。常见的用例是构建分组结构而无需单独的存在检查。

.setdefault(key, default) 是一个读或创建的 O(1) 查找:现有键返回它们的现有值不动,缺失键得到 default 插入并返回。值得知道的陷阱:default 是一个普通参数,所以即使键已存在它也总是被构建。如果那个默认值构建很昂贵(一个新对象,一个函数调用),你每次调用都为它付费。defaultdict,下面涵盖的,通过仅在未找到时运行工厂来绕过这一点。对于日常"将项分组到列表"模式,.setdefault(key, []).append(...) 是替代 key in d 检查的标准一行代码。

python
inventory = {}

inventory.setdefault("arrows", 0)    # 设置 "arrows": 0,返回 0
inventory.setdefault("arrows", 10)   # "arrows" 已存在,无改变,返回 0

它对在不首先检查键存在的情况下构建分组结构很有用:

python
groups = {}

for name, team in players:
    groups.setdefault(team, []).append(name)
Junosetdefault.setdefault(key, default) 读取一个键如果它在那里,或如果它不在的话设置为默认值并返回它。这是确保键存在的整洁方式,不会破坏已经存在的值。我最常用它来分组东西,无需"这个键在这里吗?"检查。
Junosetdefault.setdefault(key, default) 返回现有值不变,或在未找到时插入默认值并返回它。好处是分组:d.setdefault(k, []).append(x) 按键构建列表而无需单独的存在检查。
Junosetdefault 一个 O(1) 读或创建,但默认值总是被构建即使是未找到的,所以昂贵的默认值每次调用都成本你。那正是 defaultdict 的工厂修复的。对于分组,.setdefault(k, []).append(x) 是退休 key in d 检查的一行代码。

collections.defaultdict 和 Counter

标准库有两个字典子类,自动处理常见模式。defaultdict 为缺失键创建默认值所以你永远不会得到 KeyErrorCounter 计数序列中每个项出现多少次并将结果作为字典提供。

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

defaultdict(factory) 第一次你读缺失键时运行工厂(一个零参数可调用对象,像 listint),存储结果,并返回它,所以 d[k] 永不引发 KeyError。那是与 .setdefault() 的关键区别:工厂仅在未找到时运行,所以昂贵默认值在常见路径上成本为零。一个值得标记的陷阱:因为缺失键的普通读创建它,仅仅检查 d[k] 可以增长字典,所以当你只想测试不插入时使用 k in dCounter 是一个为计数构建的 dict:给它一个序列而它计数出现,而 .most_common(n) 返回前 n 个按计数。两者都在 collections 模块中。

defaultdictCounter 在标准库中,所以它们需要首先导入。导入在模块章节获得完整处理。

python
from collections import defaultdict

groups = defaultdict(list)
for name, team in players:
    groups[team].append(name)   # 如果 team 是新的无 KeyError
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 节省大量"在循环中计数东西"样板。

Junocollections.defaultdict 和 Counterdefaultdict 为任何缺失键填充默认值,所以分组和计数停止对你抛出 KeyErrorCounter 统计项在序列中出现多少次并作为字典交回。第一次我用 Counter 交换了一个手写计数循环,半个我的代码消失了。
Junocollections.defaultdict 和 Counterdefaultdict(list)defaultdict(int) 在新键上自动创建默认值,为分组和计数退休 .setdefault()Counter 是一个针对频率调整的字典,带有内置的 .most_common(n)。两者都是字典子类,所以你知道的所有东西仍然起作用。
Junocollections.defaultdict 和 Counterdefaultdict 的工厂仅在未找到时运行,所以不像 .setdefault() 昂贵的默认值在常见路径上成本为零。陷阱:读缺失键创建它,所以用 k in d 测试,不是 d[k]Counter 统计序列而 .most_common(n) 拉前 n 个。

实践中

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

python
scores = {"张三": 87, "李四": 74, "王五": 92, "赵六": 55}

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

print(f"玩家:  {len(scores)}")
print(f"平均:  {average:.1f}")
print(f"最高:  {max(scores.values())}")
print(f"最低:   {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   # 真实文件大小的占位符
    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"处理 {len(job_results)} 文件:{ok_count} 正常,{large_count} 大型")

通过迭代必填字段验证嵌套请求字典,然后原地归一化特征重要性字典:

python
request = {
    "method": "POST",
    "path": "/users",
    "headers": {"Content-Type": "application/json"},
    "body": {"username": "alice", "email": "alice@example.com"},
}

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

for field in ["username", "email"]:
    if not body.get(field):
        errors.append(f"缺失必填字段:{field}")

if "email" in body and "@" not in body["email"]:
    errors.append("无效的电子邮件格式")

print(f"方法:{request['method']} {request['path']}")
if errors:
    print(f"错误:{errors}")
else:
    print("验证通过")

# 归一化特征重要性值以和为 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"已归一化:{feature_importance}")