Post

《FluentPython2ndEdition》-第一部分

第一章 Python数据模型

  • 通过特殊方法利用Python数据模型,这样做有两个优点
    • 类的用户不需要记住标准操作的方法名称。
    • 可以充分利用Python标准库,例如random.choice函数,无需重复发明轮子。

特殊方法是如何使用的?

要明确一点,特殊方法供Python解释器调用,而不是你自己。也就是说,没有 my_object.len()这种写法,正确的写法是len(my_object)。

特殊方法的重要用途:

模拟数值类型

__abs__获取对象的绝对值

mul、__add__返回一个新的Vector实例,没有修改运算对象,只读取self或other。

对象的字符串表示形式

__repr__获取对象的字符串表现形式。

Vector类__repr__方法中的f字符串使用!r以标准的表示形式显示属性。

于此形成对照的是__str__方法由内置函数str()调用,在背后供print()函数使用,返回对终端用户友好的字符串。

如果你熟悉的边程序语言使用ToString()方法,你可能习惯实现__str__方法,而不是__repr__方法。在Python中如果必须二选一的话,请选择__repr__方法。

对象的布尔值

默认情况下,用户定义类的实例都是真值,除非实现了__bool__或__len__方法。

容器API

每一个容器类型均应实现如下事项

  • Iterable要支持for、拆包和其他迭代方式
  • Sized要支持内置函数len
  • Container要支持in运算符

Python不强制要求具体类继承这些抽象基类中的任何一个。这要实现了__len__方法,就说明那个类满足Sized接口。

Collection有3个非常重要的专用接口

  • Sequence规范和list和str等内置类型的接口
  • Mappingi被dict、collections.defaultdict等实现
  • Set是set和frozenset两个内置类型的接口

len为什么不是方法

因为经过了特殊的处理,被当作Python数据模型的一部分,就像abs函数一样。但是借助特殊方法__len__,也可以让len适用于自定义对象。这是一种相对公平的折中方案,既满足了对内置对象速度的要求,又保证了语言的一致性。

延伸阅读 Python语言参考手册

第二章 丰富的序列

深入理解Python中不同的序列类型,不但能避免重新发明轮子,还可以从他们共通的接口上受到启发,在自己实现API时合理支持及利用现有和将来可能添加的序列类型。

内置序列概览

容器序列

可存放不同类型的项,其中包括嵌套容器。例如 list、tuple 和 collections.deque。

存放的是对象的引用,对象可以是任意类型。

扁平序列

可存放一种简单类型的项,例如 str、bytes 和 array.array。

在自己的内存空间中存储所含内容的值,而不是各自不同的Python对象。

列表推导式和生成器表达式

使用列表推导式(目标是了列表)或生成器表达式(目标是其他序列类型)可以快速构建一个序列。

使用这两种句法写出的代码更容易理解,速度通常更快。

列表推导式涵盖map和filter两个函数的功能。

生成器一次产出一项提供给for循环,如果是两个各有1000项的列表,则使用生成器表达式计算笛卡尔积可以节省大量内存,因为不用先构建一个包含100万项的列表提供给for循环。

元组不仅仅是不可变列表

元祖有两个作用,除了可以作为不可变列表外,还可用作没有字段名称的记录。

一般使用_表是虚拟变量

Python解释器和标准库经常把元祖当做不可变列表使用,这样做意图清晰(只要在源码中见到元组,你就知道它长度不可变),性能优越。

元祖的不可变性仅针对元祖中的引用而言。元祖中的引用不可删除,不可替换。倘若引用的时可变对象,改动对象之后,元祖的值也会随之变化。存放可变项的元组可能会导致bug。

序列和可迭代对象拆包

拆包的特点是不用我们自己动手通过索引从序列中提取元素,这样就减少了出错的可能。

拆包的目标可以是任意可迭代对象。

定义函数时可以使用*args捕获余下的任意数量的参数,这是Python的一个经典特性。

序列模式匹配

match关键字后面的表达式是 匹配对象 ,即各个case字句中尝试匹配的数据。

case _ 是默认的case语句,相当于C#中的default。

