22 KiB
| title | date | tags | categories |
|---|---|---|---|
| 《流畅的Python》学习笔记 | 2018-10-09 19:48:53 | [python] | python |
《流畅的Python》学习笔记,备忘录形式。
第1章 Python数据模型
第2章 序列构成的数组
一个关于+=的谜题
+=对应的方法为__iadd__,对应的指令为INPLACE_ADD,表示inplace add。对应实现了__iadd__的对象,解释器会直接调用该方法,否则退化为调用__add__,然后再赋值。
>>> t=(1,2,[30,50])
>>> t[2] += [50,60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 50, 50, 60])
通过上面的代码,我们发现:t[2] += [50,60]抛出异常,但是t的值仍然被改变了,通过字节码来分析执行过程:
>>> t=(1,2,[30,50])
>>> dis.dis('t[2] += [50,60]')
1 0 LOAD_NAME 0 (t)
2 LOAD_CONST 0 (2)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_CONST 1 (50)
10 LOAD_CONST 2 (60)
12 BUILD_LIST 2
14 INPLACE_ADD
16 ROT_THREE
18 STORE_SUBSCR
20 LOAD_CONST 3 (None)
22 RETURN_VALUE
18 STORE_SUBSCR执行失败,因为t是个不可变对象。
三个点:
- 不要把可变对象(列表)放在不可变对象(元组)中;
- 增量赋值不是原子操作;
- 多查看Python的字节码,来分析背后的运行机制。
第3章 字典和集合
子类化UserDict
创造自定义映射类型,优先使用collections.UserDict为基类,而不是以dict为基类。UserDict有个属性叫做data,是dict的实例,这个属性实际上是UserDict最终存储数据的地方。
不可变映射类型
标准库里所有的映射类型都是可变的,如果希望构造一个不可变的映射,可以使用MappingProxyType,给这个类一个映射,它会返回一个只读的映射视图。
集合字面量
构造空集合,需要写成set()的形式,{}表示构造空字典。使用字面量构造集合比使用构造函数的形式要快,因为从字面量构造时,Python会利用一个专门的字节码BUILD_SET来创建集合。
>>> dis.dis('{1}')
1 0 LOAD_CONST 0 (1)
2 BUILD_SET 1
4 RETURN_VALUE
>>> dis.dis('set([1])')
1 0 LOAD_NAME 0 (set)
2 LOAD_CONST 0 (1)
4 BUILD_LIST 1
6 CALL_FUNCTION 1
8 RETURN_VALUE
第4章 文本和字节序列
默认编码值
-
如果打开文件时没有指定
encoding参数,默认值由locale.getpreferredencoding()提供。 -
如果设定了
PYTHONIOENCODING环境变量,sys.stdout/stdin.stderr的编码使用设定的值,否则继承自所在控制台,如果输入/输出重定向到文件,则由locale.getpreferredencoding()定义。在Python3.6以后,Windows平台下,指定
PYTHONIOENCODING的同时还需要指定PYTHONLEGACYWINDOWSSTDIO才能让sys.stdout/stdin/stderr使用指定编码。 -
Python在二进制数据和字符串之间转换时,内部使用
sys.getdefaultencoding()获得的编码(在Python3中无法被修改)。 -
sys.getfilesystemencoding()用于编解码文件名(不是文件内容)。把字符串参数作为文件名传给open()函数时就会使用它,如果传入的文件名参数是字节序列,那就不经改动直接传给OS API。可参考Unicode filenames一文。
第5章 一等函数
高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)。
可调用对象
如果想判断对象能否调用,可以使用内置的callable()函数。Python数据模型文档列出了7中可调用对象。
-
用户定义的函数
使用def语句或lambda表达式创建。
-
内置函数
使用C语言(CPython)实现的函数,如
len或time.strftime。 -
内置方法
使用C语言实现的方法,如
dict.get。 -
方法
在类的定义体中定义的函数。
-
类
调用类时会运行类的
__new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实例返回给调用方。因为Python没有new运算符,所以调用类相当于调用函数。 -
类的实例
如果类定义了
__call__方法,那么它的实例可以作为函数调用。 -
生成器函数
使用
yield关键字的函数或方法。调用生成器函数返回的是生成器对象。
获取关于参数的信息
函数对象有个__defaults__属性,它的值是一个元组,里面保存着定位参数和关键字参数的默认值。仅限关键字参数的默认值在__kwdefaults__属性中。然而,参数的名称在__code__属性中,它的值是一个code对象引用,自身也有很多属性。
使用inspect模块提取函数签名,inspect.Parameter.kind值有以下5中:
-
POSITIONAL_OR_KEYWORD可以通过定位参数和关键字传入的形参(多数Python函数的参数属于此类)。
-
VAR_POSITIONAL定位参数元组。
-
VAR_KEYWORD关键字参数字典。
-
KEYWORD_ONLY仅限关键字参数(Python3新增)。
-
POSITIONAL_ONLY仅限定位参数。目前,Python声明函数的语法不支持,但是有些使用C语言实现且不接受关键字参数的函数(如divmod)支持。
函数注解
Python3提供了一种句法,用于为函数声明中的参数和返回值附加元数据。
def clip(text: str, max_len: 'int > 0'=80) -> str:
"""
在max_len前面或后面的第一个空格处截断文本
"""
return text
函数声明中的各个参数可以在:之后附加注解表达式。如果参数有默认值,注解放在参数名和=之间。如果想注解返回值,在)和函数声明末尾的:之间添加->和一个表达式。那个表达式可以是任何类型。
注解不会做任何处理,只是存储在函数的__annotations__属性中。
Python对注解所做的唯一的事情是,把它们存储在函数的__annotations__属性里,解释器不会对注解做任何处理或验证。注解只是元数据,可供IDE、框架和装饰器等工具使用。可以使用inspect.signature()函数提取注解。
第6章 使用一等函数实现设计模式
案例分析:重构“策略”模式
在Python中,模块也是一等对象,而且标准库提供了几个处理模块的函数。
globals()返回一个字典,表示当前的全局符号表。这个符号表始终针对当前模块(对函数或方法来说,是指定义他们的模块,而不是调用他们的模块)。
inspect.getmembers函数用于获取对象的属性。
第7章 函数装饰器和闭包
装饰器基础知识
装饰器是可调用对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或者可调用对象。
假如有个名为decorate的装饰器
@decorate
def target():
print('running target()')
上述代码的效果与下述写法一致:
def target():
print('running target()')
target = decorate(target)
装饰器只是语法糖,有两大特性:
- 能把被装饰的函数替换成其他函数。
- 装饰器在加载模块时立即执行。
Python何时执行装饰器
函数装饰器在导入模块时(被装饰函数定义时)立即执行,而被装饰的函数只在明确调用时运行。
变量作用域规则
b = 6
def f2(a):
print(a)
print(b)
b = 9
f2(3)
上述代码,执行会报错:
Traceback (most recent call last):
File "", line 1, in
File "", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
Python在编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了。
这不是缺陷,而是设计选择:Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。如果在函数中赋值时想让解释器把b当成全局变量,要使用global声明。
闭包
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
示例 average_oo.py:
计算移动平均值的类
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)
avg = Averager()
avg(10)
avg(11)
计算移动平均值的高阶函数
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(self.series)
return total/len(self.series)
return averager
avg = make_averager()
avg(10)
avg(11)
在averager函数中,series是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量。
{% asset_img frame7-1.png 自由变量 %}
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍然能使用那些绑定。
nonlocal声明
用于在闭包中声明变量作用域。
标准库中的装饰器
functools.lru_cache实现了备忘(memorization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。可用来优化递归调用。因为lru_cache使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数都必须是可散列的。
functools.singledispatch可以使普通的函数变为泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。singledispatch机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。
参数化装饰器
创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。
from inspect import signature
from functools import wraps
def typeassert(*ty_args, **ty_kwargs):
def decorate(func):
# If in optimized mode, disable type checking
if not __debug__:
return func
# Map function argument names to supplied types
sig = signature(func)
bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments
@wraps(func)
def wrapper(*args, **kwargs):
bound_values = sig.bind(*args, **kwargs)
# Enforce type assertions across supplied arguments
for name, value in bound_values.arguments.items():
if name in bound_types:
if not isinstance(value, bound_types[name]):
raise TypeError(
'Argument {} must be {}'.format(name, bound_types[name])
)
return func(*args, **kwargs)
return wrapper
return decorate
import types
from functools import wraps
class Profiled:
def __init__(self, func):
wraps(func)(self)
self.ncalls = 0
def __call__(self, *args, **kwargs):
self.ncalls += 1
return self.__wrapped__(*args, **kwargs)
def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance)
{% cq %} 示例来自python3-cookbook{% endcq %}
第8章 对象引用、可变性和垃圾回收
在==和is之间选择
==比较是两个变量的值是否相等,is比较两个变量是否同一个对象,即对象标识是否相等。
is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。
默认做浅复制
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) # l2是l1的浅复制副本
l1.append(100) # 把100追加到l1中,对l2没有影响
l1[1].remove(55) # 对l2有影响,因为l2[1]绑定的列表与l1[1]是同一个
print('l1:', l1) # l1: [3, [66, 44], (7, 8, 9), 100]
print('l2:', l2) # l2: [3, [66, 44], (7, 8, 9)]
l2[1] += [33, 22] # 对可变对象来说,如l2[1]引用的列表,+=运算符就地修改列表。这次修改在l1[1]中也有提现
l2[2] += (10, 11) # 对元组来说,+=运算符创建一个新元组,然后新绑定给变量l2[2]。现在l2和l1最后位置上的元组不是同一个对象。
print('l1:', l1) # l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
print('l2:', l2) # l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]
函数的参数作为引用时
函数可选参数的默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。
del和垃圾回收
CPython主要使用引用计数进行内存管理,CPython2.0增加了分代垃圾回收算法,用于检测循环引用的问题。
del不会删除对象,但是执行del后,可能会导致对象不可获取,从而被GC删除。
弱引用
weakref模块的文档指出,weakref.ref类其实是底层接口,供高级用途使用,多数程序最好使用weakref集合和finalize。也就是说,应该使用WeakKeyDictonary、WeakValueDictionary、WeakSet和finalize,不要自己动手创建并处理weakref.ref实例。
第9章 符合Python风格的对象
Python的私有属性和“受保护的”属性
以两个前导下划线开头,尾部没有或最多有一个下划线的实例属性,Python会把属性名存入实例的__dict__属性中,而且会在前面加上一个下划线和类名,这个语言特性叫做名称改写(name mangling)。
以单个下划线开头的实例属性,表示“受保护的”属性,但是仍然可以直接访问到。
使用__slots__类属性节省空间
默认情况下,Python在各个实例中名为__dict__的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过__slots__类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。
继承自超类的__slots__属性没有效果。Python只会使用各个类中定义的__slots__属性。
定义__slots__的方式是,创建一个类属性,使用__slots__这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。
不要在__slots__中添加__dict__,这样做违背了设计初衷。
为了让对象支持弱引用,必须有__weakref__属性。用户定义的类默认就有__weakref__属性,可是,如果类中定义了__slots__属性,而且想把实例作为弱引用的目标,那么要手动把__weakref__添加到__slots__中。
几个注意点:
- 每个子类都要定义
__slots__属性,因为解释器会忽略继承的__slots__属性。 - 实例只能拥有
__slots__中列出的属性,除非把__dict__加入__slots__中(这样做就失去了节省内存的功效)。 - 如果不把
__weakref__加入__slots__,实例就不能作为弱引用的目标。
第10章 序列的修改、散列和切片
协议和鸭子类型
Python的序列协议只需要__len__和__getitem__两个方法。任何类,只要使用标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。人们称其为鸭子类型(duck typing)。
切片原理
slice.indices(len) -> (start, stop, stride)
all, functools.reduce, zip, reprlib
第11章 接口:从协议到抽象基类
协议是接口,但不是正式的(只由文档和约定定义),因此协议不能像正式接口那样施加限制。
标准库中的抽象基类
Collections Abstract Base Classes
abccollections.abcnumbers
定义并使用一个抽象基类
在Python 3.4及以上版本,可以直接继承自abc.ABC,在旧版本中,需要在class语句中使用metaclass关键字,把值设为abc.ABCMeta。
class Tombola(metaclass=abc.ABCMeta): # python3
pass
class Tombola(object): # python2
__metaclass__ = abc.ABCMeta
@abc.abstractmethod装饰器可以堆叠,但是与其他装饰器合用时,该装饰器必须位于最里层。
白鹅类型的一个基本特性:即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。注册虚拟子类的方式是在抽象基类上调用register方法。这么做之后,注册的类会变成抽象基类的虚拟子类,而且issubclass和isinstance等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性。Python不会对注册类的实现做检查,如果有接口未实现,只能通过运行时错误发现。Python3.3之后的版本,register方法可以当做装饰器使用。
类的继承关系在一个特殊的类属性中指定__mro__,即方法解析顺序(Method Resolution Order)。这个属性的作用很简单,按顺序列出类及其超类,Python会按照这个顺序搜索方法。通过register方法注册的类,不在__mro__列表中。
鹅的行为有可能像鸭子
__subclasshook__在白鹅类型中添加了一些鸭子类型的踪迹。我们可以使用抽象基类定义正式接口,可以始终使用isinstance检查,也可以完全使用不相关的类,只要实现特定的方法即可。只有提供__subclasshook__方法的抽象基类才能这么做。
第12章 继承的优缺点
子类化内置类型很麻烦
直接子类化内置类型(如dict, list或str)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自定义的类应该继承collections模块中的类,例如UserDict、UserList和UserString,这些类做了特殊处理,因此易于扩展。
处理多重继承
- 把接口继承和实现继承区分开
使用多重继承时,一定要明确一开始为什么创建子类。主要原因可能有:- 继承接口,创建子类型,实现“是什么”关系
- 继承实现,通过重用避免代码重复
- 使用抽象基类显示表示接口
现代的Python中,如果类的作用是定义接口,应该明确把它定义为抽象基类,创建abc.ABC或其他抽象基类的子类 - 通过混入重用代码
如果一个类的作用是为多个不相关的子类提供方法实现,从而实现方法重用,但不体现“是什么”关系,应该把那个类明确定义为混入类(mixin class) - 在名称中明确指明混入
因为在Python中没有把类声明为混入的正规方式,所以强烈推荐名称中加入...Mixin后缀 - 抽象基类可以作为混入,反过来则不成立
抽象基类可以实现具体方法,因此可以作为混入使用。不过,抽象基类会定义类型,而混入做不到。 - 不要子类化多个具体类
具体类可以没有,或最多只有一个具体超类。也就是说,具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入。 - 为用户提供聚合类
如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,使用易于理解的方式把它们组合起来。这种类叫做聚合类(aggregate class) - 优先使用对象组合,而不是类继承
第13章 正确重载运算符
运算符重载基础
- 不能重载内置类型的运算符
- 不能新建运算符,只能重载现有的
- 某些运算符不能重载--
is、and、or和not(不过位运算符&|~可以)
重载向量加法运算符+
实现一元运算符和中缀运算符的特殊方法一定不能修改操作数。使用这些运算符的表达式期待结果是新对象。
__add__, __radd__, __iadd__
{% asset_img frame13-1.png %}
众多比较运算符
- 正向和反向调用使用的是同一系列方法。
- 对
==和!=来说,如果反向调用失败,Python会比较对象的ID,而不抛出TypeError。