# Python 字典（dict）：创建、方法、遍历、排序与 frozendict

URL: https://skillaria.top/zh/guides/python/slovari-python

字典（dict）是可变的“键—值”集合，按键查找几乎瞬时完成；用花括号创建，例如 {"咖啡": 250}，或使用 dict() 函数。键必须唯一且可哈希，从 Python 3.7 起保留插入顺序；3.15 中新增了内置的不可变版本 frozendict。

Python 字典（`dict`）是“键—值”对的集合，无论里面有多少条记录，按键查找值几乎都是瞬间完成的。要统计各类别的支出、保存设置，或者按 id 快速查找用户，都会用到字典。下面是使用 python dict 所需的全部内容：创建、方法、遍历、排序、合并，以及 3.15 中的新特性 `frozendict`。

## Python 字典是什么

字典保存的是键值对，每个键都是唯一的：对已存在的键再次赋值，旧值会被替换。从实现上看，它是一张哈希表，因此按键查找、添加和删除平均都是常数时间，而不需要逐个遍历。

键必须是可哈希的对象：字符串、数字、由不可变值组成的元组。列表不能作为键，否则会得到 `TypeError: unhashable type: 'list'`。而值可以是任何东西，包括其他字典和列表。

从 Python 3.7 开始，字典保证记住插入顺序：遍历时，键会按照添加的先后顺序出现。

## 如何创建字典

创建字典有好几种方式，实际代码中都会遇到。

```
spent = {"餐饮": 1200, "打车": 450}       # 字面量
empty = {}                                 # 空字典
also_empty = dict()                        # 用 dict 函数，效果相同
user = dict(name="安娜", age=30)           # 关键字参数
pairs = dict([("a", 1), ("b", 2)])         # 从一组键值对创建
keys = dict.fromkeys(["x", "y"], 0)        # {'x': 0, 'y': 0}
```

注意：空的花括号 `{}` 创建的是空字典，而不是集合。空集合只能用 `set()` 创建。

用两个列表构造字典的一个便捷方法是 `zip`：

```
names = ["餐饮", "打车", "电影"]
sums = [1200, 450, 600]
spent = dict(zip(names, sums))
print(spent)   # {'餐饮': 1200, '打车': 450, '电影': 600}
```

## 按键访问：d[k] 与 get

用方括号读取值。如果键不存在，Python 会抛出 `KeyError`。要避免这种错误，使用 `get` 方法：它会返回 `None` 或指定的默认值。

```
spent = {"餐饮": 1200}
print(spent["餐饮"])            # 1200
print(spent.get("电影"))        # None
print(spent.get("电影", 0))     # 0
spent["电影"]                   # KeyError: '电影'
```

要检查字典中是否存在某个键，使用运算符 `in`：`"餐饮" in spent` 返回 `True`。它检查的是键，而不是值。检查值应写成 `1200 in spent.values()`。

## 添加、修改与删除

添加或更新一条记录只需一行赋值。配合 `get` 做计数非常方便：

```
spent = {}
for category, amount in [("餐饮", 250), ("打车", 300), ("餐饮", 400)]:
    spent[category] = spent.get(category, 0) + amount
print(spent)   # {'餐饮': 650, '打车': 300}
```

删除键可以使用 `del d[k]`（如果键不存在会抛出 `KeyError`），或者 `d.pop(k, default)`，后者还会返回对应的值。清空整个字典使用 `d.clear()`。字典的长度，即键值对的数量，用 `len(d)` 获得。

## 字典方法速查

| 方法 | 作用 |
| --- | --- |

| `get(k, default)` | 按键取值，不存在时返回 default，不报错 |

| `setdefault(k, default)` | 返回值；如果键不存在，先写入 default |

| `update(other)` | 添加另一个字典的键值对，并覆盖已有的键 |

| `pop(k, default)` | 删除键并返回值 |

| `popitem()` | 删除并返回最后添加的键值对 |

| `keys()`、`values()`、`items()` | 键、值以及键值对的视图 |

| `copy()` | 浅复制 |

| `clear()` | 清空字典 |

| `fromkeys(keys, value)` | 用相同的值创建新字典 |

### setdefault：用列表填充字典

当值是列表并且需要分组时，`setdefault` 非常好用：

```
groups = {}
for item, cat in [("咖啡", "餐饮"), ("地铁", "交通"), ("披萨", "餐饮")]:
    groups.setdefault(cat, []).append(item)
print(groups)   # {'餐饮': ['咖啡', '披萨'], '交通': ['地铁']}
```