表面上看,match/case与C语言中的switch/case语法很相似。与swith相比,match的一大改进时支持 析构,这是一种高级拆包形式。

在match/case上下文中,str、bytes和bytearray实例不作为序列处理。match把这些类型视为原子值,就像证书987被视为一个整体值,而不是数字序列。

与拆包不同,模式不析构序列意外的可迭代对象。

添加类型信息可以让模式更加具体。

以if开头的卫语句是可选的,仅当匹配模式时才运行。

模式匹配是一种声明式编程风格,即描述你想匹配什么,而不是如何匹配,这样写出的代码结构与数据结构是一致的。

18.3节还会进一步分析lis.py,届时将全面研究evaluate中的match/case语句,如果想要深入了解lis.py,可阅读Norvig写的文章:​“(How to Write a (Lisp)Interpreter (in Python)) ”​。

切片

本节讨论切片的高级用法。

为什么切片和区间排除最后一项

切片和区间排除最后一项是一种Python风格约定,有以下好处

  • 在仅指定停止位置时,容易判断切片或区间的长度。
  • 同时指定起始和停止位置时,容易计算切片或区间的长度,做个减法即可:stop - start。

arr = [10,20,30,40,50,60]
arr[:2] 代表从开始位置到索引2截止,排除最后一项 arr[2:] 代表从索引2开始到最后位置截止,包含最后一项

还可以使用arr[a:b:c]句法来制定步距c,让切片跳过部分项。步距也可以是负数,反向返回项。

a:b:c表示法只在[]内部有效,表是索引或下标索引。

多维切片和省略号

例如,在外部包numPy中,numpy.ndarray表示的二维数组可以使用a[i,j]句法获取数组中的元素,还可以使用表达式a[m:n, k:l]获取二维切片。

NumPy在处理多位数组切片时把…解释为一种快捷句法。例如,对四位数组x x[i, …]是x[i, :, :, :]的快捷句法。

为切片赋值

在赋值语句的左侧使用切片表示法,或者作为del语句的目标,可以就地移植、切除或以其他方式修改可变序列。

如果赋值目标是一个切片,则右边必须是一个可迭代对象,即便只有一项。

使用 + 和 * 处理序列

+和 * 始终创建一个新对象,绝不更改操作数

初始化潜逃列表可以使用 * 运算符,例如 board = [[‘_’] * 3 for i in range(3)]

对不可变序列重复拼接效率低下,因为解释器必须复制整个目标序列,创建一个新序列,包含要拼接的项,而不是简单追加新项

不要在元组中存放可变的项

增量赋值不是原子操作。

检查Python字节码不太难,从中可以看出Python在背后做了什么。

list.sort与内置函数sorted

list.sort方法就地排序列表,即不创建副本,返回值为None,目的就是提醒我们,它更改了接收者,没有创建新列表。这是PythonAPI的一个重要约定:就地更改对象的函数或方法应该返回None,让调用方清楚地知道接收者已被更改,没有创建新对象

与之相反,内置函数sorted返回创建的新列表。该函数接收任何可迭代对象作为参数,包括不可变序列生成器。无论传入什么类型的可迭代对象,sorted函数始终反悔新创建的列表。

顺便说一下,使用key参数,哪怕掺杂数值和类似数值的字符串,也可以排序。我们只需要决定把所有项全都视为整数还是字符串。

当列表不适用时

array

使用数组处理上百万个浮点数可以节省大量内存。数组支持所有可变序列操作,此外还有快速加载项和保存项的方法。

从Python3.10开始,array类型没有列表那种就低排序方法sort,如果需要排序,请使用内置函数sorted重构数组。原因可能是数组是存储在连续的空间内,使用sort方法原地排序性能开销较大。

memoryview

内置的memoryview类型是一种共享内存的序列类型,可在不复制字节的情况下处理数组的切片。

memoryview是NumPy中一种普遍使用的结构,本质上就是Python中的数组。memoryview在数据结构(例如PIL图像、SQLite数据库、NumPy数组等)之间共享内存,而不是事先复制,这对大型数据集来说非常重要。

