《流畅的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)实现的函数,如lentime.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。也就是说,应该使用WeakKeyDictonaryWeakValueDictionaryWeakSetfinalize,不要自己动手创建并处理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
- abc - collections.abc
- numbers

定义并使用一个抽象基类

在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方法。这么做之后,注册的类会变成抽象基类的虚拟子类,而且issubclassisinstance等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性。Python不会对注册类的实现做检查,如果有接口未实现,只能通过运行时错误发现。Python3.3之后的版本,register方法可以当做装饰器使用。
类的继承关系在一个特殊的类属性中指定__mro__,即方法解析顺序(Method Resolution Order)。这个属性的作用很简单,按顺序列出类及其超类,Python会按照这个顺序搜索方法。通过register方法注册的类,不在__mro__列表中。

鹅的行为有可能像鸭子

__subclasshook__在白鹅类型中添加了一些鸭子类型的踪迹。我们可以使用抽象基类定义正式接口,可以始终使用isinstance检查,也可以完全使用不相关的类,只要实现特定的方法即可。只有提供__subclasshook__方法的抽象基类才能这么做。

第12章 继承的优缺点

子类化内置类型很麻烦

直接子类化内置类型(如dict, liststr)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自定义的类应该继承collections模块中的类,例如UserDictUserListUserString,这些类做了特殊处理,因此易于扩展。

处理多重继承

  1. 把接口继承和实现继承区分开
    使用多重继承时,一定要明确一开始为什么创建子类。主要原因可能有:
    • 继承接口,创建子类型,实现“是什么”关系
    • 继承实现,通过重用避免代码重复
  2. 使用抽象基类显示表示接口
    现代的Python中,如果类的作用是定义接口,应该明确把它定义为抽象基类,创建abc.ABC或其他抽象基类的子类
  3. 通过混入重用代码
    如果一个类的作用是为多个不相关的子类提供方法实现,从而实现方法重用,但不体现“是什么”关系,应该把那个类明确定义为混入类(mixin class)
  4. 在名称中明确指明混入
    因为在Python中没有把类声明为混入的正规方式,所以强烈推荐名称中加入…Mixin后缀
  5. 抽象基类可以作为混入,反过来则不成立
    抽象基类可以实现具体方法,因此可以作为混入使用。不过,抽象基类会定义类型,而混入做不到。
  6. 不要子类化多个具体类
    具体类可以没有,或最多只有一个具体超类。也就是说,具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入。
  7. 为用户提供聚合类
    如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,使用易于理解的方式把它们组合起来。这种类叫做聚合类(aggregate class)
  8. 优先使用对象组合,而不是类继承

第13章 正确重载运算符

运算符重载基础

  • 不能重载内置类型的运算符
  • 不能新建运算符,只能重载现有的
  • 某些运算符不能重载–isandornot(不过位运算符&|~可以)

重载向量加法运算符+

实现一元运算符和中缀运算符的特殊方法一定不能修改操作数。使用这些运算符的表达式期待结果是新对象。

__add__, __radd__, __iadd__

流程图

众多比较运算符

  • 正向和反向调用使用的是同一系列方法。
  • ==!=来说,如果反向调用失败,Python会比较对象的ID,而不抛出TypeError

python

6929 Words

2018-10-10 03:48