深入理解 Python 的 defaultdict

1. defaultdict 简介

defaultdict 是 Python 标准库 collections 模块中的一个类,它扩展了普通字典(dict)的功能。通过使用 defaultdict,可以在尝试访问字典中不存在的键时自动创建默认值,这极大地简化了某些编程模式。

2. defaultdict 的基本语法和初始化

在深入探讨 defaultdict 的高级用法和应用场景之前,理解其基本语法和初始化过程是非常重要的。这有助于确保在实际使用中能够正确且有效地利用 defaultdict 提供的功能。

1. 引入 defaultdict

首先,要使用 defaultdict,需要从 collections 模块中导入它:

from collections import defaultdict

2. 创建 defaultdict 对象

defaultdict 的构造函数需要一个参数:一个无参数的工厂函数,该函数决定了当访问不存在的键时,字典应该返回的默认值。

2.1 初始化示例

下面是几种常见的初始化方式:

使用 list 作为默认工厂函数
from collections import defaultdict

# 使用 list 作为默认工厂函数
dd_list = defaultdict(list)
dd_list['a'].append(3)
dd_list['a'].append(3)
dd_list['b'].append('three')
print(dd_list)
print(type(dd_list))

在这里插入图片描述

使用 int 作为默认工厂函数
from collections import defaultdict

# 使用 int 作为默认工厂函数
dd_int = defaultdict(int)
# dd_int['a'].append(2) # 报错:AttributeError: 'int' object has no attribute 'append'
dd_int['a'] = 1
dd_int['b'] = 2
dd_int['b'] = [3,'aa'] # 注意用=操作会重设类型
print(dd_int)
print(type(dd_int))

在这里插入图片描述

使用 set 作为默认工厂函数
from collections import defaultdict

# 使用 set 作为默认工厂函数
dd_set = defaultdict(set)
dd_set['a'].add(2)
dd_set['b'].add(3)
dd_set['b'].add(4)
print(dd_set)
print(type(dd_set))

在这里插入图片描述

3. 访问和修改元素

defaultdict 的行为在很多方面与普通字典相似。你可以像使用普通字典那样添加、修改或访问元素。

3.1 添加和访问元素
# 添加元素
from collections import defaultdict

dd_list = defaultdict(list)
dd_list['a'].append(1)
dd_list['a'].append(2)

# 访问元素
print(dd_list['a'])  # 输出: [1, 2]

# 访问不存在的键
print(dd_list['b'])  # 输出: [],自动创建空列表

在这里插入图片描述

3.2 修改元素
# 添加元素
from collections import defaultdict

dd_int = defaultdict(int)

dd_int['key1'] = 2

# 增加计数
dd_int['key1'] += 1
print(dd_int['key1'])  # 输出: 1

# 访问不存在的键,自动初始化为 0 并加 1
dd_int['key2'] += 1
print(dd_int['key2'])  # 输出: 1

在这里插入图片描述

4. 默认值的行为

当你访问 defaultdict 中不存在的键时,defaultdict 会自动调用工厂函数来创建一个默认值,并将这个键和默认值作为键值对存入字典。

5. 利用工厂函数的灵活性

可以利用更复杂的工厂函数来实现特定的默认值行为,例如,返回复杂的数据结构或执行更复杂的初始化逻辑。

5.1 自定义工厂函数示例
from collections import defaultdict


def complex_factory():
    return {"count": 0, "total": 0}


dd_complex = defaultdict(complex_factory)

print(dd_complex)
# defaultdict(<function complex_factory at 0xffff87348280>, {})

print(dd_complex['anything'])
# {'count': 0, 'total': 0}

print(dd_complex)
# defaultdict(<function complex_factory at 0xffff87348280>, {'anything': {'count': 0, 'total': 0}})

# 使用自定义默认值
dd_complex['product1']['count'] += 1
dd_complex['product1']['total'] += 59.99

print(dd_complex['product1'])  
# 输出: {'count': 1, 'total': 59.99}

在这里插入图片描述

了解这些基本语法和概念之后,你就可以更好地理解 defaultdict 的高级应用,并将其有效地应用在各种编程场景中。

3. defaultdict 的应用场景

3.1 分组统计

defaultdict 特别适用于需要将数据分组的场景。例如,根据某个属性将数据分类并存储到列表中。

示例代码:根据首字母分组单词
from collections import defaultdict

words = ['apple', 'banana', 'cherry', 'date', 'apricot', 'blueberry', 'almond']
grouped_words = defaultdict(list)

for word in words:
    first_letter = word[0]
    grouped_words[first_letter].append(word)

print(grouped_words)

在这里插入图片描述

3.2 计数器

使用 int 作为默认工厂函数,可以快速实现一个计数器。

示例代码:计数列表中元素的出现次数
from collections import defaultdict

fruits = ['apple', 'banana', 'cherry', 'apple', 'cherry', 'cherry', 'banana']
fruit_count = defaultdict(int)

