第三章 字典与集合

第3章 字典与集合

类和实例属性、模块命名空间、函数关键字参数等核心Python构造在内存中都是以字典形式表示。字典和集合的底层实现基于哈希表,这是它们高性能的关键。

字典现代操作

字典推导式

字典推导式通过任意可迭代对象中提取键值对来构建dict实例

python 复制代码
# 示例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)

解包映射

python 复制代码
# 在函数调用中对多个参数应用**解包
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}

合并映射

python 复制代码
# 使用|操作符合并映射(创建新映射)
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}
py 复制代码
# 通常,新映射的类型与左操作数的类型相同,但涉及用户自定义类型时可能根据操作符重载规则采用右操作数的类型
# 此处涉及python重载类型的具体实现 以及类__or__的实现

模式匹配

match/case语句支持映射对象作为匹配主体,映射模式看起来像字典字面量,但可以匹配任何collections.abc.Mapping的具体子类或者虚拟子类。

python 复制代码
def get_creators(record):
    """从媒体记录中提取创作者姓名"""
    match record:
        case {'type': 'book', 'api': 2, 'authors': [*names]}:
            return names
        case {'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 OrderedDict
    b2 = 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}

映射模式匹配特点

  1. 键的顺序无关紧要 ,即使主体是OrderedDict也是如此
  2. 映射模式在部分匹配时也能成功(主体可以包含模式中未指定的额外键)
  3. 可以使用**details捕获额外的键值对 (必须是模式中的最后一个变量
  4. 自动处理缺失键的功能不会被触发 ,因为模式匹配使用d.get(key, sentinel)方法

!NOTE

OrderedDict 会记住并维护键被添加的顺序

py 复制代码
from collections import OrderedDict

d1 = {'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模块提供了MappingMutableMapping两个抽象基类(ABC),用于文档化并形式化映射的标准接口

python 复制代码
import collections.abc as abc

def check_mapping_types():
    """验证不同映射类型的ABC兼容性"""
    my_dict = {}
    print(f"Is dict a Mapping? {isinstance(my_dict, abc.Mapping)}")  # True
    print(f"Is dict a MutableMapping? {isinstance(my_dict, abc.MutableMapping)}")  # True
    
    # 其他映射类型示例
    from collections import defaultdict, OrderedDict
    dd = defaultdict(list)
    od = OrderedDict()
    
    print(f"Is defaultdict a Mapping? {isinstance(dd, abc.Mapping)}")  # True
    print(f"Is OrderedDict a MutableMapping? {isinstance(od, abc.MutableMapping)}")  # True

if __name__ == "__main__":
    check_mapping_types()

使用ABC进行类型检查优于直接检查是否为dict类型,因为它能兼容各种映射实现。

要实现自定义映射,更简单的方式是继承 collections.UserDict,或者通过组合(composition)封装一个 dict,而不是直接继承这些 ABC。collections.UserDict 类以及标准库中所有具体的映射类,其内部实现都封装了一个基本的 dict,而 dict 本身又是基于哈希表构建的,其要求映射的键必须是可哈希的

!WARNING

一个对象是可哈希的,当且仅当在其生命周期内拥有一个永不改变的哈希码(需要实现__hash__()方法),并且可以与其他对象进行比较(需要实现__eq__()方法)。相等的可哈希对象必须具有相同的哈希码

python 复制代码
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)}")  # 输出类似 -4118419923444501110

if __name__ == "__main__":
    demonstrate_hashable()
  • 对于正确实现的对象,其哈希码仅在单个 Python 进程内保证恒定。不同版本的哈希计算给出的盐值可能是不一样的。
  • 数值类型和扁平的不可变类型 (如strbytes)是可哈希的
  • 容器类型如果是不可变的,并且其所有元素也都是可哈希的,则该容器也是可哈希的
  • 用户自定义类型默认是可哈希的(哈希码基于id()
  • 如果实现自定义__eq__(),则必须确保__hash__()仅依赖于对象生命周期内永不改变的属性

常见映射方法

高效处理可变 值

Python的字典操作遵循快速失败(fail-fast)哲学 :当使用d[k]访问不存在的键时会直接引发KeyError。虽然d.get(k, default)可以作为安全访问的替代方案,但在需要更新可变值(如列表、字典等)时,存在更高效的处理方法。

考虑一个文本索引任务:构建单词到出现位置列表的映射。每个键是一个单词,值是该单词在文本中出现位置的列表(位置编码为(行号, 列号)对)。

示例输出

py 复制代码
# 单词 位置
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)]
python 复制代码
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])
python 复制代码
# 优化方案
import re
import sys

WORD_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() + 1
            location = (line_no, column_no)
            
            # 一行代码完成所有操作
            index.setdefault(word, []).append(location)  # 关键优化

# 按字母顺序显示结果
for word in sorted(index, key=str.upper):
    print(word, index[word])

