Skip to main content

字典与集合

创建字典

字典(dict)是一种用途广泛的数据类型。在其它编程语言中,类似的数据结构或容器也会被叫做 Map、映射表、哈希表、散列表等。

使用大括号

字典是一种集合,集合中每个元素是一对“键”和“值”。在表示字典的时候,字典本身是使用大括号包裹,内部的元素使用逗号分隔,每个元素内部的键和值通过冒号 : 分隔。创建一个字典最简单的方法是使用大括号,包所需键值数据包裹起来即可:

# 创建一个空字典
empty_dict = {}

# 创建字典有内容的字典
person = {
"姓名": "杜子腾",
"年龄": 30,
"城市": "Pythora"
}

需要注意的是:

  • 字典的键必须是不可变类型,如整数、浮点数、字符串或元组。列表或其他字典不能作为键使用。这是为了避免键被修改,引起查找字典时出现混乱。
  • 字典的键是唯一的,如果重复添加相同的键,后一次添加的值会覆盖前一个值。
  • 字典的值可以是任何类型,包括其他字典或列表。

dict() 函数

使用 dict() 函数可以从其他数据结构创建字典。

最简单的方式是把要创建的字典中的键和值,作为关键字参数传递给 dict() 函数,比如:

# 使用关键字参数
my_dict = dict(姓名="蔡泰贤", 年龄=25, 城市="上海")

另一种使用 dict() 的方式是,传递一个一个可迭代对象给它,输入的可迭代对象的每个元素又是一个包含两个数据的元组或列表。比如:

# 使用 (key, value) 的元组列表
pairs = [("姓名", "蔡泰贤"), ("年龄", 25), ("城市", "上海")]
my_dict = dict(pairs)

如果已有的数据不符合这个格式,那么需要把格式转换一下再传递给 dict() 函数。常见的情况是,已有的数据为两个列表,一个列表包含所有的键,另一个列表包含所有的值。这时候,我们可以使用 zip() 函数,将两个列表组合成一列表后,再创建字典:

keys = ["姓名", "年龄", "城市"]
values = ["蔡泰贤", 25, "上海"]
my_dict = dict(zip(keys, values))

字典推导式

字典推导式也是一种最常用的创建字典的方法,不过它稍微复杂一些,我们在介绍了其它一些基础知识后,再来介绍。

字典的常用操作

检查键是否存在

与检查一个元素是否在列表中一样,检查键是否在字典中也可以使用 in 关键字。这会返回一个布尔值,指示键是否存在于字典中。

person = {
"姓名": "唐胡录",
"年龄": 43,
"城市": "上海"
}

if "姓名" in person:
print('字典中存在 "姓名" 键。')

由于字典的数据存储方式,检查键是否存在于字典的效率要远高于检查元素是否存在在列表中。

访问值

使用中括号和键,类似索引列表的方法,可以从字典中获取相应的值:

person = {
"姓名": "阮奇桢",
"年龄": 30,
"城市": "上海"
}

print(person["姓名"]) # 输出: 阮奇桢

有可能,需要访问的键是不存在的,如果访问一个不存在的键,程序会抛出一个 KeyError 异常。关于异常极其处理,可以参考异常处理一节。 为了避免异常,我们可以先检查键是否存在,然后再访问键。这样比较麻烦,一个更简洁的办法是使用字典的 get() 方法来访问值。如果键不存在,get() 方法不会抛出异常,而是返回 None。我们还可以为 get() 方法指定键不存在时的默认值,这样,它就会在键不存在时,返回默认值:

person = {
"姓名": "史珍香",
"年龄": 20,
"城市": "北京"
}

# 使用 get 方法,如果键不存在,返回 None
gender = person.get('gender')
print(gender) # 输出: None

# 使用 get 方法,同时指定键不存在时的默认值
gender = person.get('gender', 'Not Specified')
print(gender) # 输出: Not Specified

在字典中,通过键来访问值是极其高效的,这是因为字典底层的数据结构优化了键的查找速度。我们将在数据结构与算法部分深入探讨字典、列表等数据结构的效率问题。现在重要的是要了解,使用键来检索数据是非常快速的操作。正因为如此,字典的主要用途是能够通过键快速获取相应的值。相比之下,直接在字典中查询特定的值,或者根据值找到对应的键,效率会非常低。

如果我们有两个数据集合,它们之间存在一对一的映射关系,即每个元素都唯一对应于另一个集合中的元素,而且我们需要能够高效地通过任一元素找到其对应的元素,那么可以考虑创建两个字典。这样,一个字典用第一个数据集合的元素作为键,另一个字典则用第二个数据集合的元素作为键。采用这种方式,就可以实现两个数据集合之间的高效双向查找。

