自学内容网 自学内容网

【流畅的Python】第三章:字典和集合——《Fluent Python》学习笔记

字典和集合

1 字典的现代用法

1.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_dial = {country: code for code, country in dial_codes}
print(country_dial)

1.2 映射拆包

从 Python 3.5 开始,调用函数时,多个参数可使用**,且**可在 dict 字面量中使用。

def dump(**kwargs):
    """
    定义一个函数,接受关键字参数并返回一个字典
    """
    return kwargs

# 使用**拆包多个字典作为参数传递给函数
print(dump(**{'x': 1}, y=2, **{'z': 3}))  
# 输出: {'x': 1, 'y': 2, 'z': 3}

# 在字典字面量中使用**合并多个字典
print({'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}})  
# 输出: {'a': 0, 'x': 4, 'y': 2, 'z': 3}

1.3 使用 | 合并映射

Python 3.9 支持使用||=合并映射,|创建新映射,|=就地更新现有映射。

d1 = {'a': 1, 'b': 3}
d2 = {'a': 2, 'b': 4, 'c': 6}
# 使用|合并两个字典,返回一个新字典
print(d1 | d2)  # 右边字典(后出现的)中的键值对会覆盖左边字典(先出现的)中的键值对。
# 输出: {'a': 2, 'b': 4, 'c': 6}

# 使用|=就地更新字典d1
d1 |= d2
print(d1)  
# 输出: {'a': 2, 'b': 4, 'c': 6}

2 使用模式匹配处理映射

match/case语句的匹配对象可以是映射,模式看似dict字面量,能匹配collections.abc.Mapping的具体子类或虚拟子类,可通过模式匹配处理半结构化数据,注意模式中键的顺序、部分匹配、捕获多余键值对等问题。

def get_creators(record: dict) -> list:
    """
    根据记录类型和内容返回创作者名字列表
    """
    match record:
        case {'type': 'book', 'api': 2, 'authors': [*names]}:
            return names
        case {'type': 'book', 'api': 1, 'author': name}:
            return [name]
        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}')

b1 = dict(api=1, author='Douglas Hofstadter', type='book', title='Gödel, Escher, Bach')
print(get_creators(b1))  
# 输出: ['Douglas Hofstadter']

from collections import OrderedDict
b2 = OrderedDict(api=2, type='book', title='Python in a Nutshell', authors='Martelli Ravenscroft Holden'.split())
print(get_creators(b2))  
# 输出: ['Martelli', 'Ravenscroft', 'Holden']

print(get_creators({'type': 'book', 'pages': 770}))  
# 输出: ValueError: Invalid 'book' record: {'type': 'book', 'pages': 770}

print(get_creators('Spam, spam, spam'))  
# 输出: ValueError: Invalid record: 'Spam, spam, spam'

3 映射类型的标准API

3.1 “可哈希”指什么

“可哈希”对象的哈希码在生命周期内不变且课比较,数值类型、不可变扁平类型及满足条件的容器是可哈希的,用户定义类型默认可哈希,自定义__eq__方法时需注意__hash__方法的返回值。

tt = (1, 2, (30, 40))
print(hash(tt))  
# 输出: 8027212646858338501

tl = (1, 2, [30, 40])
try:
    hash(tl)
except TypeError as e:
    print(e)  
# 输出: unhashable type: 'list'

tf = (1, 2, frozenset([30, 40]))
print(hash(tf))  
# 输出: -4118419923444501110

3.2 常用映射方法概述

介绍了dict、defaultdict和OrderedDict实现的常用方法,如clear、copy、get、items、keys、pop、popitem、setdefault、update、values等,以及它们的功能和用法。

3.3 插入或更新可变的值

当键不存在时,d[k]会抛错,可使用d.get(k, default)获取默认值,若要更新可变值,dict.setdefault是更好的方法,它可避免重复搜索键。

# 使用dict.get获取并更新词出现的位置列表(不完美示例)
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)
            # 使用get方法获取单词位置列表,若不存在则返回空列表
            occurrences = index.get(word, [])  
            occurrences.append(location)
            index[word] = occurrences

for word in sorted(index, key=str.upper):
    print(word, index[word])