如果要对数组做一些高级数值处理,应该使用NumPy库。

NumPy

科学计算需要经常做一些高级数组和矩阵运算,得益于NumPy,Python成为这一领域的主流语言。NumPy实现了多维同构数组和矩阵类型,除了存放数值外,还可以存放用户定义的记录,而且提高了高效的元素层面操作。

NumPy和SciPy这两个库功能异常强大,为很多优秀的工具提供了坚实的基础,例如Pandas和scikit-learn。

双端队列和其他队列

列表可以当做栈或队列使用,但是插入和删除项有一定开销,因为整个列表都必须在内存中移动。

collections.deque类实现一种线程安全的双端队列,旨在快速在两端插入和删除项。

除了deque外,Python标准库中的其他包还实现了以下队列

  • queue
  • multiprocessing
  • asyncio
  • heapq

第三章 字典和集合

Python中的字典能如此高效,要归功于 哈希表

除了字典外,内置类型中的set和frozenset也基于哈希表。

字典的现代用法

字典推导式 从任何可迭代对象中获取键值对,构建dict实例。

调用函数时,不止一个参数可以使用**。但是,所有键都要是字符串,而且在所有参数中是唯一的。

**可以在dict字面量中使用,同样可以多次使用。这种情况下允许键重复,后面的键覆盖前面的键。

使用|合并映射

Python3.9支持=操作符合并映射。因为两者也是并集运算符。
使用运算符创建一个新映射,通常新映射的类型与左操作数的类型相同。
如果想就地更新因故射,则使用=运算符。

使用模式匹配处理映射

不同类型的模式可以组合和嵌套,不同类型的模式可以组合和嵌套。借助析构可以处理嵌套和序列等结构化记录。我们经常需要从Json API和具有半结构化的数据库中读取这类记录。

模式中键的顺序无关紧要。

倘若你想把多出的键值对捕获到一个dict中,可以在一个变量前面加上**,不过必须放在模式最后。

映射类型的标准API

可哈希指的是什么?

如果一个对象的哈希玛在整个生命周期内永不改变(依托__hash__方法),而且可与其他对象比较(依托__eq__方法),那么这个对象就是可哈希的。两个哈希对象仅当哈希玛相同时相等。

数值类型和不可变的扁平类型str和bytes都是可哈希的。

一个对象的哈希玛根据所用的Python版本和设备架构有所不同。正确实现的对象,其哈希玛在一个Python进程内保持不变。

插入或更新可变的值

根据Python的快速失败原则,当键k不存在时,d[k]抛出错误,如果需要默认值,可以把d[k]换成d.get(k, default)。这样写并不完美,最好使用d.setdefault(key,[]).append(value)。

自动处理缺失的键

人为设置默认值有两种办法:第一种是把普通的dict换成defaultdict,第二种是定义dict或其他映射类型的子类,实现__missing__方法。

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

实现的原理是,实例化defaultdict对象时提供一个可调用对象,当__getitem__遇到不存在的键时,调用那个可调用对象生成一个默认值。

举个例子,假设使用 dd = defaultdict(list)创建一个defaultdict对象,而且dd中没有”new-key”键,那么dd[“new-key”]表达式按以下几步处理。

  • 调用list()创建一个新列表
  • 把该列表插入dd,对应到’new-key’键上。
  • 返回该列表的引用。

__missing__方法

映射处理缺失键的底层逻辑在__missing__方法中。dict基类本身没有定义这个方法,但如果dict的子类定义了这个方法,那么dict.__getitem__找不到键时将调用__mingssing__方法,不抛出KeyError。

自己定义的类,如果继承标准库中的映射,在实现__getitem__、get或__contains__方法时不一定要回落到__missing__方法,因为标准库对missing__方法的使用不一致。

dict子类只实现__mingssing方法,其他均不实现。

collections.UserDict子类,同样实现__missing__方法,其他均不实现。继承自UserDict的get方法调用__getitem。在查找键时可能会调用missing方法。

abc.Maaping子类,以最简单的方式实现__getitem__方法

abc.Mapping子类,实现__getitem__方法,并定义__missing__方法。

dict的变体