因为 Python 采用的动态数据类型,同一个字典中的键可以用有不同的数据类型,这与很多其它主流编程语言不同。使用不同数据类型的数据作为键的时候,需要注意,如果它们的值相等,即便数据类型不同,那么 Python 也会认为它们是同一个键,比如整数 1 与浮点数 1.0 会被认为是同一个键。使用浮点数作为键,要格外注意误差的问题,它会导致一些看起来应该相同的键,其实并不相同:

dic = {}
dic[0.3] = 'a'
dic[0.1+0.2] = 'b'
print(dic[0.3]) # 输出:a
print(dic[0.1+0.2]) # 输出:b 由于无法,0.1+0.2 并不等于 0.3

dic[1.0] = 'c'
print(dic[1]) # 输出:c 1.0 与 1 是同一个键

添加或修改键值对:

在赋值语句中,同样使用中括号与键,还可以修改字典中的数据。

person = {
"姓名": "朱大常",
"年龄": 30,
"城市": "上海"
}

# 更新年龄
person["年龄"] = 35
# 添加职业
person["职业"] = "工程师"

print(person)
# 输出: {'姓名': '朱大常', '年龄': 35, '城市': '上海', '职业': '工程师'}

这样的赋值语句是不会检查一个键是否已经存在的。如果键不存在,则添加它;如果键已经存在,则更新对应的值。

有时候,我们可能会希望当键已经存在的时候,不要覆盖它,而是要保留原来的值。这与访问值时候遇到的问题类似,我们可以先检查键是否存在,再决定是否赋值。同样,这个问题也有更简洁的编程方法:使用字典的 setdefault() 方法。setdefault() 方法用于获取某个键对应的值,如果键不存在于字典中,则将键及指定的默认值插入到字典中。如果键已经存在,那么它将返回键对应的值,不会改变字典。比如:

person = {
"姓名": "熊初默",
"年龄": 30,
"城市": "上海"
}

# 使用 setdefault 为不存在的键设置默认值,这里 "工资" 键不存在
salary = person.setdefault('工资', 50000)
print(salary) # 输出: 50000

# 使用 setdefault 获取已存在的值,这里 "城市" 键已存在
city = person.setdefault('城市', '魔都')
print(city) # 输出: 上海

print(person)
# 输出: {'姓名': '熊初默', '年龄': 30, '城市': '上海', '工资': 50000}

setdefault() 一个常见的使用案例,是用于计数,比如我们要统计一句话里每个单词出现了多少次,可以使用类似下面的程序:

counts = {}
words = ['苹果', '香蕉', '苹果', '桔子', '香蕉', '苹果']

for word in words:
counts.setdefault(word, 0)
counts[word] += 1

print(counts) # 输出: {'苹果': 3, '香蕉': 2, '桔子': 1}

在这个例子中,setdefault() 方法用于确保每个单词在 counts 字典中都有一个对应的计数值。如果单词还没有在字典中,它将单词和计数值 0 插入字典。然后字典中单词对应的计数就可以安全地增加了。

当然上面这段程序也可以使用 get() 方法,有同样的效果。如果字典的值是简单数据类型,get() 方法可以使代码更精简,如果值本身也是列表,字典等复杂类型的数据,则 setdefault() 方法更好。下面是使用 get() 方法实现的同样功能的代码:

counts = {}
words = ['苹果', '香蕉', '苹果', '桔子', '香蕉', '苹果']

for word in words:
counts[word] = counts.get(word, 0) + 1

print(counts) # 输出: {'苹果': 3, '香蕉': 2, '桔子': 1}

实际上,处理字典中缺失的键的默认值还有一些更灵活的处理方式,我们将会在统计次数一节中做详细介绍。

我们在前文介绍了多变量赋值和链式赋值语句,请读者分析一下下面程序的运行结果是什么:

x, y = x[y] = {}, "a"
print(x)

删除键值对

使用 del 语句可以删除字典中的键值对:

person = {
"姓名": "郝夏仁",
"年龄": 50,
"城市": "Pythora"
}

del person["年龄"]

print(person) # 输出: {'姓名': '郝夏仁', '城市': 'Pythora'}

获取所有的键和值:

使用 keys()、values()、items() 这三个函数可以分别访问字典的键、值和键值对。这些方法都返回的都是字典视图对象,也就是说它们返回的不是固定数据,这些视图会反映字典的变化,一旦字典数据变了,它们也会跟着变化。

person = {
"姓名": "杨逸群",
"年龄": 30,
"城市": "上海"
}