for fruit in fruits:
    fruit_count[fruit] += 1

print(fruit_count)

在这里插入图片描述

3.3 构建多级字典(将lambda: defaultdict(list)作为工厂函数)(有点复杂,给我整懵了😨)

当需要构建多级字典时,defaultdict 的嵌套使用可以避免检查上级字典中键是否存在的麻烦。

示例代码:存储学生成绩
from collections import defaultdict

# grades = defaultdict(lambda: defaultdict(list))
grades = defaultdict()


# 添加数据
grades['Class 1']['Alice'].append(88)
grades['Class 1']['Bob'].append(90)
grades['Class 2']['Charlie'].append(85)

print(grades)

lambda: defaultdict(list)作为工厂函数的作用

在这段代码中,lambda: defaultdict(list) 扮演了非常关键的角色,它是 defaultdict 构造函数的参数,用作工厂函数。这个工厂函数决定了当访问不存在的键时 defaultdict 应如何自动创建和初始化其值。

当你首次访问一个不存在的键时,例如 grades['Class 1']defaultdict 需要一个值来与这个键关联。如果没有提供工厂函数,Python 将会抛出一个 KeyError。但是,通过提供 lambda: defaultdict(list)defaultdict 会自动调用这个 lambda 函数来生成一个新的 defaultdict(list) 作为默认值。

这个过程的具体步骤如下:

  1. 创建 defaultdict(list) 实例:当 lambda 被调用时,它创建并返回一个新的 defaultdict(list)。这个新的 defaultdict 用于存储具体的学生名字和其成绩列表。

  2. 多级嵌套:由于最外层的 defaultdict 使用了 lambda: defaultdict(list) 作为工厂函数,当访问如 grades['Class 1'] 时,如果 ‘Class 1’ 不存在,将会创建一个新的 defaultdict(list) 对象来存储属于 ‘Class 1’ 的学生和成绩数据。这允许在接下来的操作中,可以直接添加学生名字作为键,成绩列表作为值。

  3. 简化数据结构操作:这种方式允许代码以非常简洁的形式进行复杂的数据结构操作。例如,grades['Class 1']['Alice'].append(88) 这行代码在不需要预先检查 ‘Class 1’ 或 ‘Alice’ 是否存在的情况下,直接将 88 添加到 Alice 的成绩列表中。如果 ‘Class 1’ 或 ‘Alice’ 不存在,defaultdict 会自动创建必要的结构来存储数据。

这种使用方式带来的主要优点是代码的简洁性和健壮性,你不需要在每次添加数据前手动检查每个键是否存在,极大地减少了代码的复杂性和出错概率。这对于需要动态添加数据到多层嵌套结构中的场景特别有用,如统计、分组、建立复杂的数据模型等。

4. defaultdict 的高级应用

4.1 使用自定义工厂函数

除了简单的类型如 listintset,可以定义更复杂的工厂函数来满足特定的需求。这些工厂函数可以是任何无参数的函数,它返回的值将用作字典的默认值。

示例代码:使用复杂的默认值
from collections import defaultdict

def default_value():
    return {'count': 0, 'total': 0}

complex_default = defaultdict(default_value)

# 使用默认值进行计算
complex_default['item1']['count'] += 1
complex_default['item1']['total'] += 250

print(complex_default)

在这里插入图片描述

这种方式特别适合需要初始化多个属性的情况,保证了代码的整洁和易于理解。

4.2 动态属性访问(这个也让我有点懵逼😣)

使用 defaultdict 可以很方便地实现动态的属性访问,尤其是在处理不确定数据源或灵活构造数据结构时非常有用。

示例代码:动态设置和获取属性
from collections import defaultdict


class FlexibleDict(defaultdict):
    def __getattr__(self, key):
        return self[key]

    def __setattr__(self, key, value):
        self[key] = value


flex_dict = FlexibleDict(int)
flex_dict.apple = 10
print(flex_dict['apple'])  # 输出 10
print(flex_dict.orange)    # 输出 0,使用默认 int 工厂

print(flex_dict)
# FlexibleDict(<class 'int'>, {'apple': 10, 'orange': 0})

在这里插入图片描述

这个动态设置和获取属性有什么用?

这个示例中的 FlexibleDict 类通过扩展 defaultdict 并重写 __getattr____setattr__ 方法,使得字典可以像访问对象属性一样方便地设置和获取键值对。这种方式的实用性在于提供了更自然和面向对象的语法来操作字典数据,有几个潜在的用途:

1. 代码可读性和简洁性

使用属性访问方式可以使代码看起来更加整洁和易读。比如,flex_dict.appleflex_dict['apple'] 更易于编写和理解,尤其是在处理复杂的数据结构时。

2. 接口一致性