collections.OrderedDict

自Python3.6起,内置的dict也保留键的顺序,使用OrderedDict的主要原因是编写与早期Python版本兼容的代码。

collections.ChainMap

ChainMap实例存放一组映射,可作为一个整体来搜索。此实例不复制输入映射,而是存放映射的引用。

collections.Counter

这是一种对键计数的映射。更新现有的键,计数随之增加。可用于统计可哈希对象的实例数量,或者作为多重集使用。

shelve.Shelf

标准库中的shelve模块持久存储字符串键与Python对象之间的映射。

shelve.Shelf时abc.MutableMapping的子类,提供了我们预期的映射类型的基本方法。

shelve.SHelf还提供了一些其他I/O管理方法,例如sync和close。

Shelf实力是上下文管理器,因此可以使用with块确保在使用后关闭

子类应继承UserDict而不是dict

创建新的映射类型,最好扩展collections.UserDict而不是dict,主要原因是,内置的dict在实现上走了一些捷径,如果继承dict,那就不得不覆盖一些方法,而继承UserDict则没有这些问题。

要注意的是,UserDict没有继承dict,使用的是组合模式:内部有一个dict实例,名为data,存放具体的项。

由于UserDict扩展abc.MutableMapping,因此使StrKeyDict成为一种功能完整的映射方法,是从UserDict、MutableMapping或Mapping继承的方法。

不可变映射

types模块提供的MappingProxy是一个包装类,把传入的映射包装成一个mappingproxy实例,这是原映射的动态代理,只可读取。

字典试图

视图对象是动态代理。更新原dict对象后,现有视图立即能看到变化。

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

Python使用哈希表实现dict,因此字典的效率非常高,不过这种设计对实践也有一些影响,不容忽视。

  • 键必须是可哈希的对象。
  • 通过键访问项速度非常快。
  • 在CPython 3.6中,dict的内存布局更为紧凑,顺带的一个副作用是键的顺序得以保留。
  • 尽管采用了新的紧凑布局,但是字典仍占用大量内存,这是不可避免的。
  • 为了节省内存,不要在__init__方法之外创建实例属性。

集合论

集合是一组唯一的对象,集合的基本作用就是去除重复项。

集合元素必须是可哈希的对象。set类型不可哈希,因此不能构建嵌套set实例的set对象。但是fronzenset可哈希,所以set对象可以包含frozenset元素。

除了强制唯一性以外,集合类型通过中缀运算符实现了许多集合运算。给定两个集合a和b,ab是a和b的并集,a&b是a和b的交集,a-b是a和b的差集,a^b是a和b的对称差集。

set字面量

set字面量的句法与集合的数学表示法几乎一样

创建空set,务必使用不带参数的构造函数,即set。

frozenset没有字面量句法,必须调用构造函数创建。

集合推导式

集合推导式用法: {chr(i) for i in range(32, 256) if ‘SIGN’ in name(chr(i), ‘’))

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

set和frozenset类型都使用哈希表,这种设计带来了以下影响:

  • 集合元素必须是可哈希的对象。
  • 成员测试效率非常高
  • 与存放元素指针的底层数组相比,集合占用大量内存。
  • 元素的顺序取决于插入顺序,但是顺序对集合没有意义,也得不到保障。
  • 向集合中添加元素后,元素的顺序可能会发生变化。

dict_keys视图始终可以当做集合使用,因为按照其设计,所有键均可哈希。

使用集合运算符处理视图可以省去大量的循环和条件判断。

第四章Unicode文本和字节序列

本章讨论Unicode字符串、二进制序列,以及在二者之间转换时使用的编码。

字符问题

字符串是个相当简单的概念:一个字符串就是一个字符序列。问题出在字符的定义上。

在2021年,字符的最佳定义是Unicode字符。因此,从Python3的str对象中获取的项是Unicode字符。

Unicode标准明确区分字符的标识和具体的字节表述。

字符的表示即码点,是0~1114111范围内的数(十进制),在Unicode标准中以4~6个十六进制数表示,前加“U+”,取值范围是U+0000~U+10FFFF。。