keys = person.keys()
print(keys) # 输出: dict_keys(['姓名', '年龄', '城市'])
print(person.values()) # 输出: dict_values(['杨逸群', 30, '上海'])
print(person.items()) # 输出: dict_items([('姓名', '杨逸群'), ('年龄', 30), ('城市', '上海')])

del person["年龄"] # 从字典中删除一对键值,所有的字典视图也会跟随变化
print(keys) # 输出: dict_keys(['姓名', '城市'])

这几个方法,经常配合遍历字典使用,比如如果我们只需要遍历字典中的每个值,那么就可以遍历 values 视图:

person = {
"姓名": "杨逸群",
"年龄": 30,
"城市": "上海"
}

for value in person.values():
print(value)

# 输出: 杨逸群 30 上海

更常见的情况是要同时遍历字典中所有的键和值,需要使用 items 视图:

person = {
"姓名": "杨逸群",
"年龄": 30,
"城市": "上海"
}

for key, value in person.items():
print(key, value)

# 输出:
# 姓名 杨逸群
# 年龄 30
# 城市 上海

拆包

与列表的拆包相似,字典可以使用双星号 ** 操作符拆包,假设有 my_dict = {'a': 1, 'b': 2},那么拆包操作 **my_dict 的返回结果就是 a=1, b=2

利用拆包操作可以方便的合并两个字典:

dict1 = {'a': 1, 'b': 2}
dict2 = {'c': 3, 'd': 4}

merged_dict = {**dict1, **dict2}
print(merged_dict) # 输出: {'a': 1, 'b': 2, 'c': 3, 'd': 4}

字典拆包最主要的用途是为函数传递参数,我们将在介绍函数的时候详细讲解。

常用的字典的方法

除了介绍过的 get,setdefault 等方法,字典还有其它一些常用的方法:

  • dict.update(another_dict): 将另一个字典的键值对合并到当前字典中。
  • dict.pop(key): 删除并返回指定键的值。如果键不存在,则引发KeyError。
  • dict.clear(): 清除字典中的所有项。

比如:

# 创建一个初始字典
my_dict = {'a': 1, 'b': 2, 'c': 3}

# 使用 update 方法
another_dict = {'e': 5, 'f': 6}
my_dict.update(another_dict)
print(my_dict) # 输出: {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6}

# 使用 pop 方法
value = my_dict.pop('f')
print(value) # 输出: 6
print(my_dict) # 输出: {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

# 使用 clear 方法
my_dict.clear()
print(my_dict) # 输出: {}

字典与列表的比较

一个元素为键值对的列表,与一个字典保存的数据可能是完全相同的。但是它们内部对于数据不同的保存结构决定了它们各有一些不同的特点和擅长的应用场合。我们现在这里做一个简单总结,等到介绍数据结构与算法的时候,还会在对它们做深入探讨。

  • 有序性: 列表和字典中的元素都可以按照添加顺序遍历,这点两者类似。
  • 读取和查找: 列表可以使用整数索引来访问数据,这一操作的速度非常快。但是在列表中插入、删除和查找数据是比较慢的。字典是通过键,快速查找访问其对应的值。键可以是整数类型,也可以使字符串等其它不可变类型数据。
  • 重复数据: 列表中可以有重复的元素。字典的键必须是唯一的,但值可以有重复。
  • 总结: 列表适合保存那些按固定顺序保存的,根据位置索引来读取的数据。字典适合保存那些需要经常通过键来查找值的数据。

集合

集合(Set)是数学上一个常用的概念,它是由不同元素组成的一个无序的集。集合可以包含任何类型的对象,如数字、字符、其他集合等,但每个元素在集合中必须是唯一的,集合不允许重复元素。Python 语言中的集合与数学定义的集合非常类似,它也是无序的,也要求元素的唯一性,并且也支持集合的基本数学操作,比如交集、并集等。但也有一些差别,数学上的集合,一般创建之后就不会改变,但程序里的集合创建后还可以增加、删除元素。Python 集合对元素的类型有额外的限制:只能采用不可变类型数据作为元素,这一点与字典的键类似,集合元素可以是数值、字符串等,但不是是列表或其它集合。

创建集合

可以使用花括号,或 set() 构造函数来创建集合:

my_set = {1, 2, 3, 4}
print(my_set) # 输出:{1, 2, 3, 4}

my_list = [1, 2, 2, 3, 4, 4, 5]
my_set = set(my_list)
print(my_set) # 输出:{1, 2, 3, 4, 5}

# 创建一个空集合
empty_set = set()

