当前位置:

首页 > 编程开发 > Python字典中defaultdict与OrderedDict的使用详解

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

defaultdict是collections模块中dict的子类,通过工厂函数在键不存在时自动生成默认值,避免了手动初始化。支持list、int、set等类型,内部依赖__missing__方法实现。常用于分组、计数、嵌套字典等场景,代码更简洁高效,减少冗余逻辑,显著提升可读性。

一、开篇:普通字典不够用时怎么办?

前面我们刚刚聊完字典的基础操作——创建、增删改查、遍历、推导式和合并。现在你已经能用字典处理大部分日常开发中的数据映射需求了。但在某些特定场景下,普通字典会暴露出两个让人头疼的问题:

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

问题一:每次往字典里追加数据,都得先检查键是否存在。比如你要按类别分组,每遇到一个新品类别,都得写if key not in dict: dict[key] = [],代码又臭又长。

# 没有defaultdict之前——很啰嗦
words = ['apple', 'banana', 'a vocado', '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(, {'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():
    a vg = sum(m['score'] for m in members) / len(members)
    print(f'  {cls}: 平均分{a vg:.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: "Ja va is also a programming language",
    3: "Python and Ja va 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)}')  # 
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))  # 
print(dd_shallow.default_factory)  # 

# 转成普通字典——default_factory丢失
dd_as_dict = dict(dd)
print(type(dd_as_dict))  # 
# 没有default_factory了

# ⚠️ 如果你需要保留default_factory但想要独立的数据
dd_deep = copy.deepcopy(dd)
print(type(dd_deep))  # 
print(dd_deep.default_factory)  # 
# 深拷贝保留了一切,且数据完全独立

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))  # 
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
相关文章 更多
C++动态数组初始化怎么写?常用语句与代码示例
C++动态数组初始化怎么写?常用语句与代码示例

深入解析C++中动态数组的初始化机制,涵盖new操作符的不同用法、基本类型与类对象的初始化差异,以及为何在现代C++开发中应优先使用std::vector。

Python安装后怎么打开:使用IDLE或命令行启动解释器
Python安装后怎么打开:使用IDLE或命令行启动解释器

刚在Windows安装好Python却不知道如何启动?本文详细演示如何通过开始菜单找到并打开IDLE集成开发环境,以及如何在PowerShell或命令提示符中使用python和py命令启动交互式解释器、运行.py脚本文件。包含退出解释器的方法及常见启动问题排查,帮助初学者快速验证安装成功并开始编写代码。

Windows系统Python安装教程:下载、勾选PATH及环境变量配置
Windows系统Python安装教程:下载、勾选PATH及环境变量配置

针对Windows初学者的Python安装实战指南。详细讲解如何从Python官网下载匹配架构的安装包,重点演示安装首屏勾选“Add python.exe to PATH”的关键操作,并提供使用python --version和py命令验证环境变量的具体步骤,帮助新手快速搭建开发环境并排查路径问题。

麒麟OS如何查看Python进程的运行状态
麒麟OS如何查看Python进程的运行状态

要想确认麒麟OS中Python程序的运行状态以及资源占用情况,我们可以这样做:用ps -ef | grep python来筛选进程;通过top命令,按P键排序查看实时负载;使用pgrep -f "script.py"精准获取PID;借助lsof -p PID验证文件打开状态。另外,还可以结合syst

Python在Debian上如何配置SSL证书
Python在Debian上如何配置SSL证书

在Debian系统上配置SSL证书通常涉及以下几个步骤:安装Web服务器:首先,你需要一个Web服务器,比如Apache或Nginx。这里以Apache为例。sudo apt updatesudo apt install apache2获取SSL证书:你可以从Let’s Encrypt免费获取SSL

统信UOS怎么安装Python开发环境
统信UOS怎么安装Python开发环境

要想让Python项目在统信UOS上正常运行,得先安装python3、python3-pip、python3-venv、python3-dev以及build-essential等组件。具体操作就是执行sudo apt install命令来一步到位完成安装,同时别忘了配置清华镜像源来给pip加速哦。在

纯Python方案实现中英文全文搜索
纯Python方案实现中英文全文搜索

在互联网上的各类网站中,无论大小,基本上都会有一个搜索框,用来给用户对内容进行搜索,小到站点搜索,大到搜索引擎搜索。从简单的来说,搜索功能确实很简单,一个简单的select语句就可以实现数据的搜索。而从复杂的来看,无论是搜索的精度还是搜索的效率,都是有很深的研究范围的。对于简单的搜索功能来说,一个s

Mac如何取消通过Python脚本运行的关机程序
Mac如何取消通过Python脚本运行的关机程序

立即在终端输入sudo shutdown -c取消倒计时关机,成功后显示“Shutdown cancelled”;若存在pmset重复任务,需再执行sudo pmset repeat cancel清除。Mac因Python脚本执行了os.system("sudo shutdown -h +10")或

Pythonasyncio异步并发与多固定出口IP调度实战
Pythonasyncio异步并发与多固定出口IP调度实战

之前写过一篇同步场景下用 Python 管理多个固定出口 IP 的实践(ExitPool + requests/httpx),覆盖了健康检查、故障转移和连接池复用。但在实际业务中,越来越多的场景用 asyncio 做高并发采集或批量接口调用——异步事件循环下多出口的管理方式和同步场景完全不同:单线程

Python在静态出口IP产品中的实战:从地址漂移巡检到多IP故障切换
Python在静态出口IP产品中的实战:从地址漂移巡检到多IP故障切换

写在前面:为什么静态出口 IP 不是"买了就行"不少团队在引入静态出口 IP 产品时,第一反应往往是:“地址配上去,这事就算完了。”可真到了真实业务里,静态出口 IP 真正能体现价值的地方,往往不在分配这一步,而在分配之后怎么管:地址有没有漂移,质量是否达标,某一条线路突然不可用时怎么切换,连接层又

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。