setdefault方法执行以下操作:

  1. 检查键是否存在于字典中
  2. 如果存在,返回对应的值
  3. 如果不存在,将键添加到字典,值设置为提供的默认值,然后返回该值

其等效于

python 复制代码
def setdefault(self, key, default=None):
    if key not in self:
        self[key] = default
    return self[key]

缺失键处理

defaultdict

defaultdict会在访问缺失键时自动创建具有默认值的项

python 复制代码
def word_index_with_defaultdict():
    # 简洁方案 使用defaultdict构建单词索引
    import collections
    import re
    import sys
    
    if len(sys.argv) < 2:
        print("请提供文本文件作为参数")
        return
    
    WORD_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() + 1
                location = (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时,可以实现此方法:

python 复制代码
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()

def test_str_key_dict():
    """测试StrKeyDict0的功能"""
    d = StrKeyDict0([('2', 'two'), ('4', 'four')])
    
    # 测试d[key]表示法
    print(f"d['2'] = {d['2']}") # d['2'] = two
    print(f"d[4] = {d[4]}") # d[4] = four
    try:
        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') = two
    print(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? True
    print(f"1 in d? {1 in d}") # 1 in d? False

if __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 将多个映射组合成一个统一的可更新视图,不复制原始映射,而是持有引用

  • 查找规则:从第一个映射开始依次搜索,返回首个匹配的键值。
  • 写入规则:所有更新(赋值、删除)仅作用于第一个映射。
  • 典型用途:模拟嵌套作用域(如解释器)、分层配置(命令行参数 > 环境变量 > 默认值)。
python 复制代码
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

Counterdict 的子类,用于统计可哈希对象的出现次数,也可视为多重集(multiset)

  • 键为元素,值为其计数(可为零或负数)。
  • 访问不存在的键返回 0,而非抛出 KeyError
  • 支持累加更新、数学运算(+, -, &, |)及专用方法。
python 复制代码
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__inkeys() 等)。shelve.Shelf的键必须是 字符串str),值可以是任意 pickle 可序列化的 Python 对象 (包括类实例、嵌套结构、共享引用等)。shelve.Shelf 还提供了一些 I/O 管理方法 ,如 sync()close()

但根据书本描述其存在挺多缺陷,使用前记得看看文档。

子类应继承 UserDict 而非 dict

在创建自定义映射类型时,推荐继承 collections.UserDict 而非直接继承内置 dict

这主要因为UserDict 并不继承 dict,而是内部持有一个名为 data 的普通字典self.data),通过组合实现映射功能。这使得自定义逻辑更清晰、更安全,避免递归调用问题

!NOTE

python 复制代码
class BadDict(dict):
 def __setitem__(self, key, value):
     # 此处的self[key] = value 会再次调用 __setitem__ 从而导致无限递归
     self[str(key)] = value  
python 复制代码
class GoodDict(collections.UserDict):
 def __setitem__(self, key, value):
     # 操作的是内部字典 self.data,不会触发自身的 __setitem__
     self.data[str(key)] = value  # 无递归风险

同时dict 是用 C 实现的,内部使用了多种优化捷径。直接继承 dict 时,某些方法(如 __contains__getupdate 等)可能不会调用我们重写的 __getitem____setitem__,导致行为不一致。

!NOTE

py 复制代码
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__,其余方法(如 getupdatekeysvalues 等)都会自动提供,并且正确委托给我们的自定义方法

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 default
    
    # 用来与下面的实现进行对比
    def __contains__(self, key):
        return key in self.keys() or str(key) in self.keys()
python 复制代码
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 继承了MutableMappingMapping,这些 ABC 提供了大量具体方法(非抽象),例如:

  • Mapping.get(key, default=None)
  • MutableMapping.update(other)
  • keys(), values(), items(), pop(), popitem(), clear()

因此,只需专注实现核心方法(__getitem__, __setitem__, __delitem__, __len__, __iter__),其余功能自动完备。

不可变映射

Python 标准库中没有真正的不可变映射类型 ,可通过 types.MappingProxyType 创建只读代理 ,实现"不可变视图"的效果。对原始映射的更新会反映在代理中,但无法通过代理进行修改

  • 只读:无法通过代理修改映射(赋值、删除、清空等操作均被禁止)。
  • 动态 :代理与原始映射共享数据 ,原始映射的任何变更会立即反映在代理中。
python 复制代码
from types import MappingProxyType

d = {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_keysdict_valuesdict_items),这些字典视图是字典内部数据结构的只读投影。

特性 说明
动态性 视图是字典数据的实时投影。字典变更会立即反映在已有视图中。
只读性 无法通过视图修改字典(如赋值、删除),但可通过原字典修改。
内存高效 视图不复制数据,仅提供访问接口。
不可构造 视图类是内部实现,无法直接实例化 (如 dict_values() 会报错)。
python 复制代码
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 层实现,无法脱离字典独立存在:

