--- title: 《流畅的Python》学习笔记 date: 2018-10-09 19:48:53 tags: [python] categories: python --- 《流畅的Python》学习笔记,备忘录形式。 ## 第1章 Python数据模型 ## 第2章 序列构成的数组 ### 一个关于+=的谜题 `+=`对应的方法为`__iadd__`,对应的指令为`INPLACE_ADD`,表示`inplace add`。对应实现了`__iadd__`的对象,解释器会直接调用该方法,否则退化为调用`__add__`,然后再赋值。 ```python >>> t=(1,2,[30,50]) >>> t[2] += [50,60] Traceback (most recent call last): File "", line 1, in TypeError: 'tuple' object does not support item assignment >>> t (1, 2, [30, 50, 50, 60]) ``` 通过上面的代码,我们发现:`t[2] += [50,60]`抛出异常,但是`t`的值仍然被改变了,通过字节码来分析执行过程: ```python >>> 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`](https://docs.python.org/3/library/types.html#types.MappingProxyType),给这个类一个映射,它会返回一个只读的映射视图。 ### 集合字面量 构造空集合,需要写成`set()`的形式,`{}`表示构造空字典。使用字面量构造集合比使用构造函数的形式要快,因为从字面量构造时,Python会利用一个专门的字节码`BUILD_SET`来创建集合。 ```python >>> 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`](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONLEGACYWINDOWSSTDIO)才能让`sys.stdout/stdin/stderr`使用指定编码*。 - Python在二进制数据和字符串之间转换时,内部使用`sys.getdefaultencoding()`获得的编码(在Python3中无法被修改)。 - `sys.getfilesystemencoding()`用于编解码文件名(不是文件内容)。把字符串参数作为文件名传给`open()`函数时就会使用它,如果传入的文件名参数是字节序列,那就不经改动直接传给OS API。可参考[Unicode filenames](https://docs.python.org/3/howto/unicode.html#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提供了一种句法,用于为函数声明中的参数和返回值附加元数据。 ```python 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`的装饰器 ```python @decorate def target(): print('running target()') ``` 上述代码的效果与下述写法一致: ```python def target(): print('running target()') target = decorate(target) ``` 装饰器只是语法糖,有两大特性: - 能把被装饰的函数替换成其他函数。 - 装饰器在加载模块时立即执行。 ### Python何时执行装饰器 函数装饰器在**导入**模块时(被装饰函数定义时)立即执行,而被装饰的函数只在明确**调用**时运行。 ### 变量作用域规则 ```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: 计算移动平均值的类 ```python 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) ``` 计算移动平均值的高阶函数 ```python 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`机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。 ### 参数化装饰器 创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。 ```python 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 ``` ```python 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](https://python3-cookbook.readthedocs.io/zh_CN/latest/chapters/p09_meta_programming.html)*{% endcq %} ## 第8章 对象引用、可变性和垃圾回收 ### 在`==`和`is`之间选择 `==`比较是两个变量的值是否相等,`is`比较两个变量是否同一个对象,即对象标识是否相等。 `is`运算符比`==`速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。 ### 默认做浅复制 ```python 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`模块的[文档](https://docs.python.org/3/library/weakref.html)指出,`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](https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes) - `abc` - `collections.abc` - `numbers` ### 定义并使用一个抽象基类 在Python 3.4及以上版本,可以直接继承自`abc.ABC`,在旧版本中,需要在class语句中使用`metaclass`关键字,把值设为`abc.ABCMeta`。 ```python python3的写法 class Tombola(metaclass=abc.ABCMeta): # python3 pass ``` ```python python2的写法 class Tombola(object): # python2 __metaclass__ = abc.ABCMeta ``` [`@abc.abstractmethod`](https://docs.python.org/3/library/abc.html#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`,这些类做了特殊处理,因此易于扩展。 ### 处理多重继承 1. 把接口继承和实现继承区分开 使用多重继承时,一定要明确一开始为什么创建子类。主要原因可能有: - 继承接口,创建子类型,实现“是什么”关系 - 继承实现,通过重用避免代码重复 2. 使用抽象基类显示表示接口 现代的Python中,如果类的作用是定义接口,应该明确把它定义为抽象基类,创建`abc.ABC`或其他抽象基类的子类 3. 通过混入重用代码 如果一个类的作用是为多个不相关的子类提供方法实现,从而实现方法重用,但不体现“是什么”关系,应该把那个类明确定义为**混入类(mixin class)** 4. 在名称中明确指明混入 因为在Python中没有把类声明为混入的正规方式,所以强烈推荐名称中加入...Mixin后缀 5. 抽象基类可以作为混入,反过来则不成立 抽象基类可以实现具体方法,因此可以作为混入使用。不过,抽象基类会定义类型,而混入做不到。 6. 不要子类化多个具体类 具体类可以没有,或最多只有一个具体超类。也就是说,具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入。 7. 为用户提供聚合类 如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,使用易于理解的方式把它们组合起来。这种类叫做**聚合类(aggregate class)** 8. 优先使用对象组合,而不是类继承 ## 第13章 正确重载运算符 ### 运算符重载基础 - 不能重载内置类型的运算符 - 不能新建运算符,只能重载现有的 - 某些运算符不能重载--`is`、`and`、`or`和`not`(不过位运算符`&|~`可以) ### 重载向量加法运算符+ 实现一元运算符和中缀运算符的特殊方法一定不能修改操作数。使用这些运算符的表达式期待结果是新对象。 `__add__`, `__radd__`, `__iadd__` {% asset_img frame13-1.png %} ### 众多比较运算符 - 正向和反向调用使用的是同一系列方法。 - 对`==`和`!=`来说,如果反向调用失败,Python会比较对象的ID,而不抛出`TypeError`。