字符的具体表述取决于所用的编码。编码是在码点和直接系列之间转换时使用的算法。例如,字母A(U+0041)在UTF-8编码中使用单个字节\x41标书,而在UTF016LE编码中使用字节序列\x41\x00表述。再比如,欧元符号(U + 20AC)在UTF-8编码中需要3个字节,即\xe2\x82\xac,而在UTF-16LE中,同一个码点编码成两个字节,即\xac\x20。

把码点转为字节序列的过程叫编码,把字节序列转为码点的过程叫解码。

字节概要

Python内置两种基本的二进制序列类型:bytes和bytearray。

bytes和bytearray中的项是0~255范围内的整数。然而,二进制序列的切片始终是同一类型的二进制序列,包括长度为1的切片。

构建bytes和bytearray实例可以调用各自的构造函数,传入以下参数

一个str对象和encoding关键字

一个可迭代对象,项为0-255范围内的数

一个实现了缓冲协议的对象。构造函数把源对象中的字节序列复制到新创建的二进制序列中。

基本的编码解码器

Python自带超过100种编码解码器(codec, encoder/decoder),用于在文本和字节之间相互转换。

处理编码和解码问题

UnicodeError是一般性的异常,Python在报告错误时通常更具体,抛出UnicodeEncodeError或UnicodeDecodeError。。如果源码的编码与预期不符,那么加载Python模块时还可能跑出SyntaxError。

多数非UTF编码解码器只能处理Unicode字符的小部分子集。把文本转换成字节序列时,如果目标编码没有定义某个字符,则会抛出UnicodeEncodeError,除非把errors参数传递给编码方法或函数,做特殊处理。

并非所有字节都包含有效的ASCII字符,也并非所有字节序列都是有效的UTF-8或UTF-16码点。

如何找出字节序列的编码

简单来说,不能,只能由别人来告诉你。

有些通信协议和文件格式,例如HTTP和XML,通过首部明确指明内容编码。如果字节流中包含大于127的字节值,则可以肯定,用的不是ASCII编码。另外,按照UTF-8和UTF-16编码的设计方式,可用的字节序列也受到限制。

就像人类的语言也有规则和限制一样,只要假定字节流是人类可读的纯文本,就可能通过试探和分析找出编码。

BOM:有用的鬼符

UTF-16编码的开头有几个额外的自己,例如:b’\xff\xfeE\x001\x00\x00N。其中xfe指得就是BOM,即字节序标记(byte-order mark),指明编码时使用Intel CPU的小端序。

在小端序设备中,各个码点的最低有效字节在前面。在大端序CPU中,编码顺序反过来,’E’被编码为0和69。

UTF-16有两个变种:UTF-16LE,显示指明使用小端序;UTF-16BE,显示指明使用大端序。如果直接指明使用这两个变种,则不生成BOM。

处理文本文件

Unicode三明治:输入时解码字节序列;中间层只处理文本;输出时编码文本。

Python3中,可以轻松采纳Unicode三明治的建议,因为内置函数open()在读取文件时会做必要的解码,以文本模式写入文件时还会做必要的编码,所以调用my_file.read()方法得到的以及my_file.write()方法传入的都是str对象。

在Python中,I/O默认使用的编码受到几个设置的影响。

总结一下编码:

  • 打开文件时如果没有指定encoding参数,则默认编码由locale.getpreferredencoding()函数决定。
  • 在二进制数据与str之间转换时,Python内部使用sys.getfilesystemencoding()函数决定编码。该设置不可更改。
  • 编码和解码文件名使用sys.getfilesystemencoding()函数决定。

locale.getpreferredencoding()函数根据用户偏好设置,返回文本数据的编码。用户偏好设置在不同的系统中以不同的方式设置,而且在某些系统只可能无法通过编程方式设置,因此这个函数返回的只是猜测的编码。

因此,关于默认编码的最佳建议:别依赖默认编码。

为了正确比较而规范化Unicode字符串

因为Unicode有组合字符,所以字符串比较起来比较复杂。解决方案是使用unicodedata.normalize()函数。该函数的第一个参数是NFC、NFD、NFKC和NFKD之一,表示要使用的规范。

