Python字典中defaultdict与OrderedDict的使用详解
作者:星河耀银海
一、开篇:普通字典不够用时怎么办?
前面我们学完了字典的基础操作——创建、增删改查、遍历、推导式和合并。现在你已经能用字典处理大部分日常开发中的数据映射需求了。但在某些特定场景下,普通字典会暴露出两个让人头疼的问题:
问题一:每次往字典里追加数据,都得先检查键是否存在。比如你要按类别分组,每遇到一个新品类别,都得写if key not in dict: dict[key] = [],代码又臭又长。
# 没有defaultdict之前——很啰嗦
words = ['apple', 'banana', 'avocado', 'blueberry', 'apricot']
groups = {}
for word in words:
first_letter = word[0]
if first_letter not in groups: # 每次都要检查!
groups[first_letter] = []
groups[first_letter].append(word)
问题二:普通字典虽说Python 3.7+保序了,但在某些需要精确控制键顺序的场景(比如LRU缓存),还是不够用。
好消息是,Python的collections模块给我们准备了两个超级好用的字典变体——defaultdict和OrderedDict,专门解决这两类场景。今天这篇文章,我就带你彻底掌握它们。
二、defaultdict:自带默认值的字典
2.1 普通字典的痛点回顾
在你没用过defaultdict之前,这类代码你一定写过无数次:
# 场景一:按类别分组
students = [
('数学', '小明'),
('语文', '小红'),
('数学', '小刚'),
('英语', '小红'),
('数学', '小丽'),
('语文', '小华'),
]
# 传统写法——每次都要检查和初始化
subject_groups = {}
for subject, name in students:
if subject not in subject_groups:
subject_groups[subject] = [] # 初始化空列表
subject_groups[subject].append(name)
print(subject_groups)
# {'数学': ['小明', '小刚', '小丽'], '语文': ['小红', '小华'], '英语': ['小红']}
# 另一种写法——用setdefault(稍微好一点,但还是啰嗦)
subject_groups2 = {}
for subject, name in students:
subject_groups2.setdefault(subject, []).append(name)
# 或者用dict.get配合or(有点hacky)
subject_groups3 = {}
for subject, name in students:
lst = subject_groups3.get(subject) or []
# 等等,这里不对——如果lst是[](空列表),它也是falsy的
# 所以这写法有bug...
这些写法的问题:要么代码冗长(if key not in),要么有陷阱(get() or []当值本身是空列表时出错),要么不够直观(setdefault的名字不够直白)。
2.2 defaultdict的基本使用
defaultdict是dict的子类,它在创建时需要传入一个工厂函数(一个无参数的可调用对象),当访问的键不存在时,自动用这个工厂函数生成默认值。
from collections import defaultdict
# 语法:defaultdict(工厂函数)
# 工厂函数是一个无参数、返回默认值的可调用对象
# 最常见的用法——默认值为空列表
dd_list = defaultdict(list)
print(dd_list['a']) # []——键'a'不存在,自动创建空列表
print(dd_list) # defaultdict(<class 'list'>, {'a': []})
# 现在前面的分组问题变得多简洁:
subject_groups = defaultdict(list)
for subject, name in students:
subject_groups[subject].append(name) # 不需要检查键是否存在!
print(dict(subject_groups))
# {'数学': ['小明', '小刚', '小丽'], '语文': ['小红', '小华'], '英语': ['小红']}
# 默认值为0——计数器
dd_int = defaultdict(int)
words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
for word in words:
dd_int[word] += 1 # 键不存在时自动初始化为0,然后+1
print(dict(dd_int)) # {'apple': 3, 'banana': 2, 'orange': 1}
2.3 各种工厂函数的选择
from collections import defaultdict
# 1. list——分组收集
dd = defaultdict(list)
dd['fruits'].append('apple')
dd['fruits'].append('banana')
print(dd) # {'fruits': ['apple', 'banana']}
# 2. int——计数
dd = defaultdict(int)
dd['a'] += 1
dd['a'] += 1
dd['b'] += 1
print(dd) # {'a': 2, 'b': 1}
# 3. set——去重收集
dd = defaultdict(set)
dd['vowels'].add('a')
dd['vowels'].add('e')
dd['vowels'].add('a') # 重复——自动去重
print(dd) # {'vowels': {'a', 'e'}}
# 4. float——累加浮点数
dd = defaultdict(float)
prices = [('apple', 5.5), ('banana', 3.2), ('apple', 4.5)]
for item, price in prices:
dd[item] += price
print(dd) # {'apple': 10.0, 'banana': 3.2}
# 5. str——字符串拼接
dd = defaultdict(str)
dd['greeting'] += 'Hello'
dd['greeting'] += ' World'
print(dd) # {'greeting': 'Hello World'}
# 6. dict——嵌套字典
dd = defaultdict(dict)
dd['user1']['name'] = '小明'
dd['user1']['age'] = 25
dd['user2']['name'] = '小红'
print(dict(dd))
# {'user1': {'name': '小明', 'age': 25}, 'user2': {'name': '小红'}}
# 7. 自定义函数/lambda
dd = defaultdict(lambda: '未知')
print(dd['anything']) # '未知'
# lambda也可以做更复杂的默认值
dd = defaultdict(lambda: {'count': 0, 'items': []})
dd['electronics']['count'] += 1
dd['electronics']['items'].append('手机')
print(dict(dd))
# {'electronics': {'count': 1, 'items': ['手机']}}
重要:工厂函数必须是无参数的可调用对象。defaultdict([])会报错,因为[]不是可调用的;应该用defaultdict(list)。
2.4 defaultdict的内部原理
# defaultdict最核心的秘密在于 __missing__ 方法
# 这是Python字典的一个钩子方法,当键不存在时被调用
# 简化版的defaultdict实现
class SimpleDefaultDict(dict):
def __init__(self, default_factory=None):
super().__init__()
self.default_factory = default_factory
def __missing__(self, key):
"""当键不存在时被调用"""
if self.default_factory is None:
raise KeyError(key)
# 调用工厂函数生成默认值
value = self.default_factory()
# 把默认值存入字典(这样下次访问同一个键就不会再触发__missing__)
self[key] = value
return value
# 测试我们的简化版
sdd = SimpleDefaultDict(list)
print(sdd['a']) # []
sdd['a'].append(1)
print(sdd) # {'a': [1]}——默认值已经被存入字典了
# Python真正的defaultdict就是这个原理
# __missing__方法只在键完全不存在时被调用
# get()方法不会触发__missing__(它有自己的默认值逻辑)
from collections import defaultdict
dd = defaultdict(list)
print(dd.get('nonexistent')) # None——get不触发default_factory!
print(dd) # {}——键没有被创建
三、defaultdict的实战场景
3.1 场景一:单词频率统计
from collections import defaultdict
text = """
Python is an interpreted high-level programming language
for general-purpose programming Python is dynamically typed
and garbage-collected it supports multiple programming
paradigms including procedural object-oriented and functional
programming Python is often described as a batteries included
language due to its comprehensive standard library
"""
# 用defaultdict统计词频
word_count = defaultdict(int)
for word in text.lower().split():
# 去掉标点符号
word = word.strip('.,"\'()[]{};:')
if word:
word_count[word] += 1
# 按词频排序输出
sorted_words = sorted(word_count.items(), key=lambda x: x[1], reverse=True)
for word, count in sorted_words[:10]:
print(f'{word}: {count}')
3.2 场景二:按多种条件分组
from collections import defaultdict
# 有一个学生成绩列表,需要按多种维度分组
students = [
{'name': '小明', 'grade': 'A', 'class': '1班', 'score': 95},
{'name': '小红', 'grade': 'B', 'class': '1班', 'score': 82},
{'name': '小刚', 'grade': 'A', 'class': '2班', 'score': 91},
{'name': '小丽', 'grade': 'C', 'class': '2班', 'score': 73},
{'name': '小华', 'grade': 'B', 'class': '1班', 'score': 87},
{'name': '小强', 'grade': 'A', 'class': '2班', 'score': 98},
]
# 按等级分组
by_grade = defaultdict(list)
for s in students:
by_grade[s['grade']].append(s['name'])
print('按等级分组:')
for grade, names in sorted(by_grade.items()):
print(f' {grade}等: {names}')
# A等: ['小明', '小刚', '小强']
# B等: ['小红', '小华']
# C等: ['小丽']
# 按班级分组
by_class = defaultdict(list)
for s in students:
by_class[s['class']].append(s)
print('\n按班级分组:')
for cls, members in by_class.items():
avg = sum(m['score'] for m in members) / len(members)
print(f' {cls}: 平均分{avg:.1f}')
# 同时按等级和班级分组(嵌套defaultdict)
by_grade_and_class = defaultdict(lambda: defaultdict(list))
for s in students:
by_grade_and_class[s['grade']][s['class']].append(s['name'])
print('\n按等级→班级两级分组:')
for grade in sorted(by_grade_and_class):
print(f' {grade}等:')
for cls, names in by_grade_and_class[grade].items():
print(f' {cls}: {names}')
3.3 场景三:构建索引(倒排索引)
from collections import defaultdict
# 构建一个简单的文档搜索索引
# 倒排索引:{word: {doc_id: [positions]}}
documents = {
1: "Python is a great programming language",
2: "Java is also a programming language",
3: "Python and Java are popular languages",
}
# 构建倒排索引
inverted_index = defaultdict(lambda: defaultdict(list))
for doc_id, text in documents.items():
for position, word in enumerate(text.lower().split()):
word = word.strip('.,;:!?')
inverted_index[word][doc_id].append(position)
# 查询
def search(query):
"""简单的AND查询——所有词都必须出现在文档中"""
query_words = query.lower().split()
if not query_words:
return []
# 取第一个词的文档集合
result_docs = set(inverted_index[query_words[0]].keys())
# 与其他词取交集
for word in query_words[1:]:
result_docs &= set(inverted_index[word].keys())
return sorted(result_docs)
print('倒排索引:')
for word, doc_info in sorted(inverted_index.items()):
print(f' "{word}": 出现在 {dict(doc_info)}')
print(f'\n搜索 "programming language": 文档 {search("programming language")}')
print(f'搜索 "Python language": 文档 {search("Python language")}')
3.4 场景四:聚合操作(类似SQL的GROUP BY)
from collections import defaultdict
# 模拟SQL的GROUP BY聚合
orders = [
{'product': '手机', 'category': '电子产品', 'amount': 2999, 'quantity': 1},
{'product': '电脑', 'category': '电子产品', 'amount': 5999, 'quantity': 1},
{'product': '苹果', 'category': '食品', 'amount': 5, 'quantity': 3},
{'product': '香蕉', 'category': '食品', 'amount': 3, 'quantity': 5},
{'product': 'T恤', 'category': '服装', 'amount': 99, 'quantity': 2},
{'product': '耳机', 'category': '电子产品', 'amount': 299, 'quantity': 2},
{'product': '牛奶', 'category': '食品', 'amount': 8, 'quantity': 4},
]
# 按类别聚合
category_stats = defaultdict(lambda: {
'total_amount': 0,
'total_quantity': 0,
'order_count': 0,
'products': set()
})
for order in orders:
cat = order['category']
stats = category_stats[cat]
stats['total_amount'] += order['amount'] * order['quantity']
stats['total_quantity'] += order['quantity']
stats['order_count'] += 1
stats['products'].add(order['product'])
print('按类别汇总:')
for cat, stats in sorted(category_stats.items()):
print(f'\n{cat}:')
print(f' 总金额: ¥{stats["total_amount"]}')
print(f' 总数量: {stats["total_quantity"]}件')
print(f' 订单数: {stats["order_count"]}笔')
print(f' 涉及产品: {stats["products"]}')
四、defaultdict的注意事项
4.1 工厂函数必须是无参数的
from collections import defaultdict
# ✅ 正确——这些工厂函数都不需要参数
dd1 = defaultdict(list) # list() → []
dd2 = defaultdict(int) # int() → 0
dd3 = defaultdict(float) # float() → 0.0
dd4 = defaultdict(str) # str() → ''
dd5 = defaultdict(set) # set() → set()
dd6 = defaultdict(dict) # dict() → {}
dd7 = defaultdict(bool) # bool() → False
# ✅ 也可以用无参数的lambda
dd8 = defaultdict(lambda: '默认值')
dd9 = defaultdict(lambda: [0, 0, 0])
dd10 = defaultdict(lambda: {'hits': 0, 'first_seen': None})
# ❌ 错误——传了值而不是工厂函数
# dd = defaultdict([]) # TypeError: list object is not callable
# dd = defaultdict(0) # TypeError: int object is not callable
# dd = defaultdict({}) # TypeError: dict object is not callable
# ⚠️ 注意:下面的写法是创建一个带参数的工厂函数——这是OK的
dd11 = defaultdict(lambda: [0, 0, 0]) # lambda本身无参数,但内部返回了一个列表
print(dd11['a']) # [0, 0, 0]
4.2 访问不存在的键会创建它
from collections import defaultdict
# ⚠️ 注意:defaultdict会"悄悄地"创建键
dd = defaultdict(list)
print(f'访问前: {dict(dd)}') # {}
_ = dd['new_key'] # 只是读取,但键被创建了!
print(f'访问后: {dict(dd)}') # {'new_key': []}
# 这在某些场景下是好事(少写代码)
# 但在另一些场景下可能是坏事(不小心创建了不需要的键)
# 如果你不想创建键,可以用get
dd2 = defaultdict(int)
print(dd2.get('nonexistent')) # None——get不会触发default_factory
print(dict(dd2)) # {}——get确实没有创建键
# 判断键是否真正存在
dd3 = defaultdict(list, {'a': [1], 'b': [2]})
dd3['c'] # 这会创建'c'
print('a' in dd3) # True——原来就有
print('b' in dd3) # True——原来就有
print('c' in dd3) # True——被访问时创建了
print('d' in dd3) # False——从未访问过
4.3 不要滥用defaultdict
from collections import defaultdict
# ❌ 有时候用普通字典更清晰
# 如果你只是想给键设置默认值,普通dict.get就够了
config = {'host': 'localhost', 'port': 8080}
host = config.get('host', '0.0.0.0') # 简单明了
# ❌ 如果默认值只需要在少数几个地方使用
# 用setdefault可能比defaultdict更合适
d = {}
d.setdefault('key', []).append(1)
d.setdefault('key', []).append(2)
print(d) # {'key': [1, 2]}
# ✅ defaultdict适用于"频繁访问不存在的键"的场景
# 比如循环中的分组、计数、累加
# 一个好的判断标准:
# 如果你的代码里有超过3次的 if key not in dict 或 dict.setdefault
# 那就应该考虑用defaultdict
五、OrderedDict:记住插入顺序的字典
5.1 Python字典的顺序演变史
在Python 3.6之前,普通字典是不保证顺序的。那时如果你需要记住键的插入顺序,必须使用OrderedDict。从Python 3.7开始,普通字典已经正式保证按插入顺序排列了,那OrderedDict是不是就没用了?
不!OrderedDict还有一些普通字典没有的特殊能力:
from collections import OrderedDict
# 创建OrderedDict
od = OrderedDict()
od['first'] = 1
od['second'] = 2
od['third'] = 3
od['fourth'] = 4
print(od)
# OrderedDict([('first', 1), ('second', 2), ('third', 3), ('fourth', 4)])
# 遍历顺序=插入顺序
for key, value in od.items():
print(f'{key}: {value}', end=' | ')
# first: 1 | second: 2 | third: 3 | fourth: 4 |
5.2 OrderedDict的独特能力
from collections import OrderedDict
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
print(f'初始: {od}')
# 独特能力一:move_to_end——把某个键移到末尾
od.move_to_end('b')
print(f'b移到末尾: {od}')
# OrderedDict([('a', 1), ('c', 3), ('b', 2)])
# 移到开头
od.move_to_end('b', last=False)
print(f'b移到开头: {od}')
# OrderedDict([('b', 2), ('a', 1), ('c', 3)])
# 独特能力二:popitem——弹出指定位置的元素
od = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])
# last=True(默认)——弹出最后一个(LIFO,像栈)
item = od.popitem(last=True)
print(f'弹出最后一个: {item}, 剩余: {od}')
# 弹出最后一个: ('d', 4), 剩余: OrderedDict([('a', 1), ('b', 2), ('c', 3)])
# last=False——弹出第一个(FIFO,像队列)
item = od.popitem(last=False)
print(f'弹出第一个: {item}, 剩余: {od}')
# 弹出第一个: ('a', 1), 剩余: OrderedDict([('b', 2), ('c', 3)])
5.3 OrderedDict的相等性比较
from collections import OrderedDict
# ⚠️ OrderedDict和普通字典在相等性判断上有一个重要区别
# 普通字典——只比较内容,不关心顺序
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(f'普通字典: d1 == d2 → {d1 == d2}') # True
# OrderedDict——既比较内容,也比较顺序!
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(f'OrderedDict: od1 == od2 → {od1 == od2}') # False——顺序不同!
od3 = OrderedDict([('a', 1), ('b', 2)])
print(f'相同顺序: od1 == od3 → {od1 == od3}') # True
# 但OrderedDict可以和普通字典相等(内容相同就相等)
print(f'od1 == d1 → {od1 == d1}') # True
六、OrderedDict的实战场景
6.1 场景一:LRU缓存实现
from collections import OrderedDict
class LRUCache:
"""
使用OrderedDict实现LRU(Least Recently Used)缓存。
原理:每次访问时将键移到末尾(表示最近使用),
当容量超限时,删除最开头(最久未使用)的键。
"""
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key):
"""获取缓存值,同时标记为最近使用"""
if key not in self.cache:
return -1 # 或 raise KeyError
# 移到末尾——标记为最近使用
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
"""存入缓存,超容量时淘汰最久未使用的"""
if key in self.cache:
# 更新已有键——移到末尾
self.cache.move_to_end(key)
self.cache[key] = value
# 超容量——删除第一个(最久未使用)
if len(self.cache) > self.capacity:
oldest_key, oldest_value = self.cache.popitem(last=False)
print(f' 淘汰: {oldest_key} → {oldest_value}')
def __repr__(self):
return f'LRUCache({dict(self.cache)})'
# 使用示例
print("=== LRU缓存演示(容量=3)===\n")
cache = LRUCache(3)
cache.put('A', 1)
cache.put('B', 2)
cache.put('C', 3)
print(f'初始: {cache}') # A, B, C
# 访问A——A变为最近使用
cache.get('A')
print(f'访问A后: {cache}') # B, C, A(A移到最后)
# 存入D——触发淘汰(B最久未使用)
cache.put('D', 4)
print(f'存入D后: {cache}') # C, A, D
# 存入E——触发淘汰(C最久未使用)
cache.put('E', 5)
print(f'存入E后: {cache}') # A, D, E
6.2 场景二:去重但保留顺序
from collections import OrderedDict
# 问题:给一个列表去重,但保留第一次出现的顺序
items = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
# 方法一:用OrderedDict(Python 3.6+)
unique_ordered = list(OrderedDict.fromkeys(items))
print(f'去重保序: {unique_ordered}') # [3, 1, 4, 5, 9, 2, 6]
# 对比:用set会丢失顺序
unique_no_order = list(set(items))
print(f'set去重(失序): {unique_no_order}') # [1, 2, 3, 4, 5, 6, 9]顺序乱了
# 方法二:手动去重保序(Python 3.6-)
def dedupe_ordered(seq):
seen = set()
result = []
for item in seq:
if item not in seen:
seen.add(item)
result.append(item)
return result
print(f'手动去重保序: {dedupe_ordered(items)}') # [3, 1, 4, 5, 9, 2, 6]
# 💡 Python 3.7+中,普通字典也保序了
# 所以 dict.fromkeys(items) 也能达到同样效果
unique_with_dict = list(dict.fromkeys(items))
print(f'普通字典去重: {unique_with_dict}') # [3, 1, 4, 5, 9, 2, 6]
6.3 场景三:配置覆盖记录
from collections import OrderedDict
class ConfigTracker:
"""
追踪配置的来源和覆盖顺序。
后面的配置覆盖前面的,同时记录每个配置值的来源。
"""
def __init__(self):
self._config = OrderedDict()
self._sources = OrderedDict()
def load(self, config_dict, source_name):
"""加载配置,记录来源"""
for key, value in config_dict.items():
self._config[key] = value
# move_to_end确保这个键在最后(最新加载的)
self._config.move_to_end(key)
self._sources[key] = source_name
def get(self, key):
return self._config.get(key)
def get_source(self, key):
return self._sources.get(key, '未设置')
def show_all(self):
"""展示所有配置及其来源"""
print('当前配置(按加载顺序):')
for key, value in self._config.items():
source = self._sources.get(key, 'unknown')
print(f' {key} = {value} (来源: {source})')
# 使用示例
tracker = ConfigTracker()
# 模拟多层配置加载
tracker.load({'host': 'localhost', 'port': 8080, 'debug': True}, '默认配置')
tracker.load({'host': 'dev.server.com', 'debug': False}, '开发环境')
tracker.load({'port': 9090, 'timeout': 30}, '命令行参数')
tracker.show_all()
# host = dev.server.com (来源: 开发环境) ← 被开发环境覆盖了
# debug = False (来源: 开发环境) ← 被开发环境覆盖了
# port = 9090 (来源: 命令行参数) ← 被命令行覆盖了
# timeout = 30 (来源: 命令行参数) ← 新增
6.4 场景四:有序JSON保持
from collections import OrderedDict
import json
# JSON规范不保证键的顺序,但有时我们需要保持顺序
# 用OrderedDict构建响应数据结构
response = OrderedDict()
response['status'] = 'success'
response['code'] = 200
response['data'] = OrderedDict()
response['data']['user'] = OrderedDict()
response['data']['user']['id'] = 12345
response['data']['user']['name'] = '小明'
response['data']['user']['email'] = 'xiaoming@example.com'
response['data']['token'] = 'eyJhbGciOiJIUzI1NiJ9...'
response['timestamp'] = '2025-06-01T12:00:00Z'
# 转JSON(键的插入顺序会被保留)
json_str = json.dumps(response, ensure_ascii=False, indent=2)
print(json_str)
# 解析JSON时也可以保持顺序
json_data = '{"c": 3, "a": 1, "b": 2}'
parsed = json.loads(json_data, object_pairs_hook=OrderedDict)
print(f'类型: {type(parsed)}') # <class 'collections.OrderedDict'>
print(f'内容: {parsed}') # OrderedDict([('c', 3), ('a', 1), ('b', 2)])
七、OrderedDict vs Python 3.7+普通字典
7.1 什么时候仍需OrderedDict
from collections import OrderedDict
# Python 3.7+的普通字典已经保序了
d = {}
d['a'] = 1
d['b'] = 2
d['c'] = 3
print(list(d.keys())) # ['a', 'b', 'c']——按插入顺序
# 那OrderedDict还有什么用?
# 1. 需要move_to_end——普通字典没有这个方法
# 2. 需要popitem(last=False)——弹出第一个元素
# 3. 需要基于顺序的相等性比较
# 4. 需要兼容Python 3.6及更早版本
# 5. 需要向你的代码读者明确传达"顺序很重要"的意图
# 性能对比
import time
n = 100000
# OrderedDict有一些额外的开销(维护双向链表)
start = time.perf_counter()
for _ in range(100000):
od = OrderedDict(zip(range(100), range(100)))
_ = od.keys()
print(f'OrderedDict: {time.perf_counter() - start:.3f}s')
start = time.perf_counter()
for _ in range(100000):
d = dict(zip(range(100), range(100)))
_ = d.keys()
print(f'普通dict: {time.perf_counter() - start:.3f}s')
# 普通字典通常更快一些
7.2 选型建议
| 需求 | 推荐 | 理由 |
|---|---|---|
| 基本键值映射 | dict | 最简单、最快 |
| 需要记住插入顺序(Python3.7+) | dict | 已经内置保序 |
| 需要记住插入顺序(Python3.6-) | OrderedDict | 老版本不保序 |
| 需要LRU缓存 | OrderedDict | move_to_end是核心 |
| 需要弹出最旧条目 | OrderedDict | popitem(last=False) |
| 需要频繁的键不存在处理 | defaultdict | 自动默认值 |
| 需要用顺序传达语义 | OrderedDict | 代码意图更明确 |
| 需要最佳性能 | dict | 开销最小 |
八、综合实战:用defaultdict + OrderedDict构建高级数据结构
8.1 带时间窗口的频率计数器
from collections import defaultdict, OrderedDict
import time
class TimeWindowCounter:
"""
带时间窗口的计数器。
用OrderedDict记录每次事件的时间戳,
查询时自动剔除超出时间窗口的旧数据。
按事件类型分组,每组维护一个时间戳有序字典。
"""
def __init__(self, window_seconds=60):
self.window_seconds = window_seconds
# {event_type: OrderedDict{timestamp: count}}
self._events = defaultdict(OrderedDict)
def record(self, event_type):
"""记录一个事件"""
now = time.time()
bucket = int(now) # 按秒分桶
od = self._events[event_type]
od[bucket] = od.get(bucket, 0) + 1
# 清理过期数据(也可以在查询时清理)
self._clean(event_type)
def count(self, event_type):
"""查询指定事件类型在时间窗口内的总次数"""
self._clean(event_type)
od = self._events[event_type]
return sum(od.values())
def _clean(self, event_type):
"""清理超出时间窗口的旧数据"""
now = time.time()
cutoff = int(now) - self.window_seconds
od = self._events[event_type]
# 从最旧的开始检查,移除过期的
expired_keys = [k for k in od if k < cutoff]
for k in expired_keys:
del od[k]
def stats(self):
"""获取所有事件类型的统计"""
result = {}
for event_type in self._events:
count = self.count(event_type)
if count > 0:
result[event_type] = count
return result
# 使用示例
counter = TimeWindowCounter(window_seconds=5)
# 模拟事件
counter.record('page_view')
counter.record('page_view')
counter.record('click')
counter.record('page_view')
print(f'当前统计: {counter.stats()}')
print('等待3秒...')
time.sleep(3)
counter.record('page_view')
counter.record('click')
print(f'3秒后统计: {counter.stats()}')
print('再等待3秒(第一批数据过期)...')
time.sleep(3)
print(f'过期后统计: {counter.stats()}')
# 前3条page_view和1条click可能已过期
8.2 多级分组 + 顺序保持
from collections import defaultdict, OrderedDict
class HierarchicalGrouper:
"""
多级分组器。
外层用defaultdict支持任意分组维度,
内层用OrderedDict保持数据插入顺序。
"""
def __init__(self):
# 第一级:类别 → 第二级分组
self._groups = defaultdict(OrderedDict)
def add(self, primary_key, secondary_key, item):
"""添加条目"""
if secondary_key not in self._groups[primary_key]:
self._groups[primary_key][secondary_key] = []
self._groups[primary_key][secondary_key].append(item)
def get_primary(self, primary_key):
"""获取某个主类别的所有数据(按子类别顺序)"""
if primary_key not in self._groups:
return OrderedDict()
return self._groups[primary_key]
def get_secondary(self, primary_key, secondary_key):
"""获取某个子类别的数据"""
return self._groups[primary_key].get(secondary_key, [])
def iter_hierarchical(self):
"""按层次遍历所有数据"""
for primary in sorted(self._groups):
print(f'\n📁 {primary}:')
for secondary, items in self._groups[primary].items():
print(f' ├─ {secondary}: {items}')
# 使用示例:组织项目文件结构
grouper = HierarchicalGrouper()
# 模拟文件
files = [
('src', 'models', 'user.py'),
('src', 'models', 'product.py'),
('src', 'views', 'home.py'),
('src', 'views', 'dashboard.py'),
('tests', 'unit', 'test_user.py'),
('tests', 'unit', 'test_product.py'),
('tests', 'integration', 'test_api.py'),
('docs', 'api', 'reference.md'),
('docs', 'guide', 'getting_started.md'),
('src', 'models', 'order.py'), # 后添加的
]
for category, subcategory, filename in files:
grouper.add(category, subcategory, filename)
grouper.iter_hierarchical()
# 可以看到:
# - 三大主类别按字母序排列
# - 每个主类别下的子类别按插入顺序排列
# - 每个子类别下的文件按插入顺序排列
九、常见陷阱与注意事项
9.1 defaultdict的序列化问题
from collections import defaultdict
import json
# ⚠️ defaultdict不能直接用json序列化
dd = defaultdict(int)
dd['a'] = 1
dd['b'] = 2
# json.dumps(dd) # TypeError: Object of type defaultdict is not JSON serializable
# ✅ 解决方案:转成普通字典
json_str = json.dumps(dict(dd))
print(json_str) # {"a": 1, "b": 2}
# 或者自定义编码器
class DefaultDictEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, defaultdict):
return dict(obj)
return super().default(obj)
json_str = json.dumps(dd, cls=DefaultDictEncoder)
print(json_str) # {"a": 1, "b": 2}
9.2 defaultdict的拷贝行为
from collections import defaultdict
import copy
dd = defaultdict(list, {'a': [1, 2], 'b': [3]})
# 浅拷贝——default_factory被保留
dd_shallow = copy.copy(dd)
print(type(dd_shallow)) # <class 'collections.defaultdict'>
print(dd_shallow.default_factory) # <class 'list'>
# 转成普通字典——default_factory丢失
dd_as_dict = dict(dd)
print(type(dd_as_dict)) # <class 'dict'>
# 没有default_factory了
# ⚠️ 如果你需要保留default_factory但想要独立的数据
dd_deep = copy.deepcopy(dd)
print(type(dd_deep)) # <class 'collections.defaultdict'>
print(dd_deep.default_factory) # <class 'list'>
# 深拷贝保留了一切,且数据完全独立
9.3 pickle序列化OrderedDict
from collections import OrderedDict
import pickle
# OrderedDict可以正常序列化
od = OrderedDict([('a', 1), ('b', 2)])
data = pickle.dumps(od)
od_restored = pickle.loads(data)
print(type(od_restored)) # <class 'collections.OrderedDict'>
print(od_restored) # OrderedDict([('a', 1), ('b', 2)])
十、本篇小结
collections模块中的两个字典变体,各自解决了普通字典的短板:
defaultdict:
- 核心价值:自动为不存在的键创建默认值
- 创建方式:
defaultdict(工厂函数),工厂函数是一个无参数的可调用对象 - 常用工厂:
list(分组)、int(计数)、set(去重收集)、float(累加)、dict(嵌套字典) - 原理:
__missing__钩子方法 - 注意:访问不存在的键会创建它,
get()不会触发默认值 - 注意:工厂函数必须是无参数的
OrderedDict:
- 核心价值:记住键的插入顺序(Python 3.7+普通字典也保序了,但OrderedDict有额外能力)
- 独特方法:
move_to_end(key, last=True)——移动键到末尾/开头 - 独特方法:
popitem(last=True)——弹出最后一个/第一个 - 相等性:既比较内容也比较顺序
- 经典应用:LRU缓存(用move_to_end + popitem)
- 经典应用:去重保序(用fromkeys)
- Python 3.7+普通字典保序后,大部分场景用dict就够了,但LRU等场景OrderedDict仍然不可替代
学完字典体系(普通字典、推导式、合并、defaultdict、OrderedDict),你已经掌握了Python中最常用也最强大的数据结构。从下一篇开始,我们将进入一个新的数据类型——集合set,学习它独特的去重和集合运算能力。
以上就是Python字典中defaultdict与OrderedDict的使用详解的详细内容,更多关于Python字典defaultdict与OrderedDict使用的资料请关注脚本之家其它相关文章!
