第一章 Python数据类型
- Python 最好的品质之一是一致性。
- sorted(cards, key=spades_high), spades_high 是一个自定义的函数, 传入的参数是card, 输出值是用来排序的依据。 lambda x: x[key]
- str 和 repr 的区别, 前者显示更友好(print, str), 后者更加无歧义(终端, 调试)。比如, 后者的显示结果能区分出数字和字符串类型, 1和‘1’
- len方法不是普通方法,如果x是内置类型, len(x) 会直接从一个C结构体里读取对象的长度,完全不会调用任何方法。 O(1)
第二章 序列
- 序列可以分为可变序列和不可变序列, 又可以分为容器序列和扁平序列
-
列表推导在python2里有变量泄露问题, python3 下没有
1 2 3 4
>>>x = 1 >>>newlist = [x for x in range(5)] >>>x 4
- 具名元组, namedtuple, 创建时需要两个元素, 一个是类名, 一个是类各个字段的名字。 City = namedtuple(‘City’, ‘name country population’) 或: City = namedtuple(‘City’, ‘name’, ‘country’, ‘population’)
- a += b, 如果a内部实现了__iadd__ 方法, a就会就地改动, 否则会新建一个对象, 等于 a = a + b, 可变序列一般都实现了__iadd__方法。
- 不要把可变对象放在元组里面, 最好不要放在容器类型序列里面。里面对与可变对象存储的是对象的引用。
- list.sort 方法会就地排序列表, 返回None, sorted(list) 会新建一个列表作为返回值。
-
一个 += 的谜题:
1 2
>>> t = (1, 2, [30, 40]) >>> t[2] += [50, 60]
结果会抛出异常, 但t的值会变为【30, 40, 50, 60]。 a += b 不是原子操作。1. 将a的值存入堆栈TOS。2. TOS += b。 3. a = TOS。 这里第二步因为[]30, 40] 是可变的列表, 可以完成操作, 但第三步给不可变的元组赋值就会报错。
第三章 字典和集合
- 散列表是字典类型性能出众的根本原因, 只有可散列的数据类型才能用作字典的键。
- 一个可散列的对象必须满足以下三个条件: * 支持 hash( ) 函数, 并且通过__hash__( ) 方法所得到的散列值是不变的。 * 支持通过__eq__( ) 方法来检测相等性 * 若 a == b为真, 则hash(a) 必须等于 hash(b)
- 原子不可变数据类型(str, bytes 和数值类型) 都是可散列类型, 元组只有所包含的所有元素都是可散列类型的情况下, 它才是可散列的。
-
my_dict.setdefault(key, []).append(1) 等价于:
1 2 3
if key not in mydict: my_dict[key] = [] my_dict[key].append(1)
-
defaultdict
1 2 3
from collections import defaultdict mydict = defaultdict(list) mydict[key].append(1) # 不用额外查询key是否已存在
- {} 是空字典, 不是空集合, 空集合应该用set()来创建
- 字典的散列表其实是一个稀疏数组, python会设法保证大概还有三分之一的表元是空的, 快到阈值时, 原有的散列表会被复制到一个更大的空间里面。 而键的顺序因此可能改变。
- 不要一边迭代字典, 一边对字典修改, 这个循环可能会跳过一些键。
- dict 的实现是典型的空间换时间, 当有大量数据时可以用具名元组来代替,不要用字典组成的列表。
- 散列表的算法——查询逻辑、插入逻辑、散列冲突。
- 优化往往是可维护的对立面。
第四章 文本和字节序列
- 人类使用文本, 计算机使用字节序列
- 把字节序列转换成人类可读的文本字符串就是解码, 把字符串变成用于存储或传输的字节序列就是编码。 码位是字符的标识。
- Python 3 默认使用UTF-8编码, Python 2 则默认使用 ASCII.
- Unicode 三明治: 要尽早把输入的字节序列解码成字符串。 中间处理过程中一定不能编码或解码。 输出时, 要尽量晚地把字符串编码成字节序列。
- 写入文件时, 如果没有指定编码参数, 会使用区域设置中的默认编码。
- os 模块中的所有函数、文件名或路径名参数既能使用字符串, 也能使用字节序列。
- Python 3 创建 str 对象时会根据字符串的字符选择最经济的内存布局: 如果字符都在 latin1 字符集中, 那就使用1个字节存储每个码位; 否则, 根据字符串中的具体字符, 选择2个或4个字节存储每个码位。
- Python 3 对 int 类型的处理方式: 如果一个整数在一个机器字中放得下, 那就存储在一个机器字中;否则解释器切换成边长表述, 类似于 python 2 中的 long 类型。
第五章 把函数视作对象
- 在 Python 中, 函数是一等对象。一等对象为满足以下条件的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
- 接受函数为参数, 或者把函数作为结果返回的函数是高阶函数。
- 列表推导或生成器表达式具有map和filter两个函数的功能。Python 3 中, map 和 filter 返回生成器, 在 Python 2 中, 这两个函数返回列表。
- lambda 关键字在 Python 表达式内创建匿名函数, 但Python 限制了lambda的定义提只能使用纯表达式, 即定义体中不能赋值, 也不能使用while 和 try 等 Python 语句。
- Python 有七种可调用对象:
- 用户定义的函数
- 内置函数(len)
- 内置方法(dict.get)
- 方法(类中定义的函数)
- 类
- 类的实例(如果类定义了__call__ 方法, 那么它的实例可以作为函数调用)
- 生成器函数(yield)
- 实现__call__ 方法的类是创建函数类对象的简便方式
- 定义函数时若想指定仅限关键字参数, 要把它们放在前面有
*
的后面。 如 def f(a,*
, b) - operator 中的itemgetter 和 attrgetter. itermgetter(1) 和 lambda fields: fields[1]的用法一样,attrgetter(‘a’) 和 lambda dict: dict.get(‘a’) 一样
- 列表推导是从Haskell 借鉴来的, 而lambda、map、filter、reduce 则是借鉴自Lisp
第六章 使用一等函数实现设计模式
- 本章主要用函数的方法实现了经典的“策略” 模式。
- globals() 返回一个字典, 表示当前的全局符号表。 这个符号表始终针对当前模块。
- inspect.getmembers(promotions, inspect.isfunction()), inspect.getmembers 函数用户获取对象的属性, 第二个参数是可选的判断条件。 isfunction() 表示只获取模块中的函数。
- 每个python 可调用对象都实现了单方法接口, 这个方法就是__call__。
- 两个设计原则: 对接口编程, 而不是对实现编程 和 优先使用对象组合, 而不是类继承。
- 推荐两本python 设计模式的书: Learning Python Design Patterns(Gennadiy Zlobin), 《Python 高级编程》(Tarek Ziade), 以及最经典的《设计模式: 可复用面向对象软件的基础》(Gamma)
第七章 函数装饰器和闭包
- 函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。
- 除了在装饰器中有用处之外,闭包还是回调式异步编程和函数式编程风格的基础。
-
装饰器是可调用的对象, 其参数是另一个函数(被装饰的函数)。 例如以下两个写法是等价的:
1 2 3
@decorate def target(): print('running target')
和
1 2 3 4
def target(): print('running target') target = decorate(target)
装饰器只是语法糖, 它可以像常规的可调用对象那样调用,其参数是另一个函数。
- 函数装饰器在导入模块时立即执行, 而被装饰的函数只在明确的调用时运行。
-
变量作用域规则:Python 不要求声明变量, 但是假定在函数体中赋值的变量是局部变量。
1 2 3 4 5
b = 6 def f(a): print(a) print(b) b = 9
调用f(3)会在print(b) 处抛出异常, 应该声明b为全局变量:
1 2 3 4 5 6
b = 6 def f(a): global b print(a) print(b) b = 9
- 闭包是指延伸了作用域的函数, 其中包含函在数定义体中引用, 但是不在定义体中定义的非全局变量(自由变量)。闭包函数会保留在定义函数时存在的自由变量的绑定。
-
python 3 中引入的nolocal, 作用是把变量标记为自由变量, 即使在函数中为变量赋值了, 也会变为自由变量。
1 2 3 4 5 6 7 8 9 10
def make_averager(): count = 0 total = 0 def averager(new_value): nonlocal count, total count += 1 total += new_value return total / count return averager
python2 中没有nonlocal, 处理方式是把内部函数需要修改的变量存储为可变对象的元素或属性。
- 标准库中一个常见的装饰器是functools.wraps, 它的作用是协助构建行为良好的装饰器(函数签名)。
- 因为funtools.lru_cache使用字典存储结果, 而且键根据调用时传入的定位参数和关键字参数创建, 所以被lru_cache装饰的函数, 它的所有参数都必须是可散列的(列表等可变对象不行)。
- 怎么让装饰器接受其他参数呢? 创建一个装饰器工厂函数, 把参数传给它, 返回一个装饰器, 然后再把它应用到要装饰的函数上。
第8章 对象引用、可变性和垃圾回收
- 变量是标注, 而不是盒子(对象)。
- Python 中的赋值语句, 应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。贴的多个标注, 就是别名。
- == 运算符比较两个对象的值(对象中保存的数据), 而is比较对象的标识(ID)。
- 元组与多数 Python 集合(列表、字典、集等)一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指tuple数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。
- str、bytes、和array.array等单一类型序列是扁平的, 它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)
- 构造方法list()或[:]做的是浅复制。如果所有元素都是不可变的,这样没有问题。如果有可变的元素, 则可能在涉及到修改时意外修改了另一个参数。
- 对于列表, += 运算符就是就地修改列表, 但对于元组来说, += 运算符创建一个新元组, 然后重新绑定给左边的变量。
- Python 函数中参数的传递模式是共享传参。共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说, 函数内部的形参是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的可变对象
- 函数的默认值在定义函数时计算(通常在加载模块时)。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。可变默认值导致的这个问题说明了为什么通常使用None作为接收可变值的参数的默认值。
- del 不会删除对象,但是执行del操作后可能会导致对象不可获取,从而被删除。
- 弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保持缓存对象。
- 不可变集合不变的是所含对象的标识。元组本身不可变,但如果里面保存着可变对象,那么元组的值可能会变。
第9章 符合 Python 风格的对象
- 鸭子类型: 一个对象有效的语义, 不是由继承自特定的类或实现特定的接口,而是由“当前方法和属性的集合”决定。
- repr( ) —— 以便于开发者理解的方式返回对象的字符串表示形式。 str( ) —— 以便于用户理解的方式返回对象的字符串表示形式。
- 类方法classmethod: 定义操作类, 而不是操作实例的方法, 因此类方法的第一个参数是类本身, 而不是实例。 静态方法staticmethod 就是普通的函数, 只是碰巧在类的定义体中,而不是在模块层定义。
- 要想创建可散列的类型,只需要正确的实现
__hash__
和__eq__
方法即可。 但是, 实例的散列值觉不应该变化。 - 如果要处理数百万个属性不多的实例, 通过
__slots__
类属性, 能节省大量内存, 方法是让解释器在元组中存储实例的属性, 而不用字典。 在类中定义__slots__
属性之后, 实例不能再有__slots__
中所列名称之外的其他属性。 - 每个子类都要定义
__slots__
属性, 因为解释器会忽略继承的__slots__
属性 - 如果不把
__weakref__
加入__slots__
, 实例就不能作为弱引用的目标。 - 粗心的优化甚至比提早优化还糟糕。
- 类属性是公开的, 因此会被子类继承, 于是经常会创建一个子类, 只用于定制类的数据属性。Django基于类的视图就大量使用了这个技术。
第10章 序列的修改、散列和切片
- 在面向对象编程中, 协议是非正式的接口,只在文档中定义,在代码中不定义。例如, Python 的序列协议只需要
__len__
和__getitem__
两个方法。 -
S.indices(len) ——> (start, stop, stride) . 给定长度为len的序列, 计算S表示的扩展切片的起始(start) 和结尾(stop)索引, 以及步幅(stride)。例如:
1
slice(None, 10, 2).indices(5) ——> (0, 5, 2)
- 仅当对象没有指定名称的属性时, Python 才会调用
__getattr__
方法, 这是一种后备机制。 - 多数时候, 如果实现了
__getattr__
方法, 那么也要定义__setattr__
方法, 以防对象的行为不一致。 - 在 Python3 中, map函数和filter函数都是惰性的, 会返回一个生成器。
第11章 接口: 从协议到抽象基类
- 接口: 类实现或继承的公开属性。接口是实现特定角色的方法集合。协议与继承没有关系。一个类可能会实现多个接口, 从而让实例扮演多个角色。协议是接口, 但不是正式的(只由文档和约定定义), 因此协议不能像正式接口那样施加限制。
- 鉴于序列协议的重要性, 如果没有
__iter__
和__contains__
方法, Python 会调用__getitem__
方法, 设法让迭代和in运算符可用。 - 猴子补丁: 在运行时修改类或模块, 而不改动源码。
- 在一连串 if/elif/elif 中使用 isinstance 做检查, 然后根据对象的类型执行不同的操作, 通常是不好的做法;此时应该使用多态, 即采用一定的方式定义类,让解释器把调用分派给正确的方法,而不使用 if/elif/elif 块硬编码分派逻辑。
- numbers 包定义的是“数字塔”, 从上到下依次是 Number、Complex、Real、Rational、Integral。 因此, 如果想检查一个数是不是整数,可以使用 isinstance(x, numbers.Integral), 这样的代码就能接受 int、bool(int的子类)。与之类似, 如果一个值可能是浮点数类型,可以使用 isinstance(x, numbers.Real)检查。这样代码就能接受 bool、int、float、fraction.Fraction等。
- 若想检查对象是否能调用, 可以使用内置的 callable()函数; 但是没有类似的hashable()函数, 因此测试对象是否可散列, 最好使用 isinstance(my_obj, Hashable)
- 与其他方法描述符一起使用时, abstractmethod() 应该放在最里层。
- 类的继承关系在一个特殊的类属性中指定——
__mro__
, 即方法解析顺序(Method Resolution Order)。 - 如果一门语言很少隐式转换类型,说明它是强类型语言;如果经常这么做,说明它是弱类型语言。java、C++和 Python 是强类型语言。PHP、JavaScript 和 Perl 是 弱类型语言。 在编译时检查类型的语言是静态类型语言,在运行时检查类型的语言是动态类型语言。Python 是动态强类型语言。
第12章 继承的优缺点
- 内置类型(使用C语言编写)不会调用用户定义的类覆盖的特殊方法。不要子类化内置类型, 用户自己定义的类应该继承collections模块, 如不要子类化dict, 而是子类化 collections.UserDict。
- 处理多重继承时的注意事项:
- 把接口继承和实现继承区分开
- 使用抽象基类显示表示接口
- 通过混入重用代码
- 在名称中明确指明混入
- 不要子类化多个具体类
- 优先使用对象组合, 而不是类继承。
第13章 正确重载运算符
- 对于表达式 a + b 来说, 解释器会执行以下几步操作:
- 如果 a 有
__add__
方法, 而且返回值不是 NotImplemented, 调用 a.add(b), 然后返回结果 - 如果 a 没有
__add__
方法, 或者调用__add__
方法返回 Notimplemented, 检查 b 有没有__radd__
方法, 如果有, 而且没有返回 NotImplemented, 调用 b.radd(a), 然后返回结果 - 如果 b 没有
__radd__
方法, 或者调用__radd__
方法返回 NotImplemented, 抛出 TypeError, 并在错误信息中指明操作数类型不支持
- 如果 a 有
- 正向的
__gt__
方法调用的是反向的__lt__
方法,并把参数对调。 - x==y 成立不代表 x!=y 不成立。如果定义
__eq__()
方法, 也要定义__ne__()
方法, 这样运算符的行为才能符合预期。 - Python 是门高级语言, 易于使用, 支持运算符重载可能就是它这些年在科学计算领域得到广泛使用的主要原因。
第14章 可迭代的对象、迭代器和生成器
- 迭代器用于从集合中取出元素;而生成器用于”凭空“ 生成元素。 所有生成器都是迭代器,因为生成器完全实现了迭代器接口。
- 解释器需要迭代对象 x 时, 会自动调用 iter(x)。内置的 iter 函数有以下作用:
- (1) 检查对象是否实现了
__iter__
方法, 如果实现了就调用它, 获取一个迭代器。 - (2) 如果没有实现
__iter__
方法, 但是实现了__getitem__
方法, Python 会创建一个迭代器, 尝试按顺序(从索引0开始)获取元素。 - (3) 如果尝试失败, Python 抛出 TypeError 异常。
- (1) 检查对象是否实现了
- 检查对象x能否迭代,最准确的方法是: 调用 iter(x) 函数, 如果不可迭代, 再处理 TypeError 异常。 这比使用 isinstance(x, abc.Itreable) 更准确, 因为 iter(x) 函数会考虑到遗留的
__getitem__
方法, 而 abc.Iterable 类则不考虑。检查对象 x 是否为迭代器的最好方法是调用 isinstance(x, abc.Iterator) - 可迭代的对象: 使用iter 内置函数可以获取迭代器的对象。
- 标准的迭代器接口有两个方法:
__next__
: 返回下一个可用的元素, 如果没有元素了, 抛出StopIteration 异常。__iter__
: 返回self, 以便在应该使用可迭代对象的地方使用迭代器, 例如在for循环中。 - 迭代器是这样的对象: 实现了无参数的
__next__
方法, 返回序列中的下一个元素; 如果没有元素了, 那么抛出 StopIteration 异常。 Python 中的迭代器还实现了__iter__
方法, 因此迭代器也可以迭代。 - 构建可迭代的对象和迭代器时经常出现错误, 原因是混淆了二者。 要知道, 可迭代的对象有个
__iter__
方法, 每次都实例化一个新的迭代器;而迭代器要实现__next__
方法, 返回单个元素, 此外还要实现__iter__
方法, 返回迭代器本身。因此, 迭代器可以迭代,但是可迭代的对象不是迭代器。 - 为了支持多种遍历, 必须能从同一个可迭代的实例中获取多个独立的迭代器,而且各个迭代器要能维护自身的内部状态。 因此, 可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现
__iter__
方法, 但不能实现__next__
方法。 另一方面, 迭代器应该一直可以迭代。迭代器的__iter__
方法应该返回自身。 - 只要 Python 函数的定义体中有 yield 关键字, 该函数就是生成器函数。生成器函数是生成器工厂。调用生成器函数返回生成器, 生成器产出或生成值。
- 迭代时, for 机制的作用与g = iter(gen_AB()) 一样, 用于获取生成器对象,然后每次迭代时调用 next(g)。
- 惰性实现是指尽可能延后生成值。这样做能节省内存, 而且还可以避免做无用的处理。re.finditer 函数是 re.findall 函数的惰性版本。
- 除了代替循环之外, yield from 还会创建通道, 把内层生成器直接与外层生成器的客户端联系起来。
- 接受一个可迭代的对象, 然后返回单个结果的函数叫做归约函数。如 sum(), all(), min()…….
- iter 函数还有一个鲜为人知的用法: 传入两个参数, 第二个值是哨符, 这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常, 而不产出哨符。
- 使用生成器处理数据库时,我们把记录看成数据流,这样消耗的内存量最低。
- .send() 方法致使生成器前进到下一个yield 语句。
第15章 上下文管理器和else块
- with 语句会设置一个临时的上下文, 交给上下文管理器对象控制, 并且负责清理上下文。
- else 子句的行为如下:
- for: 仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止)才运行 else 块。
- while: 仅当 while 循环因为条件为假值而退出时(即 while 循环没有被 break 语句中止)才运行 else 块。
- try: 仅当 try 块中没有异常抛出时才运行 else 块。
- EAFP: 取得原谅比获得许可容易(easier to ask for forgiveness than permission)
- LBYL: 三思而后行(look before you leap)
- 与函数和模块不同, with 块没有定义新的作用域。
- contextlib.contextmanager 装饰器会把函数包装成实现
__enter__
和__exit__
方法的类。 在使用 @contextmanager 装饰的生成器中, yield 语句的作用是把函数的定义体分成两部分: yield 语句前面的所有代码在 with 块开始时(即解释器调用__enter__
方法时)执行, yield 语句后面的代码在 with 块结束时(即调用__exit__
方法时)执行。 - 使用 @contextmanager 装饰器时, 要把 yield 语句放在 try/finally 语句中。
- with 语句是子程序的补充。
第16章 协程
- 协程与生成器类似,都是定义体中包含 yield 关键字的函数。在协程中, yield 通常出现在表达式的右边(例如, data = yield)
- 协程可以把控制器让步给中心调度程序,从而激活其他的协程。
- 最先调用 next(my_coro) 函数这一步通常称为 “预激”协程。使用 yield from 句法调用协程时, 会自动预激。
- 对于 b = yield a 这行代码来说,等到客户端代码再激活协程时才会设定 b 的值。
- 在 Python 3.3 之前, 如果生成器返回值,解释器会报语法错误。 在函数外部使用 yield from 也会导致句法错误。
- 在生成器 gen 中使用使用 yield from subgen() 时, subgen 会获得控制权,把产出的值传给 gen 的调用方,即调用方可以直接控制 subgen。与此同时, gen 会阻塞,等待subgen 终止。
- yield from 的主要功能是打开双向通道, 把最外层的调用方与最内层的子生成器连接起来, 这样二者可以直接发送和产出值,还可以直接传入异常。:
- 委派生成器: 包含 yield from
表达式的生成器函数 - 子生成器: 从 yield from 表达式中
部分获取的生成器 - 调用方: 调用委派生成器的客户端代码
- 委派生成器: 包含 yield from
- 协程能自然地表述很多算法,例如仿真、游戏、异步 I/O, 以及其他事件驱动型编程形式或协作式多任务。
- 在控制台中, _ 变量绑定的是前一个结果。
- 在 asyncio 库中, 协程通常使用 @asyncio.coroutine 装饰器装饰, 而且始终使用 yield from 结构驱动, 而不通过直接在协程上调用 .send(…) 方法驱动。
- 事件驱动型框架: 在单个线程中使用一个主循环驱动协程执行并发活动。这是一种协作式多任务, 协程显示自主地把控制权让步给中央调度程序。而多线程实现的是抢占式多任务。
第17章 使用期物处理并发
- 期物(future) 是指一种对象, 表示异步执行的操作。 期物封装待完成的操作,可以放入队列,完成的状态可以查询,得到结果(或抛出异常)后可以获取结果(或异常)
- concurrent.futures 模块的主要特色是 ThreadPoolExecutor 和 ProcessPoolExecutor 类, 这两个类实现的接口能分别在不同的线程或进程中执行可调用的对象。
- 对 concurrency.futures.Future 实例来说, 调用 f.result() 方法会阻塞调用方所在的线程,直到有结果可返回。
- executor.submit 方法排定可调用对象的执行时间,然后返回一个期物,表示这个待执行的操作。as_completed 函数在期物运行结束后产出期物。
- CPython 解释器本身就不是线程安全的, 因此有全局解释器锁(GIL),一次只允许使用一个线程执行 Python 字节码。 因此, 一个 Python 进程通常不能同时使用多个 CPU 核心。标准库中每个使用C语言编写的 I/O 函数都会释放 GIL, 一个 Python 线程等待网络响应时, 阻塞型 I/O 函数会释放 GIL, 再运行一个线程。
- 在 CPU 密集型作业中使用concurrent.futures.ProcessPoolExecutor 可以轻松绕开 GIL。
- futures.as_completed 函数返回一个迭代器, 在期物运行结束后产出期物。
- 如果futures.ThreadPoolExecutor 类对某个作业来说不够灵活, 可以考虑使用 threading 模块中的组件。对CPU密集型工作来说, 简单的可以使用 future.ProcessPoolExecutor 类。 不过 multiprocessing 模块还能解决协作进程遇到的最大挑战: 在进程之间传递数据。
- GIL 简化了 CPython 解释器和 C 语言扩展的实现。得益于 GIL, Python 有很多 C 语言的扩展。
第18章 使用 asyncio 包处理并发
- 科学界有两个重要过错:使用不同的词表示相同的事物,以及使用同一个词表示不同的事物。
- 并发是指一次处理多件事, 并行是指一次做多件事。真正的并行需要多个核心。并发不是并行(并发更好)。
- asyncio 使用事件循环驱动的协程实现并发。
- 协程默认会做好全方位保护,以防止中断。对协程来说, 无需保留锁以在多个协程之间同步操作, 协程本身就会同步,因为在任意时刻只有一个协程运行。
- 获取 asyncio.Future 对象的结果通常使用 yield from, 从中产出结果。
- 协程的一大优势: 协程是可以暂停和恢复的函数。
- asyncio 包只直接支持 TCP 和 UDP。 如果想使用HTTP 可以使用 aiohttp 包。
- yield from foo 句法能防止阻塞, 是因为当前协程(即包含 yield from 代码的委派生成器)暂停后,控制权回到事件循环手中,再去驱动其他协程;foo 期物或协程运行完毕后,把结果返回给暂停的协程,将其恢复。这种方式相当于架起了管道, 让 asyncio 事件循环驱动执行低层异步 I/O 操作的库函数。
- 使用yield from 链接的多个协程最终必须由不是协程的调用方驱动,调用方显式或隐式(如 for 循环)在最外层委派生成器上调用next()函数或 send() 方法。 链条中最内层的子生成器必须是简单的生成器(只使用yield)或可迭代的对象。
- 不同存储介质中读取数据的延迟情况:
1
2
3
4
5
6
7
存储介质 | CPU 周期 | 换算成人类事件
:----------- |:------------- |:-------------
L1 缓存 | 3 | 3 秒
L2 缓存 | 14 | 14 秒
RAM | 250 | 250 秒
硬盘 | 41 000 000 | 1.3 年
网络 | 240 000 000 | 7.6 年
- 有两种方法能避免阻塞型调用中止整个应用程序的进程:
- 在单独的线程中运行各个阻塞型操作
- 把每个阻塞型操作转换成非阻塞的异步调用
- 协程比多线程和多进程都要节省内存
- 只要函数中有 yield from, 函数就会变成协程,而协程不能直接调用,我们必须使用事件循环显式地排定协程的执行事件,或者在其他排定了执行时间的协程中使用 yield from 表达式把它激活。另外, yield from 只能用于协程和 asyncio.Future 实例。
- 访问本地文件系统会阻塞,硬盘 I/O 会浪费几百万个 CPU 周期, 而这可能会对应用程序的性能产生重大影响。
- 只有驱动协程, 协程才能做事,而驱动 asyncio.coroutine 装饰的协程有两种方法, 要么使用 yield from, 要么传给 asyncio 包中某个参数为协程或期物的函数,例如 run_until_complete。
- 智能的 HTTP 客户端, 例如单页 Web 应用(如 Gmail)或智能手机应用,需要快速、轻量级的响应和推送更新,服务器端最好使用异步框架,不要使用传统的 Web 框架(如 Django)。传统框架的目的是渲染完整的 HTML 网页, 而且不支持异步访问数据库。(不知道 Instagram 怎么看)
- WebSockets 协议的作用是为始终连接的客户端(例如游戏和流式应用)提供实时更新,因此,高并发的异步服务器要不间断地与成百上千个客户端交互。 asyncio 包的架构能够很好地支持 WebSockets.
第19章 动态属性和特性
- 数据的属性和处理数据的方法统称属性(attribute)。其实, 方法只是可调用的属性。
- 特性是用于管理实例属性的类属性。
- 我们通常把
__init__
称为构造方法, 其实, 用于构建实例的是特殊方法__new__
: 这是个类方法, 必须返回一个实例。返回的实例会作为第一个参数(即self)传给__init__
方法。因为调用__init__
方法时要传入实例,而且禁止返回任何值, 所以__init__
方法其实是初始化方法。真正的构造方法是__new__
。 - 抽象特性的定义有两种方式: 使用特性工厂函数,或者使用描述符类。
- 虽然内置的 property 经常用作装饰器,但它其实是一个类。property 构造方法的完整签名如下: property(fget=None, fset=None, fdel=None, doc=None)
- obj.attr 这样的表达式不会从 obj 开始寻找 attr, 而是从 obj.class 开始, 而且, 仅当类中没有名为 attr 的特性时, Python 才会在 obj 实例中寻找。
- 值直接存到 instance.dict 中, 就是为了跳过特性。
- dir([object]) 列出对象的大多数属性, vars([object]) 返回 object 对象的
__dict__
属性。如果没有指定参数, 那么 vars() 函数的作用与 locals() 函数一样: 返回表示本地作用域的字典。
第20章 属性描述符
- 实现了
__get__
、__set__
、__delete__
方法的类是描述符。描述符的用法是,创建一个实例,作为另一个类的类属性。描述符是实现了特定协议的类, 是对多个属性运用相同存取逻辑的一种方式。 - Django 模型的字段就是描述符。
- Python 存取属性的方式特别不对等。通过实例读取属性时, 通常返回的是实例中定义的属性;但是,如果实例中没有指定的属性,那么会获取类属性。而为实例中的属性赋值时,通常会在实例中创建属性,根本不影响类。
- 实现
__set__
方法的描述符属于覆盖型描述符,因为虽然描述符是类属性, 但是实现__set__
方法的话, 会覆盖对实例属性的赋值操作。 - 没有实现
__set__
方法的描述符是非覆盖型描述符。 - 不管描述符是不是覆盖型, 为类属性赋值能覆盖描述符属性。
- 在类上调用方法相当于调用函数。
- 函数都是非覆盖型描述符。在函数上调用
__get__
方法时传入实例,得到的是绑定到那个实例上的方法。 - 内置的 property 类创建的其实是覆盖型描述符。
- 只读描述符必须有
__set__
方法, 否则, 实例的同名属性会遮盖描述符。 只读属性的__set__
方法只需抛出 AttributeError 异常, 并提供合适的错误信息。 - 仅有
__get__
方法的描述符可以实现高效缓存。这种描述符可用于执行某些耗费资源的计算, 然后为实例设置同名属性,缓存结果。同名实例属性会遮盖描述符, 因此后续访问会直接从实例的__dict__
属性中获取值, 而不会再触发描述符的__get__
方法。
第21章 类元编程
- 类元编程是指在运行时创建或定制类的技巧。
- 我们把 type 视作函数, 因为我们像函数那样用它,例如 type(my_object)。 然而, type 是一个类。 当成类使用时, 传入三个参数可以新建一个类, type 的三个参数分别是 name、bases 和 attr_dict。 把三个参数传给 type 是动态创建类的常用方式。
- import 语句不只是声明, 在进程中首次导入模块时,还会运行所导入模块中的全部顶层代码。因此, “导入时” 和 “运行时” 之间的界限是模糊的: import 语句可以触发任何 “运行时” 行为。
- 解释器在导入时定义顶层函数, 但是仅当运行时调用函数时才会执行函数的定义体;对类来说, 情况就不同了:在导入时,解释器会执行每个类的定义体,甚至会执行嵌套类的定义体。执行类定义体的结果是,定义了类的属性和方法,并构建了类对象。类的定义体属于 “顶层代码”, 因为它在导入时运行。
- type 是大多数内置的类和用户定义的类的元类, 默认情况下, Python 中的类是 type 的实例。为了避免无限回溯, type 是其自身的实例。
- object 是 type 的实例, 而 type 是object的子类。
- 所有的类都间接或直接地是 type 的实例, 不过只有元类同时也是 type 的子类。 元类(如 ABCMeta) 从 type 类继承了构建类的能力。元类可以通过实现
__init__
方法定制实例(类)。 - 编写元类时, 通常会把 self 参数改成 cls。self 最终代指我们在创建的类的实例。
- 元类的用途:
- 验证属性
- 一次把装饰器依附到多个方法上
- 序列化对象或转换数据
- 对象关系映射(ORM)
- 基于对象的持久存储
- 动态转换使用其他语言编写的类结构
10 . 元类可以定制类的层次结构, 类装饰器则不同,它只能影响一个类,而且对后代可能没有影响。
结语
我还未见过有哪门语言像 Python 这样竭尽所能,让初学者易于入门,让专业人士用着顺手,让程序高手欢欣鼓舞。感谢 Guido van Rossum, 以及为此努力的每个人。