### update：更新字典

`update` 原地修改字典：新的键被添加，已有的键被覆盖。

```
cfg = {"debug": False, "lang": "zh"}
cfg.update({"debug": True, "theme": "dark"})
print(cfg)   # {'debug': True, 'lang': 'zh', 'theme': 'dark'}
```

## defaultdict：带默认值的字典

`collections.defaultdict` 是 dict 的子类：访问不存在的键时，它会通过工厂函数自动创建一个值。这样统计和分组的代码会更简短。

```
from collections import defaultdict

spent = defaultdict(int)
for cat, amount in [("餐饮", 250), ("打车", 300), ("餐饮", 400)]:
    spent[cat] += amount
print(dict(spent))   # {'餐饮': 650, '打车': 300}

groups = defaultdict(list)
groups["餐饮"].append("咖啡")
```

如果只需要统计出现次数，还可以使用 `collections.Counter`：`Counter(["a", "b", "a"])` 得到 `Counter({'a': 2, 'b': 1})`。

## 遍历字典：for 循环

在 `for` 循环中遍历字典默认取的是键。要同时得到键和值，遍历 `items()`——这是最常见的写法。

```
spent = {"餐饮": 650, "打车": 300}

for cat in spent:                    # 键
    print(cat)

for amount in spent.values():        # 值
    print(amount)

for cat, amount in spent.items():    # 键值对
    print(f"{cat}: {amount} ₽")
```

在遍历过程中不能改变字典的大小，否则会得到 `RuntimeError: dictionary changed size during iteration`。如果需要删除部分键，应遍历它的副本：`for k in list(d):`。

## 输出字典

`print(d)` 会把字典打印在一行上。对于较大或嵌套的字典，使用 `pprint` 模块或 `json.dumps(d, ensure_ascii=False, indent=2)` 更好——它会以缩进的形式输出数据，并且中文不会被转义。

## 字典排序

字典本身不能原地排序，它没有 `sort` 方法。对字典排序的做法是：用 `sorted` 对键值对排序，然后用结果创建一个新字典。

```
spent = {"打车": 300, "餐饮": 650, "电影": 120}

by_key = dict(sorted(spent.items()))
print(by_key)    # {'打车': 300, '电影': 120, '餐饮': 650}

by_value = dict(sorted(spent.items(), key=lambda p: p[1], reverse=True))
print(by_value)  # {'餐饮': 650, '打车': 300, '电影': 120}
```

如果只需要前 N 个类别，就不必再转换回字典：`sorted(spent.items(), key=lambda p: p[1], reverse=True)[:2]` 返回的是键值对列表。

按字段对字典列表排序：`sorted(users, key=lambda u: u["age"])`。

## 合并字典

从 Python 3.9 起，字典可以用运算符 `|` 合并，`|=` 则原地更新。键冲突时，右边的操作数优先。

```
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}
print(a | b)          # {'x': 1, 'y': 20, 'z': 3}
print({**a, **b})     # 同样的结果，使用解包
a |= b                # 相当于 a.update(b)
```

注意：字典之间不能用 `a + b` 相加，会得到 `TypeError`。如果确实需要把相同键的值加起来，使用 `Counter`：`Counter(a) + Counter(b)`。

### 字典解包

两个星号 `**` 可以把字典解包为函数的关键字参数，或者解包到另一个字典中：

```
def greet(name, city):
    return f"{name} 来自 {city}"

data = {"name": "安娜", "city": "喀山"}
print(greet(**data))   # 安娜 来自 喀山
```

## 字典推导式（dict comprehension）

字典推导式可以用一行代码从任何序列构建字典：

```
squares = {n: n * n for n in range(5)}
print(squares)   # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

big = {k: v for k, v in spent.items() if v > 200}

inverted = {v: k for k, v in {"a": 1, "b": 2}.items()}
print(inverted)  # {1: 'a', 2: 'b'}
```

最后一个例子展示了如何交换键和值。它只在值唯一且可哈希时有效，否则一部分键值对会丢失。

## 嵌套字典

值可以是另一个字典——这样可以描述 API 的 JSON 响应，或者“用户字典的字典”这样的结构。

```
users = {
    101: {"name": "安娜", "skills": ["python", "sql"]},
    102: {"name": "奥列格", "skills": ["go"]},
}
print(users[101]["skills"][0])            # python
print(users.get(103, {}).get("name"))     # None，不会抛出 KeyError
```

## 复制字典：副本与第二个名字