python 复制代码
# 获取引用
values_class = type({}.values())
# v = values_class()  #  TypeError: cannot create 'dict_values' instances

这确保了视图的语义一致性:视图必须依附于一个真实的字典

dict 实现方式的实际影响

Python 的 dict 基于**哈希表(hash table)**实现,这一底层设计带来了显著的性能优势,同时也引出若干重要的实践约束和优化建议。

  1. 键必须是可哈希的(hashable)

可哈希要求对象必须实现 __hash__()__eq__(),且满足: a == b,则 hash(a) == hash(b) 。不可变内置类型(如 str, int, tuple)通常是可哈希的;可变类型(如 list, dict, set)不可哈希。违反此规则会导致 TypeError: unhashable type

  1. 查找性能极佳(平均 O(1))

通过键的哈希值直接计算存储位置,即使字典有数百万项,查找也几乎恒定时间

  1. 键的插入顺序被保留

  2. 内存开销依然显著

在python3.10 哈希表仍需存储键、值、哈希值;且需要保持至少 1/3 的空槽位以避免哈希冲突导致性能退化。相比之下,元组等线性结构仅存储元素指针,内存更紧凑。

  1. 实例属性应尽量在 __init__ 中定义

Python 将实例属性存储在字典 obj.__dict__中。自 Python 3.3起,引入键共享字典(Key-Sharing Dictionary)优化。同一类的多个实例,若具有相同的属性名集合 ,可共享同一个键表 ;每个实例的 __dict__ 仅存储值数组 (指针数组),大幅节省内存。但若在 __init__ 之后动态添加新属性(如 obj.new_attr = value),该实例的 __dict__脱离共享机制,回退到完整字典,失去内存优势。使用键共享字典可以为面向对象程序可减少 10%--20% 内存占用。

集合论

本书中集合setfrozenset 两种类型。

去重

集合是唯一对象的集合,常用于去重。

使用 set 去重(不保留顺序):

python 复制代码
l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
unique_items = set(l)  # {'eggs', 'spam', 'bacon'}
unique_list = list(set(l))  # ['eggs', 'spam', 'bacon']

使用 dict.fromkeys 去重(保留首次出现顺序):

python 复制代码
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 没有字面量语法,必须通过构造函数创建:

python 复制代码
fs = frozenset(range(10))  # frozenset({0, 1, 2, ..., 9})
python 复制代码
s = {1}
print(type(s))  # <class 'set'>
print(s)        # {1}
s.pop()
# 注意这里的空集合表示
print(s)        # set()

集合推导式

python 复制代码
# 生成平方数小于 100 的正整数的平方集合
squares = {x**2 for x in range(1, 11)}
print(squares)
# 输出(顺序可能不同):{64, 1, 4, 36, 100, 9, 16, 49, 81, 25}
# 由于 Python 的加盐哈希机制,集合元素的显示顺序在不同运行中可能不同。

集合实现方式

setfrozenset 均基于哈希表实现,这一底层机制带来了以下实际影响:

  1. 元素必须可哈希

  2. 检索操作非常高效

  3. 与底层元素指针数组相比,集合有显著的内存开销

  4. 元素顺序依赖于插入顺序,但这种方式既无用也不可靠

集合中元素的顺序取决于插入顺序和哈希值,但若两个不同元素哈希值相同,它们在集合中的相对位置由插入先后决定。

  1. 添加元素可能改变已有元素的顺序

当集合填充超过哈希表容量的 2/3 时,Python 会自动扩容并重建哈希表。重建过程中所有元素被重新插入,导致其在内部存储中的顺序可能发生变化。

相关推荐
gc_22993 小时前
学习Python中Selenium模块的基本用法(18:使用ActionChains操作鼠标)
python·selenium
Lululaurel3 小时前
从静态图表到交互叙事:数据可视化的新范式与实现
python·信息可视化·数据分析·matplotlib·数据可视化
蒋星熠3 小时前
TensorFlow与PyTorch深度对比分析:从基础原理到实战选择的完整指南
人工智能·pytorch·python·深度学习·ai·tensorflow·neo4j
qq_340474023 小时前
0.1 tensorflow例1-梯度下降法
人工智能·python·tensorflow
紫钺-高山仰止3 小时前
【Pyzmq】python 跨进程线程通信 跨平台跨服务器通信
服务器·python·github
java1234_小锋4 小时前
[免费]基于Python的在线音乐网站系统(后端Django)【论文+源码+SQL脚本】
python·在线音乐·django音乐·python音乐·python在线音乐
无垠的广袤5 小时前
【LattePanda Mu 开发套件】AI 图像识别网页服务器
服务器·人工智能·python·单片机·嵌入式硬件·物联网
Derrick__15 小时前
Python常用三方模块——Pillow
开发语言·python·pillow
小蕾Java5 小时前
PyCharm保姆级详细使用手册(Python新手快速上手篇)
ide·python·pycharm