Python字典中defaultdict与OrderedDict的使用详解

 更新时间:2026年06月14日 09:42:44   作者:星河耀银海  
这篇文章主要为大家详细介绍了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模块给我们准备了两个超级好用的字典变体——defaultdictOrderedDict,专门解决这两类场景。今天这篇文章,我就带你彻底掌握它们。

二、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缓存OrderedDictmove_to_end是核心
需要弹出最旧条目OrderedDictpopitem(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使用的资料请关注脚本之家其它相关文章!

相关文章

  • 解决jupyter加载文件失败的问题

    解决jupyter加载文件失败的问题

    这篇文章主要介绍了解决jupyter加载文件失败的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • Python编程如何在递归函数中使用迭代器

    Python编程如何在递归函数中使用迭代器

    今天下午想要复现一下学长的recursion file,想模仿源码里的精髓:迭代器遇到了bug,花了一两个小时才解决。现总结如下,有需要的朋友也可借鉴参考下
    2021-09-09
  • Python爬虫之对CSDN榜单进行分析

    Python爬虫之对CSDN榜单进行分析

    这篇文章主要介绍了Python爬虫之对CSDN榜单进行分析,文章有详细代码,简单易懂,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2023-01-01
  • Python生成字符视频的实现示例

    Python生成字符视频的实现示例

    在之前也写过生成字符视频的文章,但是使用的是命令行窗口输出,效果不是很好,而且存在卡顿的情况,所以本文介绍了mp4的字符视频,感兴趣的可以了解一下
    2021-05-05
  • Python内置模块hashlib、hmac与uuid用法分析

    Python内置模块hashlib、hmac与uuid用法分析

    这篇文章主要介绍了Python内置模块hashlib、hmac与uuid用法,结合实例形式较为详细的分析了hashlib、hmac与uuid模块的概念、功能及简单使用方法,需要的朋友可以参考下
    2018-02-02
  • Pycharm鼠标光标切换实现方式

    Pycharm鼠标光标切换实现方式

    文章介绍了在使用Windows 10时,如果光标变成特殊形状(如箭头带有一个问号),可以通过点击键盘上的Insert键来切换回正常光标,此外,还介绍了如何使用快捷键(Win+Shift+S)来截取屏幕
    2026-01-01
  • python自动发微信监控报警

    python自动发微信监控报警

    这篇文章主要为大家详细介绍了python自动发微信监控报警,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-09-09
  • selenium+Chrome滑动验证码破解二(某某网站)

    selenium+Chrome滑动验证码破解二(某某网站)

    这篇文章主要介绍了selenium+Chrome滑动验证码破解二(某某网站),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • 一文详解Python数据类型:数字、布尔与None的语义

    一文详解Python数据类型:数字、布尔与None的语义

    在 Python 中,一切皆对象,每个对象都属于特定的数据类型,理解数据类型的特性和行为是编写高质量 Python 代码的关键,本文将深入探讨 Python 的核心数据类型:数字、布尔值和 None,揭示它们背后的设计哲学和使用技巧
    2025-11-11
  • Python 将json序列化后的字符串转换成字典(推荐)

    Python 将json序列化后的字符串转换成字典(推荐)

    这篇文章主要介绍了Python 将json序列化后的字符串转换成字典,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-01-01

最新评论