大小写同一化

就是把所有文本都转为小谢,再做些其他转换。这个操作由str.casefold()函数完成。

规范化文本匹配的实用函数

如果需要处理多语言文本,应该使用nfc_equal和fold_equal函数。

极端“规范化”:去掉变音符

去掉变音符不是正确的规范化方式,因为这往往会改变词的意思,而且可能让人误判搜索结果。

Unicode文本排序

给任何类型的序列排序,Python都回注意比较序列中的每一项。对字符串来说,比较的是码点。可是,遇到飞ASCII字符,比较就会出错。

在Python,非ASCII文本的标准排序方式是用locale.strxfrm函数。这个函数“把字符串转成适合所在区域进行比较的形式”。

Unicode数据库

Unicode标准提供了一个完整的数据库(许多结构化文本文件),不仅包括码点名称之间的映射表,还包括各个字符的元数据,以及字符的关系。

unicodedata.category()返回char在Unicode数据库中的类别。

按名称查找字符

unicodedata模块中有几个函数用于获取字符的元数据。例如unicodedata.name()返回一个字符在标准中的官方名称。

字符的数值意义

unicodedata模块中有几个函数可以检查Unicode字符是不是不表示数值,如果是的话还能确定人类可读的具体数值,而不是码点数。unicodedata.name()和unicodedata.numeric()函数,以及str的.isdecimal()和.isnumeric()方法。

支持str和bytes的双模式API

Python标准库中的一些函数能接受str或bytes为参数,根据其具体类型的不同展现不同的行为。

第五章数据类构建器

Python提供了几中构建简单类的方式,这些类只是字段的容器,几乎没有额外功能。这种模式被称为“数据类”(data class),dataclass包就支持该模式。以下是三个可简化数据类构建过程的构建器。

最简单的构建方式,从Python2.6开始提供。

写法: from collections import namedtuple Coordinate = namedtuple(‘Coordinate’, ‘lat lon’)

typing.NamedTuple 另一种构建方式,需要为字段添加类型提示,从Python3.5开始提供。

写法: import typing Coordinate = typing.collections.namedtupleNamedTuple(‘Coordinate’, [(‘lat’, float), (‘lon’, float)])

这种方式可读性高,而且可以通过映射指定字段及类型,再使用**fields_and_types拆包。

@dataclasses.dataclass 一个类装饰器,与前两种相比,可定制的内容更多,增加了大量选项,可实现更复杂的功能,从Python3.7开始提供。

写法: from dataclasses import dataclass

@dataclass class Coordinate: lat: float lon: float

区别在于class语句上,@dataclass装饰器不依赖继承或元类,如果你想使用这些机制,则不受影响。

3个数据类构建器有许多主要功能如下:

  • 可变实例
    • 3个数据类构建器之间的主要区别在于,collections.namedtuple和typing.NamedTuple返回不可变实例,而@dataclass返回可变实例。
  • class语句句法
    • 只有typing.NamedTuple和@dataclass支持class语句,方便为构建的嘞增加方法和文档字符串。
  • 构造字典
    • 两种具名元组都提供了构造dict对象的实例方法(.asdict),可根据数据类实例的字段构造字典。
  • 获取字段名称和默认值
    • 3个类构建器都支持获取字段名称和可能配置的默认值。
  • 获取字段类型
    • typing.NamedTuple和@dataclass定义的类有一个__annotations__属性,返回名称到类型的映射。使用typing.get_type_hints()函数获取。
  • 更改之后创建新实例

  • 运行时定义新类

典型的具名元组

collections.namedtuple是一个工厂函数,用于构建增强的tuple子类,具有字段名称、类名和提供有用信息的__repr__方法。namedtuple构建的类可在任何需要使用元组的地方使用。

from collections import namedtuple

City = namedtuple(‘City’, ‘name country population coordinates’)

tokyo = City(‘Tokyo’, ‘JP’, 36.993, (35.689722, 139.691667))

创建元组nametuple需要制定两个参数:一个类名和一个字段名称列表。后一个参数可以是产生字符串的可迭代对象,也可以是一整个以空格分割的字符串。

