流畅的Python
卢西亚诺·拉马略
前言
- 正因为它既好学又好用,所以很多Python程序员只用到了其强大功能的一小部分。
- 不成熟的抽象和过早的优化一样,都会坏事。
- 抽象基类(abstract base class, ABC)
- REPL(read-eval-print loop,读取、求值、输出的循环)
- doctest是Python的一个标准库,做测试用的。这个库通过模拟控制台对话来检验表达式求值是否正确
第一部分 序幕
- Guido知道如何在理论上做出一定妥协,设计出来的语言让使用者觉得如沐春风,这真是不可多得。
- Python最好的品质之一是一致性。
- “Python风格”(Pythonic)
- 数据模型其实是对Python框架的描述,它规范了这门语言自身构建模块的接口
- Python解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对象操作,这些特殊方法的名字以两个下划线开头,以两个下划线结尾(例如__getitem__)
- 特殊方法也叫双下方法(dunder method)
- namedtuple就加入到Python里,用以构建只有少数属性但是没有方法的对象,比如数据库条目。
- Python已经内置了从一个序列中随机选出一个元素的函数random.choice
- 迭代通常是隐式的,譬如说一个集合类型没有实现__contains__方法,那么in运算符就会按顺序做一次迭代搜索。
- abs是一个内置函数,如果输入是整数或者浮点数,它返回的是输入值的绝对值;如果输入是复数(complex number),那么返回这个复数的模。
- Python有一个内置的函数叫repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”
- __repr__和__str__的区别在于,后者是在str( )函数被使用,或是在用print函数打印一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。
- 中缀运算符的基本原则就是不改变操作对象,而是产出一个新的值。
- bool(x)的背后是调用x.__bool__( )的结果;如果不存在__bool__方法,那么bool(x)会尝试调用x.__len__( )。若返回0,则bool会返回False;否则返回True。
- 换句话说,len之所以不是一个普通方法,是为了让Python自带的数据结构可以走后门,abs也是同理
- Python对象的一个基本要求就是它得有合理的字符串表示形式,我们可以通过__repr__和__str__来满足这个要求。前者方便我们调试和记录日志,后者则是给终端用户看的。
- 元对象所指的是那些对建构语言本身来讲很重要的对象,以此为前提,协议也可以看作接口。也就是说,元对象协议是对象模型的同义词,它们的意思都是构建核心语言的API。
第二部分 数据结构
- 迭代、切片、排序,还有拼接
- 容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。
- 掌握列表推导还可以为我们打开生成器表达式(generator expression)的大门,后者具有生成各种类型的元素并用它们来填充序列的功能。
- 列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列
- 通常的原则是,只用列表推导来创建新的列表,并且尽量保持简短。如果列表推导的代码超过了两行,你可能就要考虑是不是得用for循环重写了。
- 列表推导、生成器表达式,以及同它们很相似的集合(set)推导和字典(dict)推导,在Python 3中都有了自己的局部作用域,就像函数似的。
- 列表推导可以帮助我们把一个序列或是其他可迭代类型中的元素过滤或是加工,然后再新建一个列表。
- tshirts = [(color, size) for color in colors for size in sizes]
- 列表推导的作用只有一个:生成列表。
- 生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。
- 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。
- 用到生成器表达式之后,内存里不会留下一个有6个组合的列表,因为生成器表达式会在每次for循环运行时才生成一个组合
- 除了用作不可变的列表,它还可以用于没有字段名的记录
- 元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置
- for循环可以分别提取元组里的元素,也叫作拆包(unpacking)。因为元组中第二个元素对我们没有什么用,所以它赋值给“_”占位符。
- 元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。
- 还可以用*运算符把一个可迭代对象拆开作为函数的参数:
- 比如os.path.split( )函数就会返回以路径和最后一个文件名组成的元组(path, last_part):
- 在进行拆包的时候,我们不总是对元组里所有的数据都感兴趣,_占位符能帮助处理这种情况
- 在Python中,函数用*args来获取不确定数量的参数算是一种经典写法了。
- 在Python 3之前,元组可以作为形参放在函数声明中,例如def fn(a, (b, c), d):。然而Python 3不再支持这种格式
- collections.namedtuple是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。
- collections.namedtuple是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类
- 用namedtuple构建的类的实例所消耗的内存跟元组是一样的,因为字段名都被存在对应的类里面。
- 创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串。
- _fields类属性、类方法_make(iterable)和实例方法_asdict( )
- 我们还可以用s[a:b:c]的形式对s在a和b之间以c为间隔取值。c的值还可以为负,负值意味着反向取值
- 对seq[start:stop:step]进行求值的时候,Python会调用seq.__getitem__(slice(start, stop, step))
- 对象的特殊方法__getitem__和__setitem__需要以元组的形式来接收a[i, j]中的索引。也就是说,如果要得到a[i, j]的值,Python会调用a.__getitem__((i, j))。Python内置的序列类型都是一维的,因此它们只支持单一的索引,成对出现的索引是没有用的。
- 如果把切片放在赋值语句的左边,或把它作为del操作的对象,我们就可以对序列进行嫁接、切除或就地修改操作。
- 如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独一个值,也要把它转换成可迭代的序列
- +和*都遵循这个规律,不修改原有的操作对象,而是构建一个全新的序列。
- 含有3个指向同一对象的引用的列表是毫无用处的
- +=背后的特殊方法是__iadd__ (用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python会退一步调用__add__ 。
- 对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素
- Python Tutor是一个对Python运行原理进行可视化分析的工具。图2-
- 不要把可变对象放在元组里面。
增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
查看Python的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。
- bisect模块包含两个主要函数,bisect和insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。
- bisect_left返回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会被放置于它相等的元素的前面,而bisect_right返回的则是跟它相等的元素之后的位置
- insort(seq, item)把变量item插入到序列seq中,并能保持seq的升序顺序
- 要存放1000万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是float对象,而是数字的机器翻译,也就是字节表述。
- memoryview是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。
- SciPy是基于NumPy的另一个库,它提供了很多跟科学计算有关的算法,专为线性代数、数值积分和统计学而设计。
- collections.deque类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。
- extendleft(iter)方法会把迭代器里的元素逐个添加到双向队列的左边,因此迭代器里的元素会逆序出现在队列里。
- append和popleft都是原子操作,也就说是deque可以在多线程程序中安全地当作先进先出的队列使用,而使用者不需要担心资源锁的问题。
- 列表推导和生成器表达式则提供了灵活构建和初始化序列的方式
- 元组则恰恰相反,它经常用来存放不同类型的的元素。这也符合它的本质,元组就是用作存放彼此之间没有关系的数据的记录。
第3章 字典和集合
- 散列表则是字典类型性能出众的根本原因
- collections.abc模块中有Mapping和MutableMapping这两个抽象基类,它们的作用是为dict和其他类似的类型定义形式接口
- 标准库里的所有映射类型都是利用dict来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用作这些映射里的键
- 如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现__hash__( )方法。另外可散列对象还要有__eq__( )方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……
- 字典推导(dictcomp)可以从任何以键值对作为元素的可迭代对象中构建出字典。
- 用setdefault处理找不到的键
- defaultdict里的default_factory只会在__getitem__里被调用,在其他的方法里完全不会发挥作用。
- 而更倾向于从UserDict而不是从dict继承的主要原因是,后者有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是UserDict就不会带来这些问题
- 集合的本质是许多唯一对象的聚集。
- 集合中的元素必须是可散列的,set类型本身是不可散列的,但是frozenset可以
- 但是如果是像{1, 2, 3}这样的字面量,Python会利用一个专门的叫作BUILD_SET的字节码来创建集合。
- 由于列表的背后没有散列表来支持in运算符,每次搜索都需要扫描一次完整的列表,导致所需的时间跟据haystack的大小呈线性增长。
- 散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)
- 用元组取代字典就能节省空间的原因有两个:其一是避免了散列表所耗费的空间,其二是无需把记录中字段的名字在每个元素里都存一遍。
- dict的实现是典型的空间换时间:字典类型有着巨大的内存开销,但它们提供了无视数据量大小的快速访问——只要字典能被装在内存里。
- 往字典里添加新键可能会改变已有键的顺序
- 由此可知,不要对字典同时进行迭代和修改。如果想扫描并修改一个字典,最好分成两步来进行:首先对字典迭代,以得出需要添加的内容,把这些内容放在一个新字典里;迭代结束之后再对原有字典进行更新。
- set和frozenset的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)
第4章 文本和字节序列
- Python 3明确区分了人类可读的文本字符串和原始的字节序列。
- 字符的标识,即码位,是0~1114111的数字(十进制),在Unicode标准中以4~6个十六进制数字表示,而且加前缀“U+”。
- 把码位转换成字节序列的过程是编码;把字节序列转换成码位的过程是解码。
- 需要在多台设备中或多种场合下运行的代码,一定不能依赖默认编码。打开文件时始终应该明确传入encoding=参数,因为不同的设备使用的默认编码可能不同,有时隔一天也会发生变化。
第三部分 把函数视作对象
- 在运行时创建能赋值给变量或数据结构中的元素能作为参数传给函数能作为函数的返回结果
- 函数式编程的特点之一是使用高阶函数
- 任何单参数函数都能作为key参数的值。
- all(iterable)
如果iterable的每个元素都是真值,返回True;all([])返回True。
any(iterable)
只要iterable中有元素是真值,就返回True;any([])返回False。
- lambda关键字在Python表达式内创建匿名函数
- 调用类时会运行类的__new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实例返回给调用方。
- 不仅Python函数是真正的对象,任何Python对象都可以表现得像函数。为此,只需实现实例方法__call__。
- operator模块为多个算术运算符提供了对应的函数
- partial的第一个参数是一个可调用对象,后面跟着任意个要绑定的定位参数和关键字参数。
第7章 函数装饰器和闭包
- 函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为
- 闭包还是回调式异步编程和函数式编程风格的基础。
- 装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。
- 装饰器通常把函数替换成另一个函数
- 装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。
- 装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即Python加载模块时)
- 函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。这突出了Python程序员所说的导入时和运行时之间的区别。
- 装饰器通常在一个模块中定义,然后应用到其他模块中的函数上。
- Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。
- 闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
- 闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
- 这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。
- singledispatch机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。
- 把@d1和@d2两个装饰器按顺序应用到f函数上,作用相当于f=d1(d2(f))
第四部分 面向对象惯用法
- 本章的主题是对象与对象名称之间的区别。名称不是对象,而是单独的东西。
- 变量是标注,而不是盒子
- Python变量类似于Java中的引用式变量,因此最好把它们理解为附加在对象上的标注。
- 每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。is运算符比较两个对象的标识;id( )函数返回对象标识的整数表示。
- ==运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识。
- copy模块提供的deepcopy和copy函数能为任意对象做深复制和浅复制。
- 共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。
- 不要使用可变类型作为参数的默认值
- 问题在于,没有指定初始乘客的HauntedBus实例会共享同一个乘客列表。
- 默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。
- 除非这个方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。如果不确定,那就创建副本。这样客户会少些麻烦。
- 弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。
- WeakValueDictionary类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary中删除。因此,WeakValueDictionary经常用于缓存。
- 不是每个Python对象都可以作为弱引用的目标(或称所指对象)。基本的list和dict实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题
- 共享字符串字面量是一种优化措施,称为驻留(interning)
- 对+=或*=所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,会就地修改。
第9章 符合Python风格的对象
- 在Python 3中,__repr__、__str__和__format__都必须返回Unicode字符串(str类型)。只有__bytes__方法应该返回字节序列(bytes类型)。
- 如果要处理数百万个属性不多的实例,通过__slots__类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。
- 继承自超类的__slots__属性没有效果。Python只会使用各个类中定义的__slots__属性。
- Python有个很独特的特性:类属性可用于为实例属性提供默认值
- Python有个很独特的特性:类属性可用于为实例属性提供默认值。
第10章 序列的修改、散列和切片
- 多数时候,如果实现了__getattr__方法,那么也要定义__setattr__方法,以防对象的行为不一致。
- itertools.zip_longest函数的行为有所不同:使用可选的fillvalue(默认值为None)填充缺失的值,因此可以继续产出,直到最长的可迭代对象耗尽。
第11章 接口:从协议到抽象基类
- 这种技术叫猴子补丁:在运行时修改类或模块,而不改动源码。猴子补丁很强大,但是打补丁的代码与要打补丁的程序耦合十分紧密,而且往往要处理隐藏和没有文档的部分。
- 白鹅类型指,只要cls是抽象基类,即cls的元类是abc.ABCMeta,就可以使用isinstance(obj, cls)。
- 虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查它是否符合抽象基类的接口,即便在实例化时也不会检查。为了避免运行时错误,虚拟子类要实现所需的全部方法
第12章 继承的优缺点
- 内置类型(使用C语言编写)不会调用用户定义的类覆盖的特殊方法。
- 原生类型的这种行为违背了面向对象编程的一个基本原则:始终应该从实例(self)所属的类开始搜索方法,即使在超类实现的类中调用也是如此。
- 直接子类化内置类型(如dict、list或str)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自己定义的类应该继承collections模块中的类,例如UserDict、UserList和UserString,这些类做了特殊设计,因此易于扩展。
第五部分 控制流程
- 所有生成器都是迭代器,因为生成器完全实现了迭代器接口。
- Python从可迭代的对象中获取迭代器。
- 标准的迭代器接口有两个方法。
__next__
返回下一个可用的元素,如果没有元素了,抛出StopIteration异常。
__iter__
返回self,以便在应该使用可迭代对象的地方使用迭代器,例如在for循环中。
- 迭代器是这样的对象:实现了无参数的__next__方法,返回序列中的下一个元素;如果没有元素了,那么抛出StopIteration异常。Python中的迭代器还实现了__iter__方法,因此迭代器也可以迭代。
- 可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现__iter__方法,但不能实现__next__方法。
- 只要Python函数的定义体中有yield关键字,该函数就是生成器函数。
- re.finditer函数是re.findall函数的惰性版本,返回的不是列表,而是一个生成器,按需生成re.MatchObject实例。
- 生成器表达式可以理解为列表推导的惰性版本:不会迫切地构建列表,而是返回一个生成器,按需惰性生成元素。
- 生成器表达式是语法糖:完全可以替换成生成器函数,不过有时使用生成器表达式更便利。
- itertools.takewhile函数则不同,它会生成一个使用另一个生成器的生成器,在指定的条件计算结果为False时停止
第15章 上下文管理器和else块
- 在所有情况下,如果异常或者return、break或continue语句导致控制权跳到了复合语句的主块之外,else子句也会被跳过。
- 上下文管理器协议包含__enter__和__exit__两个方法。with语句开始运行时,会在上下文管理器对象上调用__enter__方法。with语句运行结束后,会在上下文管理器对象上调用__exit__方法,以此扮演finally子句的角色。
- 在使用@contextmanager装饰的生成器中,yield语句的作用是把函数的定义体分成两部分:yield语句前面的所有代码在with块开始时(即解释器调用__enter__方法时)执行, yield语句后面的代码在with块结束时(即调用__exit__方法时)执行。
第16章 协程
- yield item这行代码会产出一个值,提供给next(...)的调用方;此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next( )。调用方会从生成器中拉取值。
- 协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。
- 首先要调用next(...)函数,因为生成器还没启动,没在yield语句处暂停,所以一开始无法发送数据。
第六部分 元编程
- 仅当无法使用常规的方式获取属性(即在实例、类或超类中找不到指定的属性),解释器才会调用特殊的__getattr__方法。
第20章 属性描述符
- 描述符是对多个属性运用相同存取逻辑的一种方式。
- 描述符是实现了特定协议的类,这个协议包括__get__、__set__和__delete__方法