在Python日常开发中,普通字典能满足大部分键值映射需求,但遇到分组、计数或需要精确控制键顺序的场景时,代码往往会变得冗长且易错。collections模块提供了defaultdict和OrderedDict两个字典变体,分别解决“自动初始化默认值”和“保持插入顺序并支持重排”的问题。本文通过大量代码实例,深入讲解它们的原理、工厂函数选择、实战场景以及注意事项。
一、defaultdict:自动初始化默认值的字典
普通字典在追加数据时,需要先检查键是否存在。例如按首字母分组单词,传统写法必须使用if key not in dict: dict[key] = [],代码重复且不直观。defaultdict是dict的子类,创建时需传入一个无参数的工厂函数(如list、int、set等)。当访问的键不存在时,自动调用工厂函数生成默认值并存入字典。
典型用法:- from collections import defaultdict
- # 默认值为空列表,用于分组
- dd = defaultdict(list)
- dd['a'].append(1) # 键'a'不存在,自动创建空列表
- # 默认值为0,用于计数
- dd = defaultdict(int)
- dd['count'] += 1 # 自动初始化为0后加1
- # 默认值为空集合,用于去重收集
- dd = defaultdict(set)
- dd['vowels'].add('a')
- # 默认值为空字典,用于嵌套
- dd = defaultdict(dict)
- dd['user1']['name'] = 'Alice'
- # 自定义默认值
- dd = defaultdict(lambda: {'count': 0, 'items': []})
复制代码
⚠️ 工厂函数必须是无参数的可调用对象。defaultdict([])会报错,正确写法是defaultdict(list)。另外,defaultdict的__missing__方法在键不存在时被触发,但dict.get()方法不会触发工厂函数,因此get()返回None且不会创建键。
二、defaultdict实战场景
1. 单词频率统计- text = "Python is an interpreted high-level programming language..."
- word_count = defaultdict(int)
- for word in text.lower().split():
- word = word.strip('.,;!?')
- if word:
- word_count[word] += 1
复制代码
2. 按多种条件分组- students = [
- {'name': '小明', 'grade': 'A', 'class': '1班', 'score': 95},
- # ...
- ]
- by_grade = defaultdict(list)
- by_class = defaultdict(list)
- for s in students:
- by_grade[s['grade']].append(s['name'])
- by_class[s['class']].append(s)
- # 嵌套defaultdict实现两级分组
- by_grade_and_class = defaultdict(lambda: defaultdict(list))
- for s in students:
- by_grade_and_class[s['grade']][s['class']].append(s['name'])
复制代码
3. 构建倒排索引- documents = {1: "Python is great", 2: "Java is also great"}
- inverted_index = defaultdict(lambda: defaultdict(list))
- for doc_id, text in documents.items():
- for pos, word in enumerate(text.lower().split()):
- word = word.strip('.,;:!?')
- inverted_index[word][doc_id].append(pos)
- def search(query):
- words = query.lower().split()
- if not words:
- return []
- result = set(inverted_index[words[0]].keys())
- for w in words[1:]:
- result &= set(inverted_index[w].keys())
- return sorted(result)
复制代码
4. 模拟SQL GROUP BY聚合- orders = [
- {'product': '手机', 'category': '电子产品', 'amount': 2999, 'quantity': 1},
- # ...
- ]
- category_stats = defaultdict(lambda: {'total_amount': 0, 'total_quantity': 0, 'products': set()})
- for o in orders:
- cat = o['category']
- s = category_stats[cat]
- s['total_amount'] += o['amount'] * o['quantity']
- s['total_quantity'] += o['quantity']
- s['products'].add(o['product'])
复制代码
三、OrderedDict:保留插入顺序并能重排的字典
Python 3.7+ 的普通字典已经保证插入顺序,但OrderedDict仍然提供两个独有能力:move_to_end(key, last) 可以移动键到末尾或开头,以及相等的OrderedDict在比较时考虑顺序(普通字典不考虑)。
- from collections import OrderedDict
- od = OrderedDict()
- od['a'] = 1
- od['b'] = 2
- od['c'] = 3
- # 移动键'b'到末尾
- od.move_to_end('b')
- print(od) # OrderedDict([('a', 1), ('c', 3), ('b', 2)])
- # 移动到开头
- od.move_to_end('b', last=False)
- print(od) # OrderedDict([('b', 2), ('a', 1), ('c', 3)])
- # 有序比较
- od1 = OrderedDict([('a',1), ('b',2)])
- od2 = OrderedDict([('b',2), ('a',1)])
- print(od1 == od2) # False,因为顺序不同
复制代码
四、OrderedDict实战场景
1. 实现LRU缓存(最近最少使用)- class LRUCache:
- def __init__(self, capacity):
- self.capacity = capacity
- self.cache = OrderedDict()
- def get(self, key):
- if key not in self.cache:
- return -1
- 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:
- self.cache.popitem(last=False) # 移除最久未使用的(开头)
复制代码
2. 去重但保留首次出现顺序- items = ['apple', 'banana', 'apple', 'orange', 'banana']
- seen = OrderedDict.fromkeys(items)
- print(list(seen.keys())) # ['apple', 'banana', 'orange']
- # 或者用defaultdict + OrderedDict组合
- order = OrderedDict()
- for item in items:
- if item not in order:
- order[item] = len(order)
- print(list(order.keys()))
复制代码
3. 配置覆盖记录- base = OrderedDict([('host', 'localhost'), ('port', 8080)])
- overrides = OrderedDict([('port', 9090), ('debug', True)])
- merged = OrderedDict(base)
- merged.update(overrides) # 保持base顺序,覆盖的项在末尾
- print(merged)
- # OrderedDict([('host', 'localhost'), ('port', 9090), ('debug', True)])
复制代码
4. 保持JSON字段顺序
利用json.dumps时指定object_pairs_hook=OrderedDict,可以保持JSON对象中字段的原始顺序。
五、综合实战:带时间窗口的频率计数器(结合defaultdict和OrderedDict)
- from collections import defaultdict, OrderedDict
- import time
- class TimeWindowCounter:
- def __init__(self, window_size=60):
- self.window_size = window_size # 秒
- self.data = defaultdict(OrderedDict)
- def record(self, key, timestamp=None):
- if timestamp is None:
- timestamp = int(time.time())
- window_start = (timestamp // self.window_size) * self.window_size
- if window_start not in self.data[key]:
- self.data[key][window_start] = 0
- self.data[key][window_start] += 1
- # 清理过期窗口(保留最近两个窗口)
- windows = list(self.data[key].keys())
- while len(windows) > 2:
- del self.data[key][windows.pop(0)]
- def get_count(self, key):
- return sum(self.data[key].values())
复制代码
六、注意事项
- defaultdict的键访问会“悄悄”创建键,如果不希望这样,请使用dict.get()。
- 工厂函数必须可调用且无参数,例如defaultdict(list)而不是defaultdict([])。
- 在需要序列化(如pickle)时,defaultdict的默认工厂函数必须可pickle;lambda无法序列化,建议使用内置函数或自定义可pickle的函数。
- 拷贝defaultdict时,默认工厂函数也会被拷贝,但内部键值对是浅拷贝。
- OrderedDict在Python 3.7+并不是完全被替代,当你需要move_to_end、popitem(last)或顺序敏感比较时,仍需使用OrderedDict。
七、小结
defaultdict和OrderedDict是Python collections模块中两个强大的字典变体。defaultdict通过工厂函数自动处理缺失键,极大简化分组、计数和聚合代码;OrderedDict在保留顺序的基础上提供了键重排功能,适合缓存、配置管理等场景。合理使用它们能写出更简洁、更健壮的Python代码。 |