第三章 字典与集合
第3章 字典与集合
类和实例属性、模块命名空间、函数关键字参数等核心Python构造在内存中都是以字典形式表示。字典和集合的底层实现基于哈希表,这是它们高性能的关键。
字典现代操作
字典推导式
字典推导式通过任意可迭代对象中提取键值对来构建dict实例
# 示例3-1: 字典推导式
dial_codes = [(880, 'Bangladesh'),(55, 'Brazil'),(86, 'China'),(91, 'India'),(62, 'Indonesia'),(81, 'Japan'),(234, 'Nigeria'),(92, 'Pakistan'),(7, 'Russia'),(1, 'United States'),
]# 交换键值对:country作为键,code作为值
country_dial = {country: code for code, country in dial_codes}# 按国家名称排序,交换键值对,将值转为大写,并筛选出code < 70的项
filtered_codes = {code: country.upper()for country, code in sorted(country_dial.items())if code < 70
}print("country_dial:", country_dial)
print("filtered_codes:", filtered_codes)
解包映射
# 在函数调用中对多个参数应用**解包
def dump(**kwargs):return kwargs
# 此时所有的键必须是唯一的字符串 在函数调用中,关键字参数本质上是命名参数
# 在 dump(**{'x': 1}, **{'x': 2})时会报错
# TypeError: dump() got multiple values for keyword argument 'x'
result1 = dump(**{'x': 1}, y=2, **{'z': 3})
print("dump result:", result1) # 输出: {'x': 1, 'y': 2, 'z': 3}# 进行字面量解包 此时允许重复的键 但后面的键会覆盖前面的键
# **解包可以多次使用
result2 = {'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}}
print("merged dict:", result2) # 输出: {'a': 0, 'x': 4, 'y': 2, 'z': 3}
合并映射
# 使用|操作符合并映射(创建新映射)
d1 = {'a': 1, 'b': 3}
d2 = {'a': 2, 'b': 4, 'c': 6}
merged = d1 | d2
print("Merged with |:", merged) # 输出: {'a': 2, 'b': 4, 'c': 6}# 使用|=原地更新现有映射
d1 |= d2
print("d1 after |=:", d1) # 输出: {'a': 2, 'b': 4, 'c': 6}
# 通常,新映射的类型与左操作数的类型相同,但涉及用户自定义类型时可能根据操作符重载规则采用右操作数的类型
# 此处涉及python重载类型的具体实现 以及类__or__的实现
模式匹配
match/case
语句支持映射对象作为匹配主体,映射模式看起来像字典字面量,但可以匹配任何collections.abc.Mapping
的具体子类或者虚拟子类。
def get_creators(record):"""从媒体记录中提取创作者姓名"""match record:case {'type': 'book', 'api': 2, 'authors': [*names]}:return namescase {'type': 'book', 'api': 1, 'author': name}:return [name]# 不符合上述匹配的type模式都是不合法的 报错case {'type': 'book'}:raise ValueError(f"Invalid 'book' record: {record!r}")case {'type': 'movie', 'director': name}:return [name]case _:raise ValueError(f'Invalid record: {record!r}')# 测试示例
if __name__ == "__main__":# 测试书籍记录 (api=1)b1 = dict(api=1, author='Douglas Hofstadter',type='book', title='Gödel, Escher, Bach')print("Book 1 creators:", get_creators(b1)) # 输出: ['Douglas Hofstadter']# 测试书籍记录 (api=2)from collections import OrderedDictb2 = OrderedDict(api=2, type='book',title='Python in a Nutshell',authors='Martelli Ravenscroft Holden'.split())print("Book 2 creators:", get_creators(b2)) # 输出: ['Martelli', 'Ravenscroft', 'Holden']# 测试无效书籍记录try:get_creators({'type': 'book', 'pages': 770})except ValueError as e:print("Error:", str(e))# 测试非字典记录try:get_creators('Spam, spam, spam')except ValueError as e:print("Error:", str(e))# 测试捕获额外键值对food = dict(category='ice cream', flavor='vanilla', cost=199)match food:case {'category': 'ice cream', **details}:print(f"Ice cream details: {details}") # 输出: {'flavor': 'vanilla', 'cost': 199}
映射模式匹配特点
- 键的顺序无关紧要,即使主体是OrderedDict也是如此
- 映射模式在部分匹配时也能成功(主体可以包含模式中未指定的额外键)
- 可以使用
**details
捕获额外的键值对(必须是模式中的最后一个变量) - 自动处理缺失键的功能不会被触发,因为模式匹配使用
d.get(key, sentinel)
方法
[!NOTE]
OrderedDict
会记住并维护键被添加的顺序from collections import OrderedDictd1 = {'a': 1, 'b': 2} d2 = {'b': 2, 'a': 1} print(d1 == d2) # True - 标准 dict 不考虑顺序od1 = OrderedDict([('a', 1), ('b', 2)]) od2 = OrderedDict([('b', 2), ('a', 1)]) print(od1 == od2) # False - OrderedDict 考虑顺序
映射类型的标准化API
ABC与哈希
Python通过collections.abc
模块提供了Mapping
和MutableMapping
两个抽象基类(ABC),用于文档化并形式化映射的标准接口。
import collections.abc as abcdef check_mapping_types():"""验证不同映射类型的ABC兼容性"""my_dict = {}print(f"Is dict a Mapping? {isinstance(my_dict, abc.Mapping)}") # Trueprint(f"Is dict a MutableMapping? {isinstance(my_dict, abc.MutableMapping)}") # True# 其他映射类型示例from collections import defaultdict, OrderedDictdd = defaultdict(list)od = OrderedDict()print(f"Is defaultdict a Mapping? {isinstance(dd, abc.Mapping)}") # Trueprint(f"Is OrderedDict a MutableMapping? {isinstance(od, abc.MutableMapping)}") # Trueif __name__ == "__main__":check_mapping_types()
使用ABC进行类型检查优于直接检查是否为dict
类型,因为它能兼容各种映射实现。
要实现自定义映射,更简单的方式是继承 collections.UserDict
,或者通过组合(composition)封装一个 dict
,而不是直接继承这些 ABC。collections.UserDict
类以及标准库中所有具体的映射类,其内部实现都封装了一个基本的 dict
,而 dict
本身又是基于哈希表构建的,其要求映射的键必须是可哈希的。
[!WARNING]
一个对象是可哈希的,当且仅当在其生命周期内拥有一个永不改变的哈希码(需要实现
__hash__()
方法),并且可以与其他对象进行比较(需要实现__eq__()
方法)。相等的可哈希对象必须具有相同的哈希码。
def demonstrate_hashable():"""展示可哈希与不可哈希对象的区别"""# 可哈希的元组示例tt = (1, 2, (30, 40))print(f"Hash of tt: {hash(tt)}") # 输出类似 8027212646858338501# 不可哈希的元组示例(包含列表)try:tl = (1, 2, [30, 40])print(f"Hash of tl: {hash(tl)}")except TypeError as e:print(f"Error hashing tl: {e}")# 可哈希的元组示例(包含frozenset)tf = (1, 2, frozenset([30, 40]))print(f"Hash of tf: {hash(tf)}") # 输出类似 -4118419923444501110if __name__ == "__main__":demonstrate_hashable()
- 对于正确实现的对象,其哈希码仅在单个 Python 进程内保证恒定。不同版本的哈希计算给出的盐值可能是不一样的。
- 数值类型和扁平的不可变类型(如
str
和bytes
)是可哈希的 - 容器类型如果是不可变的,并且其所有元素也都是可哈希的,则该容器也是可哈希的
- 用户自定义类型默认是可哈希的(哈希码基于
id()
) - 如果实现自定义
__eq__()
,则必须确保__hash__()
仅依赖于对象生命周期内永不改变的属性
常见映射方法
略
高效处理可变 值
Python的字典操作遵循快速失败(fail-fast)哲学:当使用d[k]
访问不存在的键时会直接引发KeyError
。虽然d.get(k, default)
可以作为安全访问的替代方案,但在需要更新可变值(如列表、字典等)时,存在更高效的处理方法。
考虑一个文本索引任务:构建单词到出现位置列表的映射。每个键是一个单词,值是该单词在文本中出现位置的列表(位置编码为(行号, 列号)
对)。
示例输出:
# 单词 位置
a [(19, 48), (20, 53)]
Although [(11, 1), (16, 1), (18, 1)]
ambiguity [(14, 16)]
be [(15, 14), (16, 27), (20, 50)]
better [(3, 14), (4, 13), (5, 11), (6, 12), (7, 9), (8, 11), (17, 8), (18, 25)]
import re
import sys# 创建一个正则对象 \w匹配任何字母数字字符(等同于[a-zA-Z0-9_])+ 表示匹配一个或多个前面的模式
WORD_RE = re.compile(r'\w+')
# 用于储存索引的空字典
index = {}# 使用with语句确保文件在使用后正确关闭
# sys.argv[1]获取命令行中指定的第一个参数 即要处理的文件名
# 以UTF-8编码打开该文件
with open(sys.argv[1], encoding='utf-8') as fp:# enumerate(fp, 1) 为文件的每一行生成行号,起始值为1(而不是默认的0)for line_no, line in enumerate(fp, 1):# 使用正则表达式的finditer方法查找当前行中所有匹配的单词# finditer()方法在字符串line中查找所有非重叠匹配 # 返回一个迭代器,每次迭代产生一个Match对象for match in WORD_RE.finditer(line):# 获取匹配的单词字符串word = match.group()# match.start()返回匹配在字符串中的起始位置(从0开始计数)column_no = match.start() + 1# 创建位置元组,记录单词出现的具体位置# 格式:(行号, 列号)location = (line_no, column_no)# 1. 尝试从索引字典中获取单词对应的出现位置列表# 如果单词不存在于字典中,则返回默认值:空列表 []# 这里使用get方法避免KeyError异常occurrences = index.get(word, [])# 2. 将新的位置信息添加到出现列表中# 注意:occurrences是列表的引用,修改它会直接影响字典中的值occurrences.append(location)# 3. 将更新后的列表存回字典# 即使单词已存在,也需要重新赋值,因为列表对象已被修改# 对于新单词,这会创建新的键值对index[word] = occurrences# 次优的原因是因为对于已经存在的单词 occurrences.append(location)可以直接修改其中的值 此时index[word] = occurrences是多余的操作。# 但是对于新单词 occurrences是一个新创建的列表,与字典没有关联 所以需要使用index[word] = occurrences进行写回# 按字母顺序显示索引结果
# sorted()函数对字典的键进行排序
# key=str.upper参数指定排序时使用单词的大写形式进行比较
# 这样可以确保排序不区分大小写(例如,"Apple"和"apple"会按字母顺序排列)
# 注意:这里传递的是str.upper方法的引用,而不是调用它
# 这是将方法作为一等函数使用的例子(第7章会深入讨论)
for word in sorted(index, key=str.upper):# 打印单词及其所有出现位置print(word, index[word])
# 优化方案
import re
import sysWORD_RE = re.compile(r'\w+')
index = {}with open(sys.argv[1], encoding='utf-8') as fp:for line_no, line in enumerate(fp, 1):for match in WORD_RE.finditer(line):word = match.group()column_no = match.start() + 1location = (line_no, column_no)# 一行代码完成所有操作index.setdefault(word, []).append(location) # 关键优化# 按字母顺序显示结果
for word in sorted(index, key=str.upper):print(word, index[word])
setdefault
方法执行以下操作:
- 检查键是否存在于字典中
- 如果存在,返回对应的值
- 如果不存在,将键添加到字典,值设置为提供的默认值,然后返回该值
其等效于
def setdefault(self, key, default=None):if key not in self:self[key] = defaultreturn self[key]
缺失键处理
defaultdict
defaultdict
会在访问缺失键时自动创建具有默认值的项
def word_index_with_defaultdict():# 简洁方案 使用defaultdict构建单词索引import collectionsimport reimport sysif len(sys.argv) < 2:print("请提供文本文件作为参数")returnWORD_RE = re.compile(r'\w+')# 使用list作为default_factory# 当查找的键值不在该对象中时 则会自动生成一个该键值的新列表index = collections.defaultdict(list)with open(sys.argv[1], encoding='utf-8') as fp:for line_no, line in enumerate(fp, 1):for match in WORD_RE.finditer(line):word = match.group()column_no = match.start() + 1location = (line_no, column_no)# 直接追加,defaultdict会自动处理缺失键index[word].append(location)# 按字母顺序显示for word in sorted(index, key=str.upper):print(word, index[word])# 使用示例
if __name__ == "__main__":word_index_with_defaultdict()
default_factory
仅在__getitem__
(即d[k]
)调用时触发。d.get(k)
和k in d
不会触发default_factory
,生成的默认值会存储在字典中,后续访问会返回相同的对象。
__missing__
方法
__missing__
是Python处理缺失键的底层机制。当继承dict
时,可以实现此方法:
class StrKeyDict0(dict):"""在查找时将非字符串键转换为str的字典"""# 添加了对于缺失内容的处理方法def __missing__(self, key):if isinstance(key, str):raise KeyError(key)return self[str(key)]def get(self, key, default = None):try:return self[key]except KeyError:return defaultdef __contains__(self, key):return key in self.keys() or str(key) in self.keys()def test_str_key_dict():"""测试StrKeyDict0的功能"""d = StrKeyDict0([('2', 'two'), ('4', 'four')])# 测试d[key]表示法print(f"d['2'] = {d['2']}") # d['2'] = twoprint(f"d[4] = {d[4]}") # d[4] = fourtry:print(d[1]) except KeyError as e:# 出现错误时 直接输出键名print(f"KeyError: {e}") # KeyError: '1'# 测试d.get(key)表示法print(f"d.get('2') = {d.get('2')}") # d.get('2') = twoprint(f"d.get(4) = {d.get(4)}") # d.get(4) = four# 获取不到时就输出默认值print(f"d.get(1, 'N/A') = {d.get(1, 'N/A')}") # d.get(1, 'N/A') = N/A# 测试in操作符print(f"2 in d? {2 in d}") # 2 in d? Trueprint(f"1 in d? {1 in d}") # 1 in d? Falseif __name__ == "__main__":test_str_key_dict()
变体
collections.OrderedDict
Python 3.10中 dict 已保证保持插入顺序,因此如今使用 OrderedDict
主要是为了向后兼容旧版本 Python。OrderedDict
与普通 dict
仍存在以下关键差异:
- 顺序敏感的相等性比较:两个
OrderedDict
只有在键值对完全相同且顺序一致时才相等。 popitem()
方法签名不同:OrderedDict.popitem(last=True)
可通过参数控制弹出首项(last=False
)或末项(last=True
)。- 提供
move_to_end(key, last=True)
方法:可高效地将指定键移动到字典开头或末尾。 - 设计目标不同:
dict
优先优化映射操作(如查找、插入),顺序保持是次要特性。OrderedDict
专为频繁重排序场景设计,在这类操作上性能更优,适用于实现 LRU 缓存等需要动态调整顺序的结构。
collections.ChainMap
ChainMap
将多个映射组合成一个统一的可更新视图,不复制原始映射,而是持有引用。
- 查找规则:从第一个映射开始依次搜索,返回首个匹配的键值。
- 写入规则:所有更新(赋值、删除)仅作用于第一个映射。
- 典型用途:模拟嵌套作用域(如解释器)、分层配置(命令行参数 > 环境变量 > 默认值)。
import collections# 查找规则 返回首个匹配的键值
d1 = dict(a=1, b=3)
d2 = dict(a=2, b=4, c=6)
chain = collections.ChainMap(d1, d2)print(chain['a']) # 输出: 1(来自 d1)
print(chain['c']) # 输出: 6(来自 d2)# 更新操作只影响第一个映射
chain['c'] = -1
print(d1) # 输出: {'a': 1, 'b': 3, 'c': -1}
print(d2) # 输出: {'a': 2, 'b': 4, 'c': 6}# 模拟一个嵌套作用域
# 可用于查找变量:先局部,再全局,最后内置作用域
import builtins
pylookup = collections.ChainMap(locals(), globals(), vars(builtins))
collections.Counter
Counter
是 dict
的子类,用于统计可哈希对象的出现次数,也可视为多重集(multiset)。
- 键为元素,值为其计数(可为零或负数)。
- 访问不存在的键返回
0
,而非抛出KeyError
。 - 支持累加更新、数学运算(
+
,-
,&
,|
)及专用方法。
import collections# 初始化并统计字符串中的字母
ct = collections.Counter('abracadabra')
print(ct) # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})# 使用 update() 累加计数
ct.update('aaaaazzz')
print(ct) # Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})# 获取最常见的 n 个元素
top3 = ct.most_common(3)
print(top3) # [('a', 10), ('z', 3), ('b', 2)]
# 注意:'b' 和 'r' 计数相同,但只返回前三个结果,按首次出现顺序决定
当用作多重集时,每个键代表一个元素,其值表示该元素在集合中出现的次数。
shelve.Shelf
shelve
模块提供了一种将 Python 对象以键值对形式持久化存储的简单方式,其底层依赖于 dbm
数据库和 pickle
序列化机制。
shelve.open
函数返回一个 shelve.Shelf
实例。shelve.Shelf
是一个持久化的、类字典对象,继承自 collections.abc.MutableMapping
,支持标准映射操作(如 __getitem__
、__setitem__
、__delitem__
、in
、keys()
等)。shelve.Shelf
的键必须是 字符串(str
),值可以是任意 pickle
可序列化的 Python 对象(包括类实例、嵌套结构、共享引用等)。shelve.Shelf
还提供了一些 I/O 管理方法,如 sync()
和 close()
。
但根据书本描述其存在挺多缺陷,使用前记得看看文档。
子类应继承 UserDict 而非 dict
在创建自定义映射类型时,推荐继承 collections.UserDict
而非直接继承内置 dict
。
这主要因为UserDict
并不继承 dict
,而是内部持有一个名为 data
的普通字典(self.data
),通过组合实现映射功能。这使得自定义逻辑更清晰、更安全,避免递归调用问题。
[!NOTE]
class BadDict(dict):def __setitem__(self, key, value):# 此处的self[key] = value 会再次调用 __setitem__ 从而导致无限递归self[str(key)] = value
class GoodDict(collections.UserDict):def __setitem__(self, key, value):# 操作的是内部字典 self.data,不会触发自身的 __setitem__self.data[str(key)] = value # 无递归风险
同时dict
是用 C 实现的,内部使用了多种优化捷径。直接继承 dict
时,某些方法(如 __contains__
、get
、update
等)可能不会调用我们重写的 __getitem__
或 __setitem__
,导致行为不一致。
[!NOTE]
import collections# 1. 继承 dict class BadDict(dict):def __setitem__(self, key, value):# 期望:所有键转为字符串super().__setitem__(str(key), value)# 2. 继承 UserDict class GoodDict(collections.UserDict):def __setitem__(self, key, value):self.data[str(key)] = value# 测试 d1 = BadDict() d2 = GoodDict()# 直接赋值:两者都正常 d1[1] = 'one' d2[1] = 'one'# 用 update() d1.update({2: 'two'}) d2.update({2: 'two'})# 因为update() 直接操作底层哈希表,不经过 Python 层的 __setitem__ # 使用 UserDict 可以避免这个问题 print("BadDict keys:", list(d1.keys())) # ['1', 2] ← 2 是整数 和预期不符合 print("GoodDict keys:", list(d2.keys())) # ['1', '2'] ← 全是字符串
UserDict
继承自 collections.abc.MutableMapping
,因此只要实现 __getitem__
、__setitem__
、__delitem__
和 __iter__
、__len__
,其余方法(如 get
、update
、keys
、values
等)都会自动提供,并且正确委托给我们的自定义方法。
# 直接继承dict
class StrKeyDict0(dict):"""在查找时将非字符串键转换为str的字典"""# 添加了对于缺失内容的处理方法def __missing__(self, key):if isinstance(key, str):raise KeyError(key)return self[str(key)]def get(self, key, default = None):try:return self[key]except KeyError:return default# 用来与下面的实现进行对比def __contains__(self, key):return key in self.keys() or str(key) in self.keys()
import collections# 从UserDict中继承
class StrKeyDict(collections.UserDict):# 管理缺失def __missing__(self, key):if isinstance(key, str):raise KeyError(key)return self[str(key)]# 直接查 self.data,简洁可靠# 由于 __setitem__ 已确保所有键都是字符串,只需检查 `str(key) in self.data`,无需遍历 self.keys()def __contains__(self, key):return str(key) in self.data# 所有键强制转为字符串def __setitem__(self, key, item):self.data[str(key)] = item
UserDict
继承了MutableMapping
和Mapping
,这些 ABC 提供了大量具体方法(非抽象),例如:
Mapping.get(key, default=None)
MutableMapping.update(other)
keys()
,values()
,items()
,pop()
,popitem()
,clear()
等
因此,只需专注实现核心方法(__getitem__
, __setitem__
, __delitem__
, __len__
, __iter__
),其余功能自动完备。
不可变映射
Python 标准库中没有真正的不可变映射类型,可通过 types.MappingProxyType
创建只读代理,实现“不可变视图”的效果。对原始映射的更新会反映在代理中,但无法通过代理进行修改。
- 只读:无法通过代理修改映射(赋值、删除、清空等操作均被禁止)。
- 动态:代理与原始映射共享数据,原始映射的任何变更会立即反映在代理中。
from types import MappingProxyTyped = {1: 'A'}
d_proxy = MappingProxyType(d)print(d_proxy[1]) # 'A' → 可正常读取
# d_proxy[2] = 'x' # TypeError: 不支持赋值
# del d_proxy[1] # TypeError: 不支持删除d[2] = 'B' # 修改原始映射
print(d_proxy) # mappingproxy({1: 'A', 2: 'B'}) → 代理自动更新
print(d_proxy[2]) # 'B' 新值可见
视图
dict
的 .keys()
、.values()
和 .items()
方法返回动态、只读的视图对象(dict_keys
、dict_values
、dict_items
),这些字典视图是字典内部数据结构的只读投影。
特性 | 说明 |
---|---|
动态性 | 视图是字典数据的实时投影。字典变更会立即反映在已有视图中。 |
只读性 | 无法通过视图修改字典(如赋值、删除),但可通过原字典修改。 |
内存高效 | 视图不复制数据,仅提供访问接口。 |
不可构造 | 视图类是内部实现,无法直接实例化(如 dict_values() 会报错)。 |
d = dict(a=10, b=20, c=30)
values = d.values()print(values) # dict_values([10, 20, 30])
# 查询视图长度
print(len(values)) # 3
# 视图是可迭代的 可以创建列表
print(list(values)) # [10, 20, 30]
# 返回一个迭代器
print(reversed(values)) # <dict_reversevalueiterator object># 不支持索引
# values[0] # TypeError: 'dict_values' object is not subscriptable# 动态更新:修改原字典,视图自动变化
d['z'] = 99
print(values) # dict_values([10, 20, 30, 99])
视图对象与特定字典实例强绑定,由 C 层实现,无法脱离字典独立存在:
# 获取引用
values_class = type({}.values())
# v = values_class() # TypeError: cannot create 'dict_values' instances
这确保了视图的语义一致性:视图必须依附于一个真实的字典。
dict
实现方式的实际影响
Python 的 dict
基于**哈希表(hash table)**实现,这一底层设计带来了显著的性能优势,同时也引出若干重要的实践约束和优化建议。
- 键必须是可哈希的(hashable)
可哈希要求对象必须实现 __hash__()
和 __eq__()
,且满足: 若 a == b
,则 hash(a) == hash(b)
。不可变内置类型(如 str
, int
, tuple
)通常是可哈希的;可变类型(如 list
, dict
, set
)不可哈希。违反此规则会导致 TypeError: unhashable type
。
- 查找性能极佳(平均 O(1))
通过键的哈希值直接计算存储位置,即使字典有数百万项,查找也几乎恒定时间。
-
键的插入顺序被保留
-
内存开销依然显著
在python3.10 哈希表仍需存储键、值、哈希值;且需要保持至少 1/3 的空槽位以避免哈希冲突导致性能退化。相比之下,元组等线性结构仅存储元素指针,内存更紧凑。
- 实例属性应尽量在
__init__
中定义
Python 将实例属性存储在字典 obj.__dict__
中。自 Python 3.3起,引入键共享字典(Key-Sharing Dictionary)优化。同一类的多个实例,若具有相同的属性名集合,可共享同一个键表;每个实例的 __dict__
仅存储值数组(指针数组),大幅节省内存。但若在 __init__
之后动态添加新属性(如 obj.new_attr = value
),该实例的 __dict__
会脱离共享机制,回退到完整字典,失去内存优势。使用键共享字典可以为面向对象程序可减少 10%–20% 内存占用。
集合论
本书中集合指 set
和 frozenset
两种类型。
去重
集合是唯一对象的集合,常用于去重。
使用 set
去重(不保留顺序):
l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
unique_items = set(l) # {'eggs', 'spam', 'bacon'}
unique_list = list(set(l)) # ['eggs', 'spam', 'bacon']
使用 dict.fromkeys
去重(保留首次出现顺序):
l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
unique_keys = dict.fromkeys(l).keys() # dict_keys(['spam', 'eggs', 'bacon'])
unique_list = list(dict.fromkeys(l).keys()) # ['spam', 'eggs', 'bacon']
可哈希
集合元素必须是可哈希的。set
本身不可哈希,不能作为集合元素。frozenset
可哈希,可嵌套在集合中。
运算
集合支持中缀运算符,用于高效实现集合操作:
a | b
:并集a & b
:交集a - b
:差集a ^ b
:对称差集
集合字面量
集合用{1}
、{1, 2}
等表示,与数学符号一致。但是不存在空集合字面量,必须使用 set()
创建空集合,因为{}
创建的是空字典,不是空集合。集合字面量比 set([...])
更快、更易读,因为使用专门的 BUILD_SET
字节码。
frozenset
没有字面量语法,必须通过构造函数创建:
fs = frozenset(range(10)) # frozenset({0, 1, 2, ..., 9})
s = {1}
print(type(s)) # <class 'set'>
print(s) # {1}
s.pop()
# 注意这里的空集合表示
print(s) # set()
集合推导式
# 生成平方数小于 100 的正整数的平方集合
squares = {x**2 for x in range(1, 11)}
print(squares)
# 输出(顺序可能不同):{64, 1, 4, 36, 100, 9, 16, 49, 81, 25}
# 由于 Python 的加盐哈希机制,集合元素的显示顺序在不同运行中可能不同。
集合实现方式
set
和 frozenset
均基于哈希表实现,这一底层机制带来了以下实际影响:
-
元素必须可哈希
-
检索操作非常高效
-
与底层元素指针数组相比,集合有显著的内存开销
-
元素顺序依赖于插入顺序,但这种方式既无用也不可靠
集合中元素的顺序取决于插入顺序和哈希值,但若两个不同元素哈希值相同,它们在集合中的相对位置由插入先后决定。
- 添加元素可能改变已有元素的顺序
当集合填充超过哈希表容量的 2/3 时,Python 会自动扩容并重建哈希表。重建过程中所有元素被重新插入,导致其在内部存储中的顺序可能发生变化。