# 使用dict.setdefault获取并更新词出现的位置列表
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)
            # 使用setdefault方法获取单词位置列表,若不存在则设置为空列表并返回
            index.setdefault(word, []).append(location)  

for word in sorted(index, key=str.upper):
    print(word, index[word])

4 自动处理缺失的键

4.1 defaultdict:处理缺失键的另一种选择

collections.defaultdict在查找键不存在时,使用默认值创建对应项,默认值由default_factory指定。

import collections
import re
import sys

WORD_RE = re.compile(r'\w+')

# 创建一个defaultdict,默认值为列表
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)
            index[word].append(location)

for word in sorted(index, key=str.upper):
    print(word, index[word])

4.2 __miss__ 方法

dict子类可定义__missing__方法,在__getitem__找不到键时调用,可自定义键查找逻辑,但需注意避免无限递归,__contains__方法的实现也需特殊处理。

class StrKeyDict0(dict):
    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()

d = StrKeyDict0([('2', 'two'), ('4', 'four')])
print(d['2'])  
# 输出: two

print(d[4])  
# 输出: four

try:
    print(d[1])
except KeyError as e:
    print(e)  
# 输出: KeyError: '1'

print(d.get('2'))  
# 输出: two

print(d.get(4))  
# 输出: four

print(d.get(1, 'N/A'))  
# 输出: N/A

print(2 in d)  
# 输出: True

print(1 in d)  
# 输出: False

4.3 标准库对__missing__方法的使用不一致

继承标准库中的映射时,不同基类对__missing__方法的使用方式不同,需谨慎处理,dict子类、collections.UserDict子类、abc.Mapping子类在实现相关方法时对__missing__方法的触发情况不同。

5 dict的变体

5.1 collections.OrderedDict

自 Python 3.6 起,dict保留键顺序,OrderedDict在键顺序相关操作上有优势,如等值检查考虑顺序、popitem方法签名不同、多了move_to_end方法,适用于跟踪近期存取情况。

5.2 collections.ChainMap

ChainMap存放一组映射,可作为整体搜索,查找顺序按输入映射顺序,不复制映射,更新或插入操作影响第一个输入映射,可用于实现支持嵌套作用域的语言解释器。

d1 = dict(a=1, b=3)
d2 = dict(a=2, b=4, c=6)
from collections import ChainMap
chain = ChainMap(d1, d2)
print(chain['a'])  
# 输出: 1

print(chain['c'])  
# 输出: 6

chain['c'] = -1
print(d1)  
# 输出: {'a': 1, 'b': 3, 'c': -1}

print(d2)  
# 输出: {'a': 2, 'b': 4, 'c': 6}

5.3 coolections.Counter

Counter是对键计数的映射,可用于统计可哈希对象数量或当作多重集,实现了组合计数运算符和most_common等方法,用于获取计数最多的项。

import collections
ct = collections.Counter('abracadabra')
print(ct)  
# 输出: Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

ct.update('aaaaazzz')
print(ct)  
# 输出: Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

print(ct.most_common(3))  
# 输出: [('a', 10), ('z', 3), ('b', 2)]

5.4 shelve.Shelf

shelve.Shelf是持久存储字符串键与 Python 对象映射的简单键值 DBM 数据库,是abc.MutableMapping的子类,具有I/O管理方法,键必须是字符串,值必须可被pickle序列化,使用时需注意相关事项。

5.5 子类应该继承UserDict而不是dict

创建新映射类型最好扩展collections.UserDict,它避免了继承dict的一些问题,内部使用组合模式,简化了__contains__等方法的实现,还介绍了MutableMapping.updateMapping.get方法的作用。

import collections

class StrKeyDict(collections.UserDict):
    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]

    def __contains__(self, key):
        return str(key) in self.data

    def __setitem__(self, key, item):
        self.data[str(key)] = item

6 不可变映射

types模块的MappingProxyType可将映射包装成只读的mappingproxy实例,原映射更新会反映在代理实例上,但不能通过代理实例更改映射,可用于防止映射被意外更改。

from types import MappingProxyType
d = {1: 'A'}
d_proxy = MappingProxyType(d)
print(d_proxy[1])  
# 输出: A