语句 `b = a` 并不会复制字典，而是给同一个对象起了第二个名字。通过 `b` 修改，在 `a` 中也能看到变化。把字典传给函数时也是如此：函数拿到的是共享对象的引用，可能会悄悄地改坏它。

```
import copy

a = {"limits": [0, 500]}
b = a.copy()              # 浅复制
b["limits"].append(1000)  # 嵌套列表是共享的！
print(a)                  # {'limits': [0, 500, 1000]}

c = copy.deepcopy(a)      # 完全独立的副本
```

## 比较字典

运算符 `==` 按内容比较字典：只有所有键值对都相同才相等。键的顺序不影响结果：`{"a": 1, "b": 2} == {"b": 2, "a": 1}` 为 `True`。要找出共同的键，可以对视图做集合运算：`a.keys() & b.keys()`。

## 带类型的字典：注解

字典的注解写作 `dict[str, int]`。如果字典有固定的一组字段，类型各不相同，可以使用 `typing` 模块中的 `TypedDict`：

```
from typing import TypedDict

class User(TypedDict):
    name: str
    age: int

u: User = {"name": "安娜", "age": 30}
```

注解在运行时不会被检查，它们是给 IDE 和 mypy 等分析工具用的。

## 不可变字典：Python 3.15 中的 frozendict

Python 3.15 引入了内置类型 `frozendict`（PEP 814）：无需导入，尝试修改它会立刻抛出 `TypeError`。以前，不可变字典只能用 `types` 模块中的包装器或第三方包来模拟。它正适合保存共享的设置——任何函数都不应悄悄地修改它们。

```
config = frozendict(min_amount=0, max_amount=500)
config["max_amount"] = 1000
# TypeError: 'frozendict' object does not support item assignment

strict = config | {"max_amount": 300}   # 得到新的 frozendict，原对象不变
cache = {config: "五月报告"}        # 可哈希，可以作为键
```

根据课程材料，需要了解以下几点：

- `frozendict` 继承自 `object`，而不是 `dict`，因此 `isinstance(cfg, dict)` 返回 `False`。要判断“任意字典”，使用 `collections.abc.Mapping` 或 `isinstance(x, (dict, frozendict))`。
- 只有当键和值都可哈希时，它才是可哈希的：`frozendict(a=[1])` 可以创建，但 `hash()` 会失败。它的不可变性是浅层的，与元组一样。
- 它保存顺序，但 `==` 不考虑顺序。
- `json.dumps`、`pickle` 和 `copy` 都支持 frozendict。

## 常见错误

- **读取时出现 KeyError** — 使用 `get` 或先用 `in` 检查。
- **用列表作为键** — 把它转换为元组：`d[tuple(lst)]`。
- **在循环中修改字典** — 遍历 `list(d)`。
- **以为赋值会产生副本** — 使用 `copy()` 或 `deepcopy`。
- **用 `{}` 表示空集合** — 它是空字典。

## 小结

Python dict 是语言中最重要的数据结构：按键快速查找，保留插入顺序，并提供便捷的方法 `get`、`setdefault`、`update`，通过 `items()` 遍历，用 `sorted` 排序，用运算符 `|` 合并。注意复制行为和可哈希的键；如果需要不可变的设置，在 Python 3.15 中使用 `frozendict`。字典、列表、元组和集合在课程《Python 3.15：从新手到专家》中结合支出记录器项目进行了实践讲解。

## FAQ

**字典中的值可以是不同类型吗？**

可以，同一个字典中的值可以是任何类型，并且彼此不同：数字、字符串、列表、其他字典。只有键有限制——它们必须是可哈希的。

**如何获取字典中元素的个数？**

函数 len(d) 返回键值对的数量。空字典的长度为 0，在 if 条件中为假。

**dict 和 defaultdict 有什么区别？**

defaultdict 是 dict 的子类，访问不存在的键时不会抛出 KeyError，而是通过工厂函数（例如 int 或 list）创建值。其他方面它的行为与普通字典相同。

**如何通过值找到键？**

没有直接的方法，需要遍历：next((k for k, v in d.items() if v == value), None)。如果经常这样查找，可以用推导式构建反向字典 {v: k for k, v in d.items()}。

**如何把字典保存到文件中？**

通常使用 json 模块：以写入模式打开文件后执行 json.dump(d, f, ensure_ascii=False, indent=2)，读取时使用 json.load(f)。保存后，键会变成字符串。


---
Source: Skillaria.Top (https://skillaria.top)