集合中的元素是唯一的,这意味着重复的元素会被自动移除。需要注意的是,我们不能使用空花括号 {} 创建空集合,因为空花括号默认表示的是一个空字典,而不是集合。如果需要空集合,只能使用 set() 来创建。

常用操作

集合的用法与字典是非常类似的,在数据查询的功能方面,他可以被看作是只有键的字典。下面列出了集合最常用的几种操作:

  • 添加元素: 使用 add() 方法。
# 创建一个空集合
s = set()

# 使用 add() 方法添加元素
s.add("苹果")
s.add("香蕉")
s.add("桔子")

# 集合是无序的,print 可以打印出所有元素,但顺序并不确定
print(s) # 输出: {'苹果', '香蕉', '桔子'}
  • 成员测试: 可以使用 in 关键字来检查一个元素是否存在于集合中。
s = set(['苹果', '香蕉', '桔子'])

# 使用 in 关键字检查一个元素是否存在于集合中
if "桔子" in s:
print("集合中包含桔子")

if "桔子" not in s:
print("集合没有桔子")
  • 删除元素: 使用 remove() 或 discard() 方法。remove() 方法在元素不存在时会引发一个错误,而 discard() 方法则不会。
s = set(['苹果', '香蕉', '桔子'])

# 使用 remove() 方法删除一个存在的元素
s.remove("香蕉")
print(s) # 输出: {'苹果', '桔子'}

# 如果尝试使用 remove() 删除一个不存在的元素,会引发 KeyError 异常
# 为避免此错误, 可以先检查元素是否存在
if "香蕉" in s:
s.remove("香蕉")

# 使用 discard() 删除元素,即使该元素不存在,也不会引发错误
s.discard("香蕉") # 由于 "香蕉" 已经被删除, 这一行不会有任何效果
s.discard("苹果")
print(s) # 输出: {'桔子'}
  • 长度: 可以使用 len() 函数来获取集合的大小,也就是元素的个数。
s = set(['苹果', '香蕉', '桔子'])

# 使用 len() 函数获取集合的大小
print(len(s)) # 输出: 3

集合的数学运算

使用集合常常是因为需要用到它做一些相关的数学运算,包括:

  • 并集: 是包含两个集合中所有元素的集合,使用 union() 方法或 | 运算符。
  • 交集: 是同时属于这两个集合的所有元素的集合,使用 intersection() 方法或 & 运算符。
  • 差集: 是属于第一个集合但不属于第二个集合的所有元素的集合,使用 difference() 方法或 - 运算符。
  • 对称差集: 是在第一个集合或第二个集合中,但不同时在第一个集合和第二个集合的所有元素的集合,使用 symmetric_difference() 方法或 ^ 运算符。
  • 子集: 检查是否第一个集合的所有元素也都在第二个集合中,使用 issubset() 方法。
  • 超集: 检查是否第一个集合包含了第二个集合的所有元素,使用 issuperset() 方法。

运算示例:

# 定义两个集合
A = {1, 2, 3, 4}
B = {3, 4, 5, 6}

# 并集
print(A.union(B)) # 输出: {1, 2, 3, 4, 5, 6}
print(A | B) # 输出: {1, 2, 3, 4, 5, 6}

# 交集
print(A.intersection(B)) # 输出: {3, 4}
print(A & B) # 输出: {3, 4}

# 差集
print(A.difference(B)) # 输出: {1, 2}
print(A - B) # 输出: {1, 2}

# 对称差集
print(A.symmetric_difference(B)) # 输出: {1, 2, 5, 6}
print(A ^ B) # 输出: {1, 2, 5, 6}

# 子集
print(A.issubset(B)) # 输出: False

# 超集
print(A.issuperset(B)) # 输出: False

下面是一个更实用一些的示例,假设我们有两个列表,一个是朋友名单,另一个是同事名单,现在需要找出既是朋友又是同事的人的名单:

# 朋友名单
friends = ["张三", "李四", "王五", "赵六"]

# 同事名单
colleagues = ["孙七", "周八", "张三", "李四"]

# 使用集合找出既是朋友又是同事的人
friends_set = set(friends)
colleagues_set = set(colleagues)
common = friends_set.intersection(colleagues_set)

print("既是朋友又是同事的人:", common)

练习

  1. 移除列表中的重复项:编写一个程序,移除列表中重复的元素,并返回一个只包含唯一元素的列表。
  2. 查找字典中的最大值:使用循环结构查找一个字典值最大的那一项,并返回其对应的键
  3. 计算字典中所有值的总和
  4. 打印集合的所有子集 比如集合 {1,2} 的全部子集是 set(), {1}, {2}, {1,2}。
  5. 统计词频 编写程序,输入一段英文文章,统计其中每个单词出现的次数。