try:
    d_proxy[2] = 'x'
except TypeError as e:
    print(e)  
# 输出: 'mappingproxy' object does not support item assignment

d[2] = 'B'
print(d_proxy[2])  
# 输出: B

7 字典视图

dict.keys().values().items()方法返回字典视图,是dict内部数据结构的只读投影,支持一些基本操作,视图对象是动态代理,更新原dict后视图立即能看到变化,dict_keysdict_items类支持类似frozenset的运算符。

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 at 0x10e9e7310>

try:
    print(values[0])
except TypeError as e:
    print(e)  
# 输出: 'dict_values' object is not subscriptable

d['z'] = 99
print(d)  
# 输出: {'a': 10, 'b': 20, 'c': 30, 'z': 99}

print(values)  
# 输出: dict_values([10, 20, 30, 99])

8 dict的实现方式对实践的影响

Python 使用哈希表实现dict,键必须可哈希,访问项速度快,内存布局紧凑但仍占用大量内存,为节省内存不要在__init__方法外创建实例属性,还介绍了相关原因及优化措施。

9 集合论

9.1 set字面量

set字面量句法与数学表示法类似,空set必须写作set(),使用字面量句法比调用构造函数速度快且更具可读性,frozenset没有字面量句法,必须调用构造函数创建。

s = {1}
print(type(s))  
# 输出: <class 'set'>

print(s)  
# 输出: {1}

print(s.pop())  
# 输出: 1

print(s)  
# 输出: set()

print(frozenset(range(10)))  
# 输出: frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

9.2 集合推导式

集合推导式与列表推导式类似,用于从可迭代对象中筛选元素构建集合,不同 Python 进程得到的输出顺序可能不同。

from unicodedata import name
# 构建一个集合,包含码点在32到255之间且名称中带有'SIGN'的字符
print({chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i), '')})  
# 输出: {'§', '=', '¢', '#', '¤', '<', '¥', 'μ', '×', '$', '¶', '£', '©', '°', '+', '÷', '±', '>', '¬', '®', '%'}

10 集合的实现方式对实践的影响

set和frozenset使用哈希表实现,元素必须可哈希,成员测试效率高,但占用内存大,元素顺序取决于插入顺序且不稳定,向集合中添加元素可能导致顺序变化,还介绍了集合元素数量对搜索速度的影响。

11 字典视图的集合运算

.keys()和.items()方法返回的视图对象与frozenset相似,支持集合运算符,可用于获取两个字典的共同键等操作,dict_keys视图始终可当作集合使用,dict_items视图仅当字典值可哈希时可当作集合使用,使用集合运算符处理视图可提高效率。

d1 = dict(a=1, b=2, c=3, d=4)
d2 = dict(b=20, d=40, e=50)
# 获取两个字典的共同键
print(d1.keys() & d2.keys())
# 输出: {'b', 'd'}

s = {'a', 'e', 'i'}
print(d1.keys() & s)
# 输出: {'a'}

print(d1.keys() | s)
# 输出: {'a', 'c', 'b', 'd', 'i', 'e'}

12 本章小结

字典在 Python 中很重要,其字面量句法增强,标准库提供多种映射类型,如defaultdict、ChainMap、Counter等,OrderedDict在特定场景仍有用。映射的setdefault和update方法强大,__missing__方法可自定义键查找行为,MappingProxyType可防止映射被意外更改,字典视图消除了 Python 2 中的内存开销且支持集合运算符。

13 延伸阅读

推荐阅读 Python 标准库文档中 “collections—Container datatypes” 获取映射类型示例和实践技巧,查看collections模块源码了解映射逻辑,《Python Cookbook 中文版(第 3 版)》第 1 章有相关经典实例。还介绍了其他相关文章和演讲,如继续使用OrderedDict的理由、字典视图功能的提出、不同 Python 解释器中字典的实现、集合相关的 PEP 和演讲等,以及一些与字典和集合相关的其他知识,如语法糖、哈希表的发明等。


原文地址:https://blog.csdn.net/HearMeRoar_/article/details/144411116

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!