字段名称必须以单个位置参数传递给构造函数,可以通过名称或者位置访问字段。

带类型的具名元组

from typeing import NamedTuple

class Coordinate(NamedTuple): lat: float lon: float reference: str = ‘WGS84’

每个字段都要注解类型

实例字段reference注解了类型,还指定了默认值

使用typing.NamedTuple构建的类,拥有的方法并不比collections.namedtuple生成的更多,而且同样也从tuple集成方法。唯一的区别是多了类属性__annotations__,而在运行时,Python弯曲忽略该属性。

typing.NamedTuple的主要功能是类型注解。

类型提示入门

首先你要知道,Python字节码编译器和解释器并不强制要求你提供类型信息。

Python的类型提示可以看作是:供IDE和类型检查工具验证类型的文档。这是因为类星体是其实对Python程序的运行时没有影响。

类型提示主要是为了第三方类型检查工具提供支持,例如Mypy和PyCharm IDE内置的类型检查器。这些都是静态分析工具,在静止状态下检查Python源码,不运行代码。

类属性是描述符,后续章节会讲到,现在可以把描述符理解为特性(property)读值(getter)方法,即不带调用运算符()的方法,用于读取实例属性。元组是不可变的。

@dataclass详解

@dataclass为Python数据类装饰器,主要用于建华数据类的创建过程,减少了样板代码。适用于需要大量数据存储和处理的场景,如配置文件、数据库模型等。

这个装饰器接受叫多个关键参数,完整签名如下:

@datasclass(*, init=Ture, repr=True, eq=True, order=False, unsave_hash=False, frozen=False)

第一个参数*表示后面都是关键参数。

@dataclass装饰器接受的关键字参数

Python规定,带默认值的参数后面不能由不带默认值的参数。类属性通常用作实例属性的默认值,数据类也是如此。

类属性通常用作实例属性的默认值,数据类也是如此。@dataclass使用类型中的默认值生成传给__init__方法的参数默认值。

在设计可变对象如列表时,为确保@dataclass能正确处理默认值,需要采取一些额外的措施。因为默认值是共享的,如果不使用default_factory,可能会导致默认值重复的问题。

default_factory是field最常用的参数。

@dataclass应该只做一件事:把传入的参数及其默认值(如未指定值)赋值给实例属性,变成实例字段。可是,有时候初始化实例要做的不止这些,这时候就可以通过__post_init__方法。如果存在这个方法,则@dataclass将在生成的__init__方法最后调用__post_init__。

有时,也需要把需要把不作为实例字段的参数传给__init__方法。这种参数叫“仅作初始化的变量”(init-only variable)。为了声明这种参数,dataclass模块提供了伪类型InitVal。

@dataclass Class C: i : int j : int = None database : InitVal[DatabaseType] = None def post_init(self, databse): if self.j is None and database is not None: self.j = database.lookup(‘j’) c = C(10, databse = my_aatabase)

InitVal阻止@dataclass把Database视为常规字段,因此dataclass不会被设为实例属性,也不会出现在dataclass.fields函数返回的列表中。然而,对于生成的__init__方法,database是参数之一,同时也会传给__post_init__方法。

@dataclass示例:都柏林核心模式

都柏林核心(Dublin Core)模式是一个小组术语,可用于描述数字资源,也可用于描述物理资源,例如图书、CD和艺术品等对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum, auto
from datetime import date

class ResourceType(Enum):
    BOOK = auto()
    EBOOK = auto()
    VIDEO = auto()

@dataclass
class Resource
    """描述媒体资源"""
    identifier: str
    title: str = '<untitled>'
    creators: list[str] = filed(default_factory=list)
    date: Optional[date] = None
    type: ResourceType = ResourceType.BOOK
    description: str = ''
    language: str = ''
    subjects: list[str] = field(default_factory=list)

数据类的确很方便,但是如果过度使用,也会为你的项目带来不好的影响.

数据类导致代码异味

所谓数据类是指他们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过分繁琐地操控着。