在某些情况下,可能需要将字典与其他使用属性访问的对象一起使用。这种方法可以让字典在语法上表现得和其他对象一致,便于编写统一的代码处理逻辑。

3. 方便的属性管理

重写 __getattr____setattr__ 允许在访问或设置属性时加入额外的逻辑,例如验证数据、自动记录修改日志、触发事件等。这在开发某些应用程序时非常有用,可以使字典的行为更加丰富和灵活。

4. 与对象模型的集成

在某些面向对象的设计中,可能需要将字典作为对象属性动态地存取。这种实现使得字典可以无缝地融入面向对象的设计模式中,而不需要修改现有的类结构。

示例解释

在你提供的代码中,flex_dict 的表现如下:

  • 使用 flex_dict.apple = 10 设置 apple 的值为 10,这是通过重写的 __setattr__ 方法实现的,内部其实是将 apple 作为键,10 作为值存入字典。
  • 使用 print(flex_dict['apple']) 打印 apple 的值,显示为 10,这是标准字典功能。
  • 使用 print(flex_dict.orange) 查询一个尚未设置的键 orange,由于 defaultdict 使用了 int 作为默认工厂函数,返回了 0,并且现在 orange 也被设置为 0。
  • 最后打印 flex_dict 时,显示其内容和结构,这反映了它同时具备字典的特性和通过属性访问的便利。

这种实现提高了字典在某些编程场景中的适用性,尤其是在需要大量动态属性处理的环境中。

4.3 使用 defaultdict 实现图结构

defaultdict 是实现图数据结构中的邻接表的理想选择,可以方便地管理顶点和边。

示例代码:构建无向图的邻接列表
from collections import defaultdict

graph = defaultdict(set)

# 添加边
graph['A'].add('B')
graph['A'].add('C')
graph['B'].add('A')
graph['C'].add('A')

print(graph)

在这里插入图片描述

这样的数据结构非常适合表示复杂的网络关系,如社交网络、交通网等。

关于“无向图的邻接列表”解释

这段代码使用 defaultdict 来构建一个无向图的邻接列表。在图论中,邻接列表是图的一种表示方法,其中每个顶点存储一个列表或集合,这个列表或集合包含与之相邻的所有顶点。在这个示例中,使用 defaultdict(set) 的方式是为了确保每个顶点的邻接顶点不会重复且可以动态增长。

代码详解
  • 初始化图graph = defaultdict(set) 创建了一个 defaultdict,其默认值为一个空的 set。这意味着任何尚未明确添加的键(顶点)在首次访问时会自动关联一个新的空集合。

  • 添加边

    • graph['A'].add('B')graph['A'].add('C') 表示顶点 ‘A’ 与顶点 ‘B’ 和 ‘C’ 相连。
    • graph['B'].add('A') 表示顶点 ‘B’ 与顶点 ‘A’ 相连。
    • graph['C'].add('A') 表示顶点 ‘C’ 与顶点 ‘A’ 相连。

    这些操作通过 add 方法将相邻顶点添加到各顶点对应的集合中。由于使用的是集合(set),这保证了即便尝试添加重复的边也不会在邻接列表中重复出现相同的顶点。

  • 打印图:最后一行 print(graph) 输出图的邻接列表。由于是无向图,所以每对顶点间的连接在列表中是双向的(例如,‘A’ 连接到 ‘B’,同时 ‘B’ 也连接到 ‘A’)。

示例输出解释

输出可能看起来类似这样:

defaultdict(<class 'set'>, {'A': {'B', 'C'}, 'B': {'A'}, 'C': {'A'}})

这表示:

  • 顶点 ‘A’ 与顶点 ‘B’ 和 ‘C’ 相连。
  • 顶点 ‘B’ 与顶点 ‘A’ 相连。
  • 顶点 ‘C’ 与顶点 ‘A’ 相连。

用途

这种数据结构的用途非常广泛,适用于需要表示和处理图结构的任何场景,比如社交网络分析、网络拓扑、路径寻找算法、推荐系统等。defaultdict(set) 的使用简化了图的构建过程,自动处理了边的添加和重复边的排除,使得代码更简洁、更易于维护。

5. 性能优化和注意事项

5.1 性能优化

虽然 defaultdict 提供了方便的默认值管理,但其在处理非常大的数据集时,可能会略微影响性能。在性能敏感的应用中,适当地使用普通字典和手动管理键的存在性可能更加高效。

5.2 注意事项

  • 在使用 defaultdict 时,如果不需要默认值功能,应考虑回退到普通的字典,以避免不必要的性能损耗。
  • 使用自定义工厂函数时,确保它们的逻辑简单且不会引入副作用。

6. 总结

defaultdict 在 Python 中是一个非常强大的工具,特别适合用于需要自动处理缺失键的场景。通过合理使用 defaultdict,可以大大简化代码,提高开发效率。不过,应当根据实际需求选择合适的数据结构,以确保程序的性能和可维护性。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