当前位置:

首页 > 编程开发 > 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模块给我们准备了两个超级好用的字典变体——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(, {'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,学习它独特的去重和集合运算能力。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Python
    相关文章 更多
    python删除一个列表里的重复元素的方法与代码示例
    python删除一个列表里的重复元素的方法与代码示例

    学习如何使用Python高效删除列表中的重复元素。本文提供5种常见方法的代码示例,涵盖保持顺序与不保持顺序的场景,适合初学者和进阶开发者参考。

    Python写接口的步骤及完整代码示例
    Python写接口的步骤及完整代码示例

    详细讲解使用Python编写Web接口的核心步骤与完整代码示例,涵盖环境准备、路由定义、数据校验、服务启动及调试测试方法。

    codex怎么读取word文档及docx文件操作教程
    codex怎么读取word文档及docx文件操作教程

    介绍如何利用编程工具读取和解析Word文档(docx格式),涵盖基础文本提取代码示例、结构化数据处理逻辑,以及处理复杂排版时的技术局限与替代方案。

    怎么在Python中调用C语言代码提升性能
    怎么在Python中调用C语言代码提升性能

    详细讲解在Python中调用C语言代码提升性能的完整实践指南,包含原理分析、编译步骤、ctypes与CFFI具体实现、性能对比及常见陷阱。

    Python与C语言的性能差异及底层原理
    Python与C语言的性能差异及底层原理

    深入解析Python与C语言在执行效率上的数量级差异,从编译机制、变量内存布局、动态类型推断到GIL锁,系统阐明底层核心原理。

    Python和C语言哪个更适合编程入门
    Python和C语言哪个更适合编程入门

    客观对比Python与C语言在心智负担、底层机制与学习反馈上的核心差异,提供清晰有据的选择标准与切实的入门学习路径。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    langgraph python guide中文入门教程详解
    langgraph python guide中文入门教程详解

    本篇LangGraph Python中文入门教程详解,系统梳理StateGraph状态机定义、节点编写、边与条件路由、检查点持久化(MemorySaver)等核心机制,提供从环境安装到完整循环智能体构建的代码示例与实操指导。

    Windows 安装 Python:从官方入口到虚拟环境的可复现实践
    Windows 安装 Python:从官方入口到虚拟环境的可复现实践

    面向后端开发者,基于 Python 官方文档说明如何在 Windows 上安装 Python、创建 venv 虚拟环境及使用 pip 管理依赖。包含可复制命令、验证方法及工具选择边界,避免常见配置误区。

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

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

    Windows
    Windows

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

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。