面向对象编程的主要思想是把行为和数据放在同一个代码单元(类)中。如果一个类使用广泛,但是自身没有什么重要的行为,那么整个系统可能遍布处理示例的代码,并出现在很多方法和函数中。

有几种情况适合使用没什么行为或者没任何行为的数据类:

  1. 把数据类用作脚手架
  2. 把数据类用作中间表述

模式匹配类实例

类模式通过类型和属性(可选)匹配实例。类模式的匹配对象可以是任何类的实例,而不是仅仅是数据类的实例。

类模式有三种变体:简单类模式、关键字类模式和位置类模式。

5.9本章小结

本章主要讲了三个数据类构建器:collections.namedtuple、typing.NamedTuple和dataclasses.dataclass。每个构建器都可以根据给工厂函数的参数生成数据类,后两个构建器还可以通过class语句提供类型提示。两种具名元组变体生成的书tuple子类,与普通的元组相比,增加了通过名称访问字段的功能,另外还提供了一个类属性_fields,以字符串元组的形式列出字段名称。

第六章 对象引用、可变性和垃圾回收

变量不是盒子

在面相对象编程语言的引用式变量中,变量不是盒子指的是,b = a,只是把a的引用给b,而不是把a的值复制给b。把变量想成盒子无法理解这个行为,影响想成便利贴,b = a 只是在a上贴了一个便利贴b。

同一性、相等性和别名

对象一旦创建,其标识始终不变。可以把标识理解为对象在内存中的地址。is运算符比较两个对象的标识,id()函数返回对象标识的整数表示。

实际编程中很少使用id()函数。对象的标识最常使用is运算符比较,无须直接调用id()函数。

元组(tuple)的相对不可变性

元组与多数Python容器(集合、字典、列表等)一样,存储的是对象的引用,如果引用的项是可变的,即使元组本身不可变,项也可以更改。也就是说元组的不可变形其实是指tuple数据结构的物理内容不可变,与引用对象无关。

默认做浅拷贝

复制列表(多数内置的可变容器)最简单的办法就是使用内置的构造函数。也可以使用[:]来创建副本,这两者都是浅拷贝(即复制最外层的容器,副本中的项是原容器的项的引用)。

有时候需要做深拷贝,可以使用copy模块提供的copy和deepcopy函数,分别对任意对象做浅拷贝和深拷贝。

函数的参数是引用时

Python唯一支持的参数传递模式是共享传参(call by sharing)。多数面向对象语言采用这一模式,包括JavaScript、Ruby和Java(Java的引用类型是这样,原始类型按值传参)​。

共享传参指函数的形参获得实参引用的副本。也就是说,函数内部的形参是实参的别名。

不要使用可变类型作为参数的默认值

默认值在定义函数时求解,因此默认值变成了函数对象的属性。所以,如果默认值是可变对象,而且修改了他的值,那么后续的函数调用都会受到影响。可变默认值导致的这个问题说明了为什么通常使用None作为接收可变值的参数的默认值。

如果你定义的函数接收可变参数,那就应该谨慎考虑调用方是否期望修改传入的参数。

除非方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。如果不确定,那就创建副本,免得给客户添麻烦。当然,创建副本会消耗一定的CPU和内存。但是,与速度和资源相比,在API中埋下难以察觉的bug显然是更严重的问题。

del和垃圾回收

del语句删除引用,而不是对象。del可能导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用时。重新绑定也可能导致对象的引用数量归零,致使对象被销毁。

Python对不可变类型施加的把戏

共享字符串字面量是一种优化措施,称为驻留(interning)。千万不要依赖字符串或整数的驻留行为!比较字符串或整数是否相等时,应该使用==,而不是is。驻留是Python解释器内部使用的功能。本节讨论的把戏,包括frozenset.copy()的行为,是“善意的谎言”​,能节省内存,提升解释器的速度。别担心,这种行为不会给你添任何麻烦,因为只有不可变类型受到影响。或许这些细枝末节的最佳用途是与其他Python程序员打赌,提高自己的胜算。

6.8本章小结

每个Python对象都有标识、类型和值。只有对象的值不时变化。

This post is licensed under CC BY 4.0 by the author.