Python 数据模型
特殊方法
内置函数会调用特殊方法,一般无需直接调用特殊方法,除了__init__
方法。
__len__
:调用len(obj),解释器会自动调用 obj.__len__()
。
对于Python内置的类型(list,str等),CPython还会抄近路提升计算速度,直接返回PyVarObject(内存中长度可变的内置对象的C语言结构体)里的ob_size属性
__repr__
:被repr()函数调用,或者直接在控制台输入,把一个对象用字符串的形式表达出来以便辨认。面向开发者使用__str__
:被str()函数调用,或者用print函数打印时调用,返回的字符串对中断用户更友好。面向用户的
区别:如果一个对象没有__str__
函数,Python又需要调用时,会用__repr__
代替
__add__
:执行+运算__mul__
:执行*运算
PS:中缀运算符的原则是,不改变操作对象,而是产生出一个新的值
__rmul__
:解决交换律问题__bool__
:被bool()函数调用,应该返回bool类型
1 | def __bool__(self): |
序列构成的数组
内置序列
按元素类型分类:
容器序列:list、tuple、collections.deque,可以存放不同类型的数据
扁平序列:str、bytes、bytearray、memoryview、array.array,只能容纳一种类型
按是否能被修改分类:
可变序列:list、bytearray、array.array、collections.deque、memoryview
不可变序列:tuple、str、bytes
列表推导式、生成器表达式
列表推导式
生成新的列表
str = '测试字符串'
arr = [ord(x) for x in str if ord(x) > 30000]
与使用filter加map的比较
list(filter(lambda x: x > 30000, map(ord, str)))
生成器表达式:生成列表以外的序列类型
逐个的产出元素,而不会在内存中留下一个列表,节省内存,提高效率。
str = '测试字符串'*1000000
arr = (ord(x) for x in str if ord(x) > 30000)
上述操作花费0.2s,如果换成列表推到式,将大约花费2s。
如果声称其表达式是函数调用中的唯一参数,那么不再需要括号围起来,如 tuple(x for x in arr)
。
元组
可以作为“不可变列表”,还可以用于没有字段名的纪录。
具名元组
namedtuple,有名称和字段名的元组。
定义:x = ('Beijing', 2018)
或省略括号 x = 'Beijing', 2018
访问元素:x[0]
元组拆包
city, year, pop = ('Tokyo', 2003, 32450)
等式左边是元组的省略括号的写法,等同于(city,year,pop) = ...
。
元组拆包可以应用到任何可迭代对象上,唯一的要求是,接受元素的元组的空档数必须要和被迭代对象中的元素数量一致。
对于不关心的元素,可以用_占位符代替。对于多余的元素,可以用*来忽略。
str = '测试字符串'
a,b,_,*args = str
*前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置。
拆包时带的变量类型被定义为list,如果加在方法的形参上面,则变量被定义为tuple。
a, *b = 'Beijing', 2018, 1
print(type(b)) #list
1 | def my(a,*b): |
不使用中间变量交换两个变量的值
b,a=a,b
嵌套元组拆包
name, cc, pop, (lat, long) = ('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
print('{:^15}|{:^9.4f}|{:^9.4f}'.format(name, lat, long)) # :9个单位的空间,保留4位小数,^居中
具名元组
1 | import collections |
元素访问:
tokyo[0],tokyo.name
属性和方法:
City._fields:返回包含这个类所有字段名称的元组
City._make():通过接受一个可迭代对象来生成一个实例,和调用City()是一样的
City._asdict():以 collections.OrderedDict 形式返回,方便遍历元素
1 | for k, v in tokyo._asdict().items(): |
元组与列表的方法区别:元组没有列表的增删改/清空等方法,包含查询和统计的方法。
切片
slice
切片和区间操作都不包含范围的最后一个元素,(左闭右开)。
优点:快速计算长度,后一个数减前一个数。快速分割元素不重叠,mylist[:10]
和mylist[10:]
s = 'bicycle'
print(s[::3]) # start:stop:step
相当于s[slice(None,None,3)]
,即s.__getitem__(slice(None,None,3))
。
多维切片:二维的numpy.ndarray就可以用a[m:n, k:l]的方式来得到二维切片
省略:ellipsis,Ellipsis对象是ellipsis类的单一实例
给切片复制:切片可以放在复制语句的左边或作为del操作的对象,可以对序列进行嫁接,切除或就地修改
1 | l[2:5] = [20,30] |
对序列使用+和*
浅表复制,序列中包含引用的话,仅仅是复制引用。
创建一个3*3的列表
arr = [['_']*3]*3
实际上创建了一个[‘‘,’‘,’_’]列表,然后复制了3次引用。改变其中一个列表的元素 arr[1][2] = '0'
,将影响三个列表。
等同于:
1 | row = ['_'] * 3 |
正确的做法:
arr = [['_'] * 3 for i in range(3)]
序列的增量赋值
a += b
增量赋值运算符+=和*=的表现取决于他们第一个操作对象。背后的特殊方法时 __iadd__
和 __imul__
,如果a实现了 __iadd__
,则会调用这个方法。
对于可变序列(list,bytearray…)来说,a就会就地改动(in-place),好像调用了 a.extend(b)
一样。
如果a没有实现 __iadd__
,这个表达式就变得跟 a = a+b
一样了。会计算 a+b
,得出一个新对象,然后赋值给a。
1 | l=[1,2,3] |
问题:
1 | t = (1, 2, [30, 40]) |
结果:t变成(1,2,[30,40,50,60]),但仍会抛出TypeError异常
查看类似过程的字节码:
1 | import dis |
步骤说明:
- 将s[a]的值存入TOS(Top Of Stack)
- TOS += b,TOS是一个list,可以执行
- s[a] = TOS,s是不可变的,所以执行失败
结论:
- 不要把可变对象放在元组里
- 增量赋值不是一个原子操作
list.sort()和sorted()
list.sort(),就地排序,返回None。
sorted,新建一个列表并返回。可接受任何形式的可迭代对象,如,字符串,不可变序列或生成器,最后都会返回一个列表。
list.sort()和sorted()的参数:
reverse:为True则降序排列
key:一个只有一个参数的函数,key=len,key=myfunc,默认值是恒等函数
包含key参数的内置函数还有,min()和max()等。
bisect管理已排序的序列
bisect模块包含两个主要函数,bisect和insort,都利用二分查找来在有序序列中查找或插入元素。
bisect.bisect:bisect(haystack, needle),在haystack(干草垛,必须升序)中找needle(针,待插入的元素)的位置
可选参数,lo和hi,控制搜索的范围。
bisect其实是bisect_right函数的别名,如果碰到相等的元素,bisect_right返回的插入位置是原序列中被插入元素相等的元素的之后的位置。
相应的还有bisect_left,对于这种情况则返回相等元素的原位置,即相等元素之前的位置。
1 | import bisect |
bisect.insort:insort(seq,item)把变量item插入到序列seq中,并能保持seq的升序顺序
1 | import bisect |
列表不是首选时
如果要存放1000万个浮点数的话,数组(array)的效率要高得多。因为数组背后存的不是float对象,而是数字的机器翻译,也就是字节表述。就跟C语言中的数组一样。
如果要频繁的进行FIFO的操作,deque速度会更快。如果检查元素是否存在的频率很高,用set更合适。因为set专为检查元素是否存在做过优化。
数组:数组支持所有跟可变序列有关的操作,pop,insert,extend。还有从文件读和存入文件的更快的方法,frombytes,tofile
1 | from array import array |
从Python3.4开始,数组类型不在支持诸如list.sort()这种就地排序,排序要用sorted函数新建一个数组。
内存视图:能让用户在不复制内容的情况下操作同一个数组的不同切片。memoryview.cast会把同一块内存里的内容打包成一个全新的memoryview对象。
1 | import array |
双向队列和其他形式的队列:
1 | from collections import deque |
append和popleft都是原子操作,deque可以在多线程程序中安全地当做FIFO的栈使用。
字典和集合
泛映射类型
collections.abc模块中有Mapping和MutableMapping两个基类。
ABC:Abstract Base Class
非抽象映射类型一般不直接继承这些abc,他们的作用是作为形式化文档,为dict何其他映射类型定义形式接口。还可以通过isinstance来判断某个数据是否是映射类型。
my_dict = {} # 或其他类型的Map
isinstance(my_dict, abc.Mapping) # True
可散列类型:
如果一个对象是可散列的,那么在这个对象的声明周期中,它的散列值是不变的,而且这个对象要实现hash()方法。
另外可散列对象还有有qe()方法,这样才能和其他键值作比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的。
原子不可变数据类型(str、butes和数值),frozenset,tuple(必须包含的所有元素都是可散列类型),都是可散列的。
可散列tuple:
1 | t1 = (1, 2) |
字典的构造方法:
1 | a = {'one': 1} |
字典推导
1 | CODES = [ |
映射的常用方法
setdefault:
my_dict.setdefault(key, []).append(new_value)
等价于
1 | if key not in my_dict: |
等价于
1 | v = my_dict.get(key, []) |
defaultdict:
1 | from collections import defaultdict |
把list的构造方法作为default_factory来创建一个defaultdict,只会在getitem里被调用,其他比如get方法,在找不到键时只会返回None。
__missing__
:
只被__getitem__
调用,如defaultdict,在__missing__
中实现了调用default_factory方法。
字典的变种
collections.OrderedDict:添加键的时候会保持顺序
collections.ChainMap:可以容纳数个不同的映射对象,然后进行键查找时会把这些对象当成一个整体逐个查找。这个功能再给有嵌套作用域的语言做解释器的时候很有用,如:
1 | import builtins |
UserDict:子类化,以UserDict为基类创造自定义映射类型,它并不是dict的子类,继承的是MutableMapping,有一个data属性,是一个dict实例。最终存储数据的地方。
不可变映射类型:返回一个映射的只读视图,但是时动态的,如果原映射有改动,可以通过视图观察到。
1 |
|
set
保证元素的唯一性,集合中的元素必须是可散列的,set本身是不可散列的,但是frozenset可以
中缀运算:a|b,返回合集。a&b返回交集,a-b返回差集
计算重叠元素的个数
found = len(needles & haystack) #两侧都需要是集合类型
或
found = len(set(needles).intersection(haystack)) #如果一个对象还不是集合类型
直接使用set的交集计算比for遍历计算求和快的多
字面量:s = {1}
或 s = set([1])
空集:s = set(),不能写{}(空字典)
直接写{1}的方式更快,因为Python会利用一个专门的BUILD_SET的字节码来创建集合。查看区别:
1 | from dis import dis |
fronzenset只能通过构造方法创建。
集合推导:
1 | from unicodedata import name |
集合的的继承关系:
基本操作:
- +/-/&/|
- e in s,元素e是否属于s
- s <= z,s是否为z的子集
- s < z,s是否为z的真子集
- s >=z/s>z
dict和set的背后原理
散列表其实是一个稀疏数组(总有空白元素的数组),散列表的单元通常叫做表元(bucket),在dict的散列表当中,每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大小一致,所以可以通过偏移量来读取某个表元。
因为Python会设法保证大概还有三分之一的表元是空的,所以快要达到这个阈值时,原有的散列表会被复制到更大的空间里面进行扩容。
散列表算法
为了获取dict[search_key]背后的值,Python首先会调用hash(search_key)来计算search_key的散列值,取这个值最低的几位数字(具体几位看当前散列表的大小)当做偏移量,在散列表里查找表元。若找到的表元是空的,则抛出KeyError异常。若不是空的,则表元里会有一堆found_key:found_value,这时候进行校验search_key == found_key是否为真,如果相等,则返回found_value。
如果search_key和found_key不匹配的话,这种情况称为散列冲突。发生这种情况原因是,把随机元素映射到几位的数字上,而索引又依赖于这几位数字的一部分而已。为了解决散列冲突,算法会在散列值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字再当成索引寻找表元,重复以上给欧成。
添加和更新操作几乎跟上面一样。
dict的实现及其限制
- 键必须是可散列的
支持hash()函数,并且通过 __hash__()
方法得到的散列值是不变的。
支持通过 __eq__()
方法来检测相对性。
若a == b为真,则hash(a) == hash(b) 也为真。
所有由用户自定义的对象默认都是可散列的,散列值由id()来计算获取。
- 字典内存开销巨大
为了保持散列表是稀疏的,将降低在空间上的效率。
如果要存放数量巨大的纪录,元组或具名元组构成的列表是比较好的选择。
- 键查询很快
dict是典型的空间换时间,内存开销大,但是查询速度快。
- 键的次序取决于添加顺序
添加新键又发生散列冲突时,新键可能会被安排存放到另一个位置。这将导致键的顺序乱掉。
- 往字典里添加新键可能会改变已有键的顺序
无论何时添加新键,Python解释器都可能做出字典寇蓉的决定。扩容导致把字典中已有的元素添加到更大的新表中,过程中如果发生散列冲突,将导致新标中键的次序变化。
因此不要同时对字典进行迭代和修改,最好分开两步进行。
文本和字节序列
字符问题
Unicode字符标识和字节表述:
字符的标识,即码位。是0~1114111的数字,以4~6个十六进制数字表示。
字节表述取决于所用的编码,是在码位和字节序列之间转换时使用的算法。如UTF-8,字符在ASCII范围内用的编码成1个字节,一个汉字占3个字节。
字节概要
调用各自的构造方法,构建bytes或bytearray实例。
一个str对象和一个encoding关键字参数。
一个可迭代对象,提供0~255之间的数值。
一个实现了缓冲协议的对象(bytes,bytearray,memoryview,array.array),把源对象中的字节序列复制到新建的二进制序列中。
1 | cafe = bytes('café', encoding='utf8') |
各个字节的值可能使用下列三种不同的方式显示,
可打印的ASCII范围内的字节(空格到~),使用ASCII字符本身。
制表符、换行符、回车符和\,使用转义序列\t、\n、\t和\。
其他字节,使用十六进制转义序列(\x00)
编码异常
UnicodeEncodeError:编码时,编解码器没有定义某个字符时,就会抛出
1 | city = 'São Paulo' |
UnicodeDecodeError:解码时碰到无法转换的字节序列时会抛出
1 | octets = b'Montr\xe9al' |
处理文本文件
1 | import os |
如果读取时不指定编码格式,那么Python会使用系统默认的编码,很有可能出现乱码。所以需要在多台设备或场合下运行的代码,打开读取文件时应始终明确传入encoding参数。
编码默认值:
1 | import sys |
输出:
1 | locale.getpreferredencoding() -> 'cp936' |
一等函数
把函数视作对象
“一等对象”的定义为满足下述条件的程序实体:
- 函数是一等对象。
- 在运行时创建。
- 可以赋值给变量,通过变量调用。
- 能作为参数传给函数。
- 能作为函数的返回结果。
1 | def factorial(n): |
高阶函数
接受函数为参数或返回函数为结果的是函数是高阶函数,如map,sorted
- map,filter返回生成器,因此现在它们的直接替代品是生成器表达式。
- functools.reduce,最常用于求和。
- all(iterable),如果iterable的每个元素都是真值,就返回True。
- any(iterable),如果iterable中有元素是真值,就返回True。
匿名函数
因为句法限制,lambda函数的定义体不能赋值和使用while等语句,在Python中很少使用。
1 | words = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana'] |
可调用对象
可被调用运算符(即())应用的,叫做可调用对象,可以使用callable()函数判断。
包括:
- 用户定义的函数(def或lambda创建)
- 内置函数(C语言实现的函数,如len)
- 内置方法(C语言实现的方法,如dict.get)
- 方法(在类的定义体中定义的函数)
- 类(调用顺序为
__new__
,__init__
初始化实例,然后返回给调用方) - 类的实例(定义了
__call__
方法的类) - 生成器函数(使用yield关键字的函数或方法,返回的是生成器对象)
用户定义的可调用类型
实现了 __call__
的对象:
1 | class Bingo(): |
函数内省
dir函数可以探知对象的属性,计算出函数专有而一般对象没有的属性:
1 | class C(): pass |
从定位参数到仅限关键字参数
1 | def tag(name='p', *content, cls=None, **attrs): |
cls即仅限关键字的参数,因为未指定关键字的参数会被cls前面的*content捕获,并且cls有默认值可不指定,所以想要指定的话就必须指定关键字。
获取关于参数的信息
1 | from inspect import signature |
更好更直观的方式:
1 | sig = signature(tag) |
函数注解
1 | from inspect import signature |
函数式编程的包
operator模块:多个算术运算符提供了对应的函数, 从而避免编写lambda a, b: a*b
这种平凡的匿名函数。
1 | from functools import reduce |
methodcaller:
1 | from operator import methodcaller |
使用一等函数实现设计模式
策略模式
经典的策略模式
1 | from abc import ABC, abstractmethod |
函数实现“策略”模式
1 |
|
找出模块中的全部策略
若想添加新的促销策略,要定义函数且加到promos列表中,下面尝试更灵活的方式。
globals:返回一个字典,表示当前的全局符号表
把策略方法封装到promotions模块中,使用inspect内省,取出
内省模块的全局命名空间, 构建 promos 列表:
1 |
|
内省单独的 promotions 模块, 构建 promos 列表:
1 | promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)] |
命令模式
命令模式是回调机制的面向对象替代品,目的是解耦调用者和接收者。做法是在调用者和接收者之间放一个Command对象,让它实现execute接口,调用接受者自己的方法执行所需的操作,这样调用者无需了解接收者的接口,不同的接收者也可以适应不同的Command子类。
在 Python 中使用函数或可调用对象实现回调更自然,可以不用为调用者提供一个Command实例,而是给它一个函数,调用者不用调用command.execute(),直接调用command()即可。
1 | class MacroCommand(): |
函数装饰器和闭包
装饰器基础
装饰器是可调用对象,参数是被装饰的函数。可以把它替换掉也可以原样返回。
1 | def decorate(func): |
装饰器只是语法糖:
1 |
|
可以转换为:
1 | def target(): |
装饰器的执行时机
装饰器在被装饰的函数定义之后立即执行,这通常是在导入时(即Python加载模块时)。
1 | def reister(func): |
打印如下:
1 | running reister <function f1 at ...> |
装饰器在真实代码中的常用方式:
- 装饰器通常定义在一个模块中,然后应用到其他模块中的函数上。
- 大多数装饰器会在内部定义一个函数,然后将其返回。也可能原样返回,比如很多Python Web框架使用这样的装饰器把函数添加到某种中央注册处。比如上面的例子中,使用促销装饰器把所有促销注册起来,供best_promo使用。
变量作用域
1 | b = 6 |
会报错local variable ‘b’ referenced before assignment,Python判断b是局部变量,运行时从本地环境中获取,但是还未定义,所以报错。通过dis查看操作b的字节码为LOAD_GLOBAL。
改为:
1 | b = 6 |
不再报错,读取和修改的都是全局变量。通过dis查看操作b的字节码为LOAD_FAST。
CPython VM是栈机器,LOAD和POP操作引用的是栈。
闭包
闭包指延伸了作用域的函数, 它能访问定义体之外定义的非全局变量。
1 | def make_averager(): |
在 averager 中, series 是自由变量(free variable, 指未在本地作用域中绑定的变量)。
1 | print(avg.__code__.co_varnames) # 局部变量 |
nonlocal 声明
1 | def make_averager(): |
实现一个简单的装饰器
1 | import time |
使用functools.wraps装饰器解决name覆盖的问题:
functools.wraps 装饰器会把相关的属性从 func 复制到 clocked 中。
1 | def clock(func): |
标准库中的装饰器
functools.lru_cache:
lru,即“Least Recently Used”。
它把耗时的函数的结果保存起来, 避免相同的参数重复计算。缓存不会无限制增长, 一段时间不用的缓存会被扔掉。
1 | import functools |
极大的减少调用次数。
functools.lru_cache(maxsize=128, typed=False)
参数:
maxsize,指定存储多少个调用的结果,应该设为2的幂。
typed,True会把不同参数类型得到的结果区分开,比如1和1.0。
因为lru_cache使用字典存储结果,所以被修饰的函数,所有参数都必须是可散列的。
functools.singledispatch:
Python 不支持重载方法或函数, 所以我们不能使用不同的签名定义函数的变体, 也无法使用不同的方式处理不同的数据类型。 在Python 中, 一种常见的做法是把函数变成一个分派函数。singledispatch装饰器可以把整体方案拆分成多个模块。
1 | from functools import singledispatch |
叠放装饰器
1 |
|
相当于:
1 | def f(): |
参数化装饰器
1 | registry = set() |
装饰器函数金字塔:
1 | import time |
对象引用、可变性和垃圾回收
标识、相等性和别名
id():
在 CPython 中, id() 返回对象的内存地址, 但是在其他 Python 解释器中可能是别的值。 关键是, ID 一定是唯一的数值标注, 而且在对象的生命周期中绝不会变。
==和is:
is运算符比==速度快,==是语法糖,等同于a.__eq__(b)
默认浅复制
列表的内置构造函数或list[:]语句,做的都是浅复制(只复制最外层的容器,容器中的元素只复制引用)。
1 | l1 = [3, [66, 55, 44], (7, 8, 9)] |
深复制和浅复制:
1 | from copy import deepcopy |
函数的参数作为引用时
Python 唯一支持的参数传递模式是共享传参,指函数的各个形式参数获得实参中各个引用的副本。 也就是说, 函数内部的形参是实参的别名。
1 | def f(a, b): |
不要使用可变类型作为参数的默认值,如:
1 | def __init__(self, passengers=[]): |
默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象, 而且修改了它的值, 那么后续的函数调用都会受到影响。
默认值会储存在class.__init__.__defaults__
的属性里(一个tuple)。
防御可变对象参数:
当参数是可变对象时,如果没有约定的情况下,最好创造一个可变参数的副本,如:
1 | def __init__(self, passengers=None): |
这样就不会影响外部的对象了。
del 和 垃圾回收
del语句删除名称,而不是对象。仅当删除的变量保存的是对象的最后一个引用, 或者无法得到对象时,del 命令才可能导致对象被当作垃圾回收。
在 CPython 中, 垃圾回收使用的主要算法是引用计数。 实际上, 每个对象都会统计有多少引用指向自己。 当引用计数归零时, 对象立即就被销毁: CPython 会在对象上调用 __del__
方法(如果定义了),然后释放分配给对象的内存。
1 | import weakref |
弱引用
弱引用不会增加对象的引用数量,不会妨碍所指对象被当作垃圾回收。弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用而始终缓存对象。
1 | import weakref |
weakref.ref类其实是低层接口,最好不要手动处理weakref.ref实例,而是使用weakref集合。
符合Python风格的对象
向量类
1 | from array import array |
classmethod与staticmethod
classmethod:
定义操作类的方法,第一个参数是类本身,最常见的用途是定义备选构造函数
staticmethod:
静态方法就是普通函数,只是碰巧在类的定义体中,而不是在模块层定义的。完全可以不用使用它。
1 | class Demo(): |
格式化显示
委托给相应的 __format__(format_spec)
方法
1 | brl = 1/2.43 |
rate是字段名,.4f是格式规范微语言。
整数使用的代码有 ‘bcdoxXn’, 浮点数使用的代码有’eEfFgGn%’, 字符串使用的代码有 ‘s’。
格式规范微语言文档(https://docs.python.org/3/library/string.html#formatspec)
1 | print(format(42, 'b')) # b二进制 o八进制 x十六进制 |
如果没有定义__format__
方法,从object继承的方法会返回str(obj)。
print(format(v)) # (2.0, 3.0)
但是如果传入格式说明符,object.format方法会抛出TypeError。
print(format(v,'.2f)) # TypeError
给Vector2d定义__format__
方法:
1 | def __format__(self, fmt_spec=''): |
自定义格式代码:
假设我们自定义的代码为p,用来显示极坐标中的向量
1 | def angle(self): |
可散列的Vector2d
1 | # other code |
让这些向量不可变是有原因的, 因为这样才能实现hash 方法。
私有属性和“受保护”的属性
1 | def __init__(self, x, y): |
使用两个_作为前缀命名实例属性,Python会把属性名存入实例的dict属性中,而且会进行“名称改写”,如prop会被改写成_ClassNameprop。
1 | v = Vector2d(3, 4) |
也有人不喜欢这种不对称的名称,他们约定使用一个_前缀编写“受保护”的属性,如self._x。Python不会对这种属性名做特殊处理,这仅仅是程序员之间遵守的约定,他们不会在类外部访问这种属性。
使用slots类属性节省空间
创建一个类属性,使用 __slots__
这个名字, 并把它的值设为一个字符串构成的可迭代对象。
1 | class Vector2d: |
作用是告诉解释器,这个类中的所有实例属性都在这儿了!
Python会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的dict属性。当同时有数百万个实例活动时,能节省大量内存。
副作用是定义了slots属性之后,实例不能再有slots 中所列名称之外的其他属性。 但故意这样禁止用户新增属性是不对的。
如果定义了slots属性,且想让对象支持弱引用,则必须把weakref添加到slots中。
解释器会忽略继承的slots属性,所以每个子类都要定义slots属性。
覆盖类属性
1 | class Vector2d(): |
类属性为实例属性提供默认值,但是并不代表实例拥有此实例,如果为类属性赋值,类属性不会受影响,实例会创建同名的属性覆盖掉类属性。想要修改类属性,需要直接在类上修改,Vector2d.typecode = 'f'
。
序列的修改、散列和切片
1 | from array import array |
reprlib.repr() 函数获取 self._components 的有限长度表示形式(如 array(‘d’, [0.0, 1.0, 2.0, 3.0, 4.0, …])) 。
协议和鸭子类型
1 | def __len__(self): |
协议是非正式的接口, 只在文档中定义, 在代码中不定义。
例如, Python 的序列协议只需要 __len__
和 __getitem__
两个方法。 任何类(如 Spam)只要实现了这两个方法, 就能用在任何期待序列的地方。
1 | v = Vector([1, 2, 3, 4, 5]) |
可切片的序列
切片原理
1 | class MySeq(): |
slice.indices方法:
1 | print(slice(None, 10, 2)) # slice(None, 10, 2) |
indices方法提供了内置序列实现的复杂“整顿”逻辑,用于优雅地处理索引缺失、负数、长度过长等情况。如果自定义的getitem最终不是依靠的底层序列,那么可以使用这个方法节省大量时间。
自定义切片的getitem
1 | def __getitem__(self, index): |
动态存取属性
使用x、y、z、t来取代v[0],v[1],v[2],v[3]。
1 | def __getattr__(self, name): |
为了防止直接向v.x赋值而创建一个新的属性。
1 | def __setattr__(self, name, value): |
散列和快速等值测试
规约函数reduce,sum,any,all把序列或有限可迭代对象编程一个聚合结果。reduce函数的参数,第一个函数是接受两个参数的函数,第二个参数是一个可迭代的对象,第三个参数是初始值。
reduce(fn,lst):
fn(lst[0],lst[1]) -> r1
fn(r1,lst[2]) -> r2
fn(r2,lst[3]) -> r3
...
实现阶乘的三种方式:
1.
1 | n = 0 |
2.
functools.reduce(lambda a, b: a ^ b, range(6))
3.
functools.reduce(operator.xor, range(6))
把Vector变成可散列的对象:1
2
3def __hash__(self):
hashes = (hash(x) for x in self._components)
return functools.reduce(operator.xor, hashes, 0)
修改 __eq__
方法:
使用zip函数
1
2
3
4
5
6
7def __eq__(self, other):
if len(self) != len(other):
return False
for a, b in zip(self, other):
if a != b:
return False
return True使用all函数
1
2def __eq__(self, other):
return len(self) == len(other) and all(a == b for a, b in zip(self, other))
出色的zip函数:
zip函数能并行迭代两个或多个可迭代对象,它返回的元组可以拆包成变量,分别对应各个并行输入中的一个元素。
1 | from itertools import zip_longest |
itertools.chain函数:合并两个序列
1 | import itertools |
接口:从协议到抽象基类
接口和协议
协议:非正式的接口,不能像正式接口那样施加限制,是Python实现多态的方式。一个类可依只实现部分接口,这是允许的。
1 | class Foo: |
Foo类没有继承abc.Sequence,只实现了序列协议的一个方法:__getitem__
,没有__iter__
方法,但是仍然会后备使用__getitem__
来进行迭代。没有实现__contains__
方法,但是也能使用in运算。
使用猴子补丁在运行时实现协议
1 | import random |
我们可以动态修正这个问题:
1 | def set_card(self, position, card): |
猴子补丁: 在运行时修改类或模块, 而不改动源码。 猴子补丁很强大, 但是打补丁的代码与要打补丁的程序耦合十分紧密, 而且往往要处理隐藏和没有文档的部分
标准库中的抽象基类
collections.abc模块中的抽象基类
collections.abc:
Iterable、 Container 和 Sized:
各个集合应该继承这三个抽象基类, 或者至少实现兼容的协议。 Iterable 通过 __iter__
方法支持迭代, Container 通过__contains__
方法支持 in 运算符, Sized 通过 __len__
方法支持len() 函数。
Sequence、 Mapping 和 Set:
这三个是主要的不可变集合类型, 而且各自都有可变的子类。
MappingView:
映射方法 .items()、 .keys() 和 .values() 返回的对象分别是 ItemsView、 KeysView 和 ValuesView 的实例。 前两个类还继承了 Set 类
Callable 和 Hashable:
没有Callable 或 Hashable 的子类。 这两个抽象基类的主要作用是为内置函数 isinstance 提供支持, 以一种安全的方式判断对象能不能调用或散列。若想检查是否能调用, 可以使用内置的 callable() 函数; 但是没有类似的 hashable() 函数, 因此测试对象是否可散列, 最好使用 isinstance(my_obj, Hashable)。
抽象基类的数字塔
numbers包定义的抽象基类是线性的层次结构,依次往下是,Number、Complex、Real、Rational、Integral。
检查是否为整数,isinstance(x,numbers.Integral)。
定义使用一个抽象基类
定义:
1 | import abc |
错误的实现:
1 | from tombola import Tombola |
正确的实现:
1 | import random |
虚拟子类:
在抽象基类上调用register方法,issubclass和isinstance等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性。Python也不会做检查。
1 | from random import randrange |
虚拟子类检查:
1 | from tombola import Tombola |
__mro__
属性:方法解析顺序,按顺序列出类及其超类。
1 | # (<class 'tombolist.TomboList'>, <class 'list'>, <class 'object'>) |
Tombolist.__mro__
中没有 Tombola, 因此 Tombolist 没有从Tombola 中继承任何方法。
继承内省属性:
1 | # __subclasses__(),这个方法返回类的直接子类列表, 不含虚拟子类。 |
使用register
Tombola.register可以当做类装饰器使用,也可以当做函数调用Tombola.register(TomboList)。
这种做法更常见,可用于注册其他地方定义的类,例如,在collections.abc模块的源码中,Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)
__subclasshook__
:
1 | from collections import abc |
没有注册和继承,抽象基类也能把一个类识别为虚拟子类,因为abc.Sized实现了一个特殊的类方法,名为__subclasshook__
。源码:
1 | class Sized(metaclass=ABCMeta): |
继承的优缺点
子类化内置类型很麻烦
直接子类化内置类型(如 dict、 list 或 str) 容易出错,因为内置类型的方法通常会忽略用户覆盖的方法(这种问题只发生在C语言实现的内置类型内部的方法委托上,而且只影响直接继承内置类型的用户自定义类)。
不要子类化内置类型, 用户自己定义的类应该继承 collections 模块(http://docs.python.org/3/library/collections.html) 中的类, 例如UserDict、 UserList 和 UserString, 这些类做了特殊设计, 因此易于扩展。
多重继承和方法解析顺序
1 | class A: |
Python能区分子类调用的是哪个父类的方法,是因为Python会按照特定的顺序遍历继承图。这个顺序叫方法解析顺序(Method Resolution Order, MRO)。类都有一个名为__mro__
的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直向上,直到object类。
1 | D.__mro__ # (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>) |
也可以在子类中绕过方法解析顺序,直接调用某个超类的方法,比如,A.ping(self),而不是super().ping()。
正确重载运算符
略
可迭代的对象、迭代器和生成器
单词序列
1 | import re |
可以迭代的原因:
从 Python 3.4 开始, 检查对象 x 能否迭代, 最准确的方法是:调用 iter(x) 函数(或直接进行迭代), 如果不可迭代, 再处理 TypeError 异常。 这比使用 isinstance(x, abc.Iterable) 更准确, 因为 iter(x)函数会考虑到遗留的 __getitem__
方法, 而 abc.Iterable 类则不考虑。
可迭代的对象与迭代器的对比
使用 iter 内置函数可以获取迭代器的对象。 如果对象实现了能返回迭代器的 __iter__
方法, 那么对象就是可迭代的。
可迭代的对象和迭代器之间的关系: Python 从可迭代的对象中获取迭代器。
1 | s = 'ABC' |
字符串’ABC’是可迭代的对象,背后运行的是迭代器。
不使用for语句的话,就需要使用while和iter组合迭代。
1 | s = 'ABC' |
标准的迭代器接口有两个方法:
__next__
,返回下一个可用元素或无元素时抛出StopIteration异常。
__iter__
,返回self,以便在应该使用可迭代对象的地方使用迭代器,例如在for循环中。
collections.abc.Iterator抽象基类中定义了__next__
抽象方法。且这个抽象基类继承自collections.abc.Iterable类,这个类中定义了__iter__
抽象方法。
1 | i1 = id(s.__iter__()) # 返回字符串的迭代器 |
Python中的迭代器是一种协议,而不是某种特定的类型。所以判断一个对象x(或类)是否为迭代器最好的方式是调用isinstance(x, abc.Iterator)(或issubclass),得益于Iterator.__subclasshook__
方法, 即使对象 x 所属的类不是Iterator 类的真实子类或虚拟子类, 也能这样检查。
1 | class X(): |
迭代器的定义:
迭代器是这样的对象: 实现了无参数的 __next__
方法, 返回序列中的下一个元素; 如果没有元素了, 那么抛出 StopIteration 异常。Python 中的迭代器还实现了 __iter__
方法, 因此迭代器也可以迭代。
典型的迭代器
1 | class Sentence(): |
为了“支持多种遍历”, 必须能从同一个可迭代的实例中获取多个独立的迭代器, 而且各个迭代器要能维护自身的内部状态, 因此这一模式正确的实现方式是, 每次调用 iter(my_iterable) 都新建一个独立的迭代器。 这就是为什么这个示例需要定义SentenceIterator 类。
可迭代的对象一定不能是自身的迭代器。 也就是说, 可迭代的对象必须实现 __iter__
方法, 但不能实现 __next__
方法。另一方面, 迭代器应该一直可以迭。 迭代器的 __iter__
方法应该返回自身。
生成器函数
实现相同功能, 但却符合 Python 习惯的方式是, 用生成器函数代替SentenceIterator 类。
1 | class Sentence(): |
生成器函数:只要 Python 函数的定义体中有 yield 关键字, 该函数就是生成器函数。 调用生成器函数时, 会返回一个生成器对象。 也就是说, 生成器函数是生成器工厂。
1 | def gen_123(): |
打印结果:
1 | start |
惰性实现
1 | class Sentence(): |
生成器表达式
生成器表达式可以理解为列表推导的惰性版本: 不会迫切地构建列表,而是返回一个生成器, 按需惰性生成元素。 如果说列表推导是制造列表的工厂,那么生成器表达式就是制造生成器的工厂。
1 | def __iter__(self): |
这里不再是生成器函数了(没有yield),而是使用生成器表达式构建生成器,然后将其返回。
生成器表达式是语法糖: 完全可以替换成生成器函数。
何时使用生成器表达式
如果生成器表达式要分成多行写,就使用生成器函数,更灵活,提高可读性,可重用,且可以作为协程使用。
如果函数或构造函数只有一个参数,传入的生成器表达式不用自带一对括号了,只要有一对函数的括号就行了。
等差数列生成器
1 | def aritprog_gen(begin, step, end=None): |
itertools模块生成等差数列:
1 | gen = itertools.count(0, 1.5) |
itertools.count函数生成无穷等差数列,作用与上面的函数相同。
itertools.takewhile函数会生成一个使用另一个生成器的生成器, 在指定的条件计算结果为 False 时停止。 因此, 可以把这两个函数结合在一起使用。
1 | gen = itertools.takewhile(lambda n: n < 5, itertools.count(0, 1.5)) # n是每次返回的产出值 |
重写aritprog_gen函数:
1 | def aritprog_gen(begin, step, end=None): |
标准库中的生成器函数
用于“过滤”的生成器函数:
代码演示:
1 | import itertools |
takewhile和filter的区别:
1 | def condition(e): |
用于”映射”的生成器函数:
代码演示:
1 | import itertools |
用于“合并”的生成器函数:
1 | print(list(itertools.chain('ABC', range(2)))) # ['A', 'B', 'C', 0, 1] |
把输入的各个元素扩展成多个输出元素的生成器函数:
1 | ct = itertools.count(5) |
扩展生成器中的“组合学”生成器函数:
1 | # [('A', 'B'), ('A', 'C'), ('B', 'C')] |
用于重新排列元素的生成器函数:
1 | print(list(itertools.groupby('AAABB'))) |
yield from
用于在生成器函数中方便的产出另一个生成器生成的值。
1 | def chain(*iterables): |
可迭代的归约函数
这里的每个内置函数都可以用functools.reduce实现,内置是为了方便使用。此外,对all和any函数来说,有一项重要的优化措施是 reduce 函数做不到的: 这两个函数会短路。
1 | print(all([1, 2, 3])) # True |
深入分析iter函数
1 | def d6(): |
上下文管理器和else块
if之外的else块
先做这个,再做那个。
1 | for i in range(10): |
while:
仅当while循环因为条件为假值而退出时(即while循环没有被break语句中止)才运行else块。
try:
仅当 try 块中没有异常抛出时才运行else块。官方文档还指出:“else 子句抛出的异常不会由前面的 except 子句处理。”
EAFP风格:
上下文管理器和with块
with 语句的目的是简化 try/finally 模式。 这种模式用于保证一段代码运行完毕后执行某项操作, 即便那段代码由于异常、 return 语句或sys.exit() 调用而中止, 也会执行指定的操作。
上下文管理器协议包含 __enter__
和 __exit__
两个方法。 with 语句开始运行时, 会在上下文管理器对象上调用 __enter__
方法。 with 语句运行结束后, 会在上下文管理器对象上调用 __exit__
方法, 以此扮演 finally 子句的角色。
上下文管理器举例:
1 | class LookingGlass(): |
1 | from mirror import LookingGlass |
使用@contextmanager
contextlib.contextmanager 装饰器会把函数包装成实现 __enter__
和 __exit__
方法的类:yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__
方法时) 执行, yield 语句后面的代码在with 块结束时(即调用 __exit__
方法时) 执行。
1 | import contextlib |
上述示例有一个严重的错误:如果在with块中抛出了异常,Python 解释器会将其捕获,然后在 looking_glass函数的yield表达式里再次抛出。但是,那里没有处理错误的代码,因此looking_glass函数会中止,永远无法恢复成原来的sys.stdout.write方法。修复代码如下:
1 |
|
协程
用作协程的生成器的基本行为
1 | def simple_coroutine(): |
协程的四种状态,可以用inspect.getgeneratorstate(…)函数确定:
- ‘GEN_CREATED’,等待开始执行
- ‘GEN_RUNNING’,解释器正在执行
- ‘GEN_SUSPENDED’,在yield表达式处暂停
- ‘GEN_CLOSED’,执行结束
仅当协程处于暂停时才能调用send方法,因此刚创建的协程必须先用next(coro)激活,也可用coro.send(None),这一步成为“预激”(即让协程向前执行到第一个yield表达式)。
使用协程计算移动平均值
1 | def averager(): |
预激协程的装饰器
自定义一个装饰器:
1 | from functools import wraps |
使用:
1 | from coroutil import coroutine |
终止协程和异常处理
1 | class DemoException(Exception): |
generator.throw(exc_type[, exc_value[, traceback]])
:
致使生成器在暂停的 yield 表达式处抛出指定的异常。
如果生成器处理了抛出的异常,代码会继续执行到下一个yield表达式,而产出的值会成为调用generator.throw方法得到的返回值。
如果生成器没有处理抛出的异常,协程会停止,即状态变成’GEN_CLOSED’,异常会向上冒泡,传到调用方的上下文中。
generator.close()
:
致使生成器在暂停的yield表达式处抛出GeneratorExit异常。
如果生成器没有处理这个异常,或者抛出了StopIteration异常(通常是指运行到结尾),调用方不会报错。
如果收到 GeneratorExit 异常,生成器一定不能产出值,否则解释器会抛出 RuntimeError 异常。
生成器抛出的其他异常会向上冒泡, 传给调用方。
让协程返回值
1 | from collections import namedtuple |
异常对象的 value 属性保存着返回的值。
注意,return表达式的值会偷偷传给调用方,赋值给StopIteration异常的一个属性。 这样做有点不合常理。
更合理的获取:
1 | try: |
使用yield from
yield from x 表达式对 x 对象所做的第一件事是, 调用 iter(x), 从中获取迭代器。 因此, x 可以是任何可迭代的对象。
yield from 的主要功能是打开双向通道, 把最外层的调用方与最内层的子生成器连接起来, 这样二者可以直接发送和产出值, 还可以直接传入异常, 而不用在位于中间的协程中添加大量处理异常的样板代码。 有了这个结构, 协程可以把职责委托给子生成器。
1 | def gen(): |
术语说明:
- 委派生成器:包含 yield from
表达式的生成器函数 - 子生成器:从 yield from 表达式中
部分获取的生成器 - 调用方:指代调用委派生成器的客户端代码。 即“客户端”
结构示意图:
代码演示:
1 | from collections import namedtuple |
yield from链:
一个委派生成器使用yield from调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield表达式的简单生成器(或任何可迭代的对象)结束;任何yield from链条都必须由客户驱动,在最外层委派生成器上调用next(…)函数或.send(…)方法。可以隐式调用,例如使用for循环。
yield from 的意义
yield from的特性:
- 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)
- 使用 send() 方法发给委派生成器的值都直接传给子生成器。 如果发送的值是 None, 那么会调用子生成器的
__next__()
方法。 如果发送的值不是 None, 那么会调用子生成器的 send() 方法。 如果调用的方法抛出 StopIteration 异常, 那么委派生成器恢复运行。 任何其他异常都会向上冒泡, 传给委派生成器。 - 生成器退出时, 生成器(或子生成器) 中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。
- yield from 表达式的值是子生成器终止时传给 StopIteration异常的第一个参数。
- 传入委派生成器的异常, 除了 GeneratorExit 之外都传给子生成器的 throw() 方法。 如果调用 throw() 方法时抛出StopIteration 异常, 委派生成器恢复运行。 StopIteration 之外的异常会向上冒泡, 传给委派生成器
- 如果把 GeneratorExit 异常传入委派生成器, 或者在委派生成器上调用 close() 方法, 那么在子生成器上调用 close() 方法, 如果它有的话。 如果调用 close() 方法导致异常抛出, 那么异常会向上冒泡, 传给委派生成器; 否则, 委派生成器抛出GeneratorExit 异常
yeidl from结构的伪代码:
1 | EXPR = 'AB' |
离散事件仿真
离散事件仿真(DES),定义:
出租车队运营仿真,代码示例:
1 | from collections import namedtuple |
事件:
驱动型框架(如 Tornado 和 asyncio) 的运作方式: 在单个线程中使用一个主循环驱动协程执行并发活动。 使用协程做面向事件编程时, 协程会不断把控制权让步给主循环, 激活并向前运行其他协程, 从而执行各个并发活动。
使用期物处理并发
网络下载的三种风格
依序下载的脚本
1 | import os |
使用concurrent.futures模块下载
1 | from concurrent import futures |
期物在哪里
标准库中有两个名为 Future 的类: concurrent.futures.Future 和 asyncio.Future。 这两个类的作用相同: 两个 Future 类的实例都表示可能已经完成或者尚未完成的延迟计算。 这与 Twisted 引擎中的 Deferred 类、 Tornado 框架中的Future 类, 以及多个 JavaScript 库中的 Promise 对象类似。
使用as_completed函数改写download_many函数,来理解期物:
1 | def download_many(cc_list): |
严格来说, 我们目前测试的并发脚本都不能并行下载。 使用concurrent.futures 库实现的两个示例受 GIL(Global InterpreterLock, 全局解释器锁) 的限制, 而 flags_asyncio.py 脚本在单个线程中运行。(GIL几乎对 I/O 密集型处理无害)。
阻塞性I/O和GIL
Python 线程受 GIL的限制, 任何时候都只允许运行一个线程,但 flags_threadpool.py 脚本的下载速度仍比 flags.py 脚本快 5倍。flags_asyncio.py 脚本和 flags.py 脚本都在单个线程中运行, 前者仍比后者快 5 倍。
Python 标准库中的所有阻塞型 I/O 函数都会释放 GIL, 允许其他线程运行。 time.sleep() 函数也会释放 GIL。 因此, 尽管有GIL, Python 线程还是能在 I/O 密集型应用中发挥作用。
CPython 解释器本身就不是线程安全的, 因此有全局解释器锁(GIL) ,一次只允许使用一个线程执行 Python 字节码。 因此, 一个 Python 进程通常不能同时使用多个 CPU 核心。这是 CPython 解释器的局限, 与 Python 语言本身无关。 Jython 和 IronPython 没有这种限制。不过, 目前最快的 Python 解释器 PyPy 也有 GIL。
使用concurrent.futures模块启动进程
在CPU密集型作业中使用concurrent.futures模块轻松绕开GIL,ProcessPoolExecutor 和 ThreadPoolExecutor 类都实现了通用的Executor 接口, 因此使用ProcessPoolExecutor能特别轻松地把基于线程的方案转成基于进程的方案。而在下载国旗的示例或其他I/O密集型作业使用ProcessPoolExecutor类得不到任何好处。
实验Executor.map方法
1 | from time import sleep, strftime |
map的特性:这个函数返回结果的顺序与调用开始的顺序一致,如果后调用的函数早完成则会处于阻塞状态。只能处理参数不同的同一个可调用对象。
executor.submit和futures.as_completed组合更灵活,不管提交顺序,只要有结果就获取。
futures.as_completed能处理的期物集合可以来自多个Executor实例。
线程和多进程的替代方案
如果futures.ThreadPoolExecutor 类对某个作业来说不够灵活, 可能要使用 threading 模块中的组件(如 Thread、 Lock、 Semaphore 等)自行制定方案, 比如说使用 queue 模块创建线程安全的队列, 在线程之间传递数据。 futures.ThreadPoolExecutor 类已经封装了这些组件。
对 CPU 密集型工作来说, 要启动多个进程, 规避 GIL。 创建多个进程最简单的方式是, 使用 futures.ProcessPoolExecutor 类。 不过和前面一样, 如果使用场景较复杂, 需要更高级的工具。 multiprocessing 模块的 API 与threading 模块相仿, 不过作业交给多个进程处理。
使用asyncio包处理并发
线程与协程的对比
spinner_thread.py:
1 | import threading |
spinner_asyncio.py:
1 | import asyncio |
以上两种supervisor实现之间的主要区别:
- asyncio.Task对象差不多与threading.Thread对象等效。
- Task对象用于驱动协程,Thread对象用于调用可调用对象。
- Task对象不由自己动手实例化,而是通过把协程传给asyncio.ensure_future(..)函数或loop.create_task(…)方法获取。
- 获取的Task对象已经排定了运行时间。Thread实例则必须调用start方法。
- 没有API能从外部终止线程。可以使用Task.cancel(),在协程内部抛出CancelledError异常,协程可以在暂停的yield处捕获这个异常,处理终止请求。
- supervisor协程必须在main函数中由loop.run_until_complete方法执行。
协程和线程的同步区别:
对协程来说, 无需保留锁, 在多个线程之间同步操作, 协程自身就会同步, 因为在任意时刻只有一个协程运行。 想交出控制权时, 可以使用 yield 或 yield from 把控制权交还调度程序。
asyncio.Future和concurrent.futures.Future区别
期物只是调度执行某物的结果。 在asyncio 包中,BaseEventLoop.create_task(…)方法接收一个协程, 排定它的运行时间,然后返回一个asyncio.Task实例——也是asyncio.Future类的实例,因为Task是Future的子类,用于包装协程。这与调用Executor.submit(…)方法创建concurrent.futures.Future实例是一个道理。
与concurrent.futures.Future类似,asyncio.Future类也提供了.done()、.add_done_callback(…)和.result() 等方法。
因为asyncio.Future类的目的是与yield from一起使用,所以通常不需要使用以下方法:
- 无需调用my_future.add_done_callback(…),因为可以直接把想在期物运行结束后执行的操作放在协程中 yield from my_future 表达式的后面。 这是协程的一大优势: 协程是可以暂停和恢复的函数。
- 无需调用my_future.result(),因为 yield from 从期物中产出的值就是结果(例如,result = yield from my_future)。
使用asyncio和aiohttp包下载
从Python3.4起,asyncio包只支持TCP和UDP。如果想使用HTTP,可以使用aiohttp包。
1 | import asyncio |
asyncio.wait 会分别把各个协程包装进一个 Task 对象。 最终的结果是, wait 处理的所有对象都通过某种方式变成 Future 类的实例。 wait 是协程函数, 因此返回的是一个协程或生成器对象。
在asyncio包的API中使用yield from时:
避免阻塞型调用
有两种方法能避免阻塞型调用中止整个应用程序的进程:
- 在单独的线程中运行各个阻塞型操作
- 把每个阻塞型操作转换成非阻塞的异步调用使用
多个线程是可以的, 但是各个操作系统线程(Python 使用的是这种线程) 消耗的内存达兆字节(具体的量取决于操作系统种类) 。 如果要处理几千个连接, 而每个连接都使用一个线程的话, 我们负担不起。
asyncio 的基础设施获得第一个响应后, 事件循环把响应发给等待结果的 get_flag 协程。 得到响应后, getflag 向前执行到下一个 yieldfrom 表达式处, 调用 resp.read() 方法, 然后把控制权还给主循环。其他响应会陆续返回(因为请求几乎同时发出) 。 所有 get flag 协程都获得结果后, 委派生成器 download_one 恢复, 保存图像文件。
改进asyncio下载脚本
把一个协程列表传给 asyncio.wait 函数, 经由loop.run_until_complete 方法驱动, 全部协程运行完毕后, 这个函数会返回所有下载结果。 可是, 为了更新进度条, 各个协程运行结束后就要立即获取结果。 为了集成进度条, 我们使用 as_completed 生成器函数;
1 | import asyncio |
从回调到期物和协程
Python中的回调地狱:链式回调
1 | def stage1(response1): |
使用协程和yield from 结构做异步编程,无需回调:
1 |
|
使用yield from 异步编程,每次下载发起多次请求:
1 |
|
使用asyncio包编写服务器
使用asyncio包编写TCP服务器
1 | import sys |
handle_queries 协程的名称是复数, 因为它启动交互式会话后能处理各个客户端发来的多次请求。
代码中所有的 I/O 操作都使用 bytes 格式。 因此, 我们要解码从网络中收到的字符串, 还要编码发出的字符串。Python 3 默认使用的编码是 UTF-8, 这里就隐式使用了这个编码。
run_until_complete 方法的参数是一个协程(start_server方法返回的结果)或一个 Future 对象(server.wait_closed 方法返回的结果),如果传给 run_until_complete 方法的参数是协程, 会把协程包装在 Task 对象中。
调用 loop.run_forever() 时阻塞。控制权流动到事件循环中, 而且一直待在那里。不过偶尔会回到 handle_queries 协程, 这个协程需要等待网络发送或接收数据时, 控制权又交还事件循环。
在事件循环运行期间, 只要有新客户端连接服务器就会启动一个handle_queries 协程实例。
使用aiohttp包编写Web服务器
1 | import sys |
只有驱动协程,协程才能做事,而驱动 asyncio.coroutine 装饰的协程有两种方法:
yield from
传给 asyncio 包中某个参数为协程或期物的函数,例如 run_until_complete。
动态属性和特性
使用动态属性转换数据
使用动态属性访问JSON类数据
下载和加载,osconfeed.py:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from urllib.request import urlopen
import warnings
import os
import json
URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = 'data/osconfeed.json'
def load():
if not os.path.exists(JSON):
msg = 'downloading {} to {}'.format(URL, JSON)
warnings.warn(msg)
with urlopen(URL) as remote, open(JSON, 'wb') as local: # 同时进行下载和保存
local.write(remote.read())
with open(JSON, encoding='utf8') as fp:
return json.load(fp)
动态访问属性:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31from collections import abc
import keyword
class FrozenJSON():
"""一个只读接口,使用属性表示法访问JSON类对象
"""
def __init__(self, mapping):
self.__data = {}
for key, value in mapping.items():
if keyword.iskeyword(key): # 判断键是否为Python关键词,特殊处理
key += '_'
self.__data[key] = value
def __getattr__(self, name):
if hasattr(self.__data, name):
# 如果 name 是实例属性 __data 的属性,返回那个属性,如 keys
return getattr(self.__data, name)
else:
# 否则从 self.__data 中获取 name 键对应的元素, 返回调用FrozenJSON.build() 方法得到的结果。
return FrozenJSON.build(self.__data[name])
def build(cls, obj): # 备选构造函数
if isinstance(obj, abc.Mapping):
return cls(obj)
elif isinstance(obj, abc.MutableSequence):
return [cls.build(item) for item in obj]
else:
return obj
动态访问:
1 | from osconfeed import load |
使用new方法灵活创建对象
__new__
: 这是个类方法(使用特殊方式处理, 因此不必使用 @classmethod 装饰器) , 必须返回一个实例(自身类或其他类的实例,返回其他类的实例解释器不会调用 __init__
方法)。 返回的实例会作为第一个参数(即 self) 传给 __init__
方法。
所以 __init__
方法其实是“初始化方法”。 真正的构造方法是 __new__
。我们几乎不需要自己编写 __new__
方法, 因为从 object 类继承的实现已经足够了。
Python构件对象的过程的伪代码:
1 | # 构建对象的伪代码 |
使用 __new__
方法取代build方法,构建可能是也可能不是FrozenJSON实例的新对象:
1 | def __init__(self, mapping): #__new__构建的“原始”对象,在__init__里进行初始化 |
shelve模块
略
使用特性验证属性
1 | class LineItem(): |
特性全解析
特性会覆盖实例属性
1 | class Class(): |
obj.attr 这样的表达式不会从 obj 开始寻找attr,而是从 obj.__class__
开始。而且, 仅当类中没有名为 attr的特性时, Python 才会在 obj 实例中寻找。
特性工厂
1 | def quantity(storage_name): |
处理属性删除操作
1 | class BlackKnight(): |
处理属性的重要属性和函数
特殊属性
1 | b = BlackKnight() |
__class__
:对象所属类的引用(即 obj.__class__
与 type(obj) 的作用相同) 。Python 的某些特殊方法, 例如 __getattr__
, 只在对象的类中寻找, 而不在实例中寻找。
__dict__
:一个映射, 存储对象或类的可写属性(键值对)。 有 __dict__
属性的对象,任何时候都能随意设置新属性。 如果类有 __slots__
属性, 它的实例可能没有 __dict__
属性。 参见下面对 __slots__
属性的说明。
__slots__
:类可以定义这个这属性, 限制实例能有哪些属性。 __slots__
属性的值是一个字符串组成的元组, 指明允许有的属性。 如果 __slots__
中没有 __dict__
, 那么该类的实例没有 __dict__
属性, 实例只允许有指定名称的属性。
内置函数
dir([object]):列出对象的大多数属性。dir 函数的目的是交互式使用, 因此没有提供完整的属性列表, 只列出一组“重要的”属性名。 dir 函数能审查有或没有 __dict__
属性的对象。 dir 函数不会列出 __dict__
属性本身, 但会列出其中的键。 dir 函数也不会列出类的几个特殊属性, 例如 __mro__
、 __bases__
和 __name__
。 如果没有指定可选的 object 参数, dir 函数会列出当前作用域中的名称。
getattr(object, name[, default]):从 object 对象中获取 name 字符串对应的属性。 获取的属性可能来自对象所属的类或超类。 如果没有指定的属性, getattr 函数抛出AttributeError 异常, 或者返回 default 参数的值(如果设定了这个参数的话) 。
hasattr(object, name):如果 object 对象中存在指定的属性, 或者能以某种方式(例如继承) 通过 object 对象获取指定的属性, 返回 True。 文档说: “这个函数的实现方法是调用 getattr(object, name) 函数, 看看是否抛出AttributeError 异常。 ”
setattr(object, name, value):把 object 对象指定属性的值设为 value, 前提是 object 对象能接受那个值。 这个函数可能会创建一个新属性, 或者覆盖现有的属性。
vars([object]):返回 object 对象的 __dict__
属性; 如果实例所属的类定义了 __slots__
属性, 实例没有 __dict__
属性, 那么 vars 函数不能处理那个实例(相反, dir 函数能处理这样的实例) 。 如果没有指定参数,那么 vars() 函数的作用与 locals() 函数一样: 返回表示本地作用域的字典。
特殊方法
使用点号或内置的 getattr、 hasattr 和 setattr 函数存取属性都会触发下述列表中相应的特殊方法。 但是, 直接通过实例的 __dict__
属性读写属性不会触发这些特殊方法——如果需要, 通常会使用这种方式跳过特殊方法。
要假定特殊方法从类上获取, 即便操作目标是实例也是如此。 因此, 特殊方法不会被同名实例属性遮盖。例如, obj.attr 和 getattr(obj, ‘attr’, 42) 都会触发 Class.__getattribute__(obj, 'attr')
方法。
__delattr__(self, name)
:只要使用 del 语句删除属性, 就会调用这个方法。 例如, del obj.attr 语句触发 Class.__delattr__(obj, 'attr')
方法。
__dir__(self)
:把对象传给 dir 函数时调用, 列出属性。 例如, dir(obj) 触发 Class.__dir__(obj)
方法。
__getattr__(self, name)
:仅当获取指定的属性失败, 搜索过 obj、 Class 和超类之后调用。表达式 obj.no_such_attr、 getattr(obj, ‘no_such_attr’) 和hasattr(obj, ‘no_such_attr’) 可能会触发 Class.__getattr__(obj, 'no_such_attr')
方法, 但是, 仅当在obj、 Class 和超类中找不到指定的属性时才会触发。
__setattr__(self, name, value)
:尝试设置指定的属性时总会调用这个方法。 点号和 setattr 内置函数会触发这个方法。 例如, obj.attr = 42 和 setattr(obj,’attr’, 42) 都会触发 Class.__setattr__(obj, ‘attr’, 42)
方法。
__getattribute__(self, name)
:尝试获取指定的属性时总会调用这个方法, 不过, 寻找的属性是特殊属性或特殊方法时除外。 点号与 getattr 和 hasattr 内置函数会触发这个方法。 调用 __getattribute__
方法且抛出 AttributeError 异常时, 才会调用 __getattr__
方法。 为了在获取 obj 实例的属性时不导致无限递归, __getattribute__
方法的实现要使用 super().__getattribute__(obj, name)
。
属性描述符
描述符是对多个属性运用相同存取逻辑的一种方式。描述符是实现了特定协议的类, 这个协议包括 __get__
、 __set__
和 __delete__
方法。 property 类实现了完整的描述符协议。 通常, 可以只实现部分协议。我们在真实的代码中见到的大多数描述符只实现了 __get__
和 __set__
方法, 还有很多只实现了其中的一个。Django模型的字段就是描述符。
描述符示例:验证属性
把19章的把 quantity 特性工厂函数重构成 Quantity 描述符类。
术语说明:
- 描述符类:实现描述符协议的类
- 托管类:把描述符实例声明为类属性的类
- 描述符实例:描述符类的各个实例, 声明为托管类的类属性
- 托管实例:托管类的实例
- 储存属性:托管实例中存储自身托管属性的属性
- 托管属性:托管类中由描述符实例处理的公开属性, 值存储在储存属性中。 也就是说, 描述符实例和储存属性为托管属性建立了基础
1 | class Quantity(): # 基于协议实现,不用继承 |
自动获取储存属性的名称
1 | class Quantity(): |
重构描述符
1 | import abc |
1 | import lineitem |
覆盖型和非覆盖型描述符对比
1 | def cls_name(obj_or_cls): |
覆盖型描述符:也叫数据描述符或强制描述符,实现 __set__
方法的描述符属于覆盖型描述符, 因为虽然描述符是类属性, 但是实现 __set__
方法的话, 会覆盖对实例属性的赋值操作
1 | obj.over # (<Overriding object>, <Managed object>, <class Managed>) |
没有 __get__
方法的覆盖性描述符:
1 | # 没有实现__get__方法,因此从类中获取描述符实例 <__main__.OverridingNoGet object at 0x000002A88A67B8D0> |
非覆盖型描述符:也叫非数据描述符或遮盖型描述符,没有实现 __set__
方法的描述符是非覆盖型描述符。
1 | obj.non_over # (<NonOverriding object>, <Managed object>, <class Managed>) |
在类中覆盖描述符
不管描述符是不是覆盖型, 为类属性赋值都能覆盖描述符。读类属性的操作可以由依附在托管类上定义有 __get__
方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有 __set__
方法的描述符处理
1 | Managed.over = 1 |
方法是描述符
在类中定义的函数属于绑定方法(bound method) , 因为用户定义的函数都有 __get__
方法, 所以依附到类上时, 就相当于描述符。
方法是非覆盖型描述符。
1 | obj = Managed() |
与描述符一样, 通过托管类访问时, 函数的 __get__
方法会返回自身的引用。 但是, 通过实例访问时, 函数的 __get__
方法返回的是绑定方法对象: 一种可调用的对象, 里面包装着函数, 并把托管实例(例如 obj) 绑定给函数的第一个参数(即 self) , 这与 functools.partial 函数的行为一致。
1 | import collections |
绑定方法对象还有个 __call__
方法, 用于处理真正的调用过程。 这个方法会调用 __func__
属性引用的原始函数, 把函数的第一个参数设为绑定方法的 __self__
属性。
描述符用法建议
- 使用特性以保持简单
内置的 property 类创建的其实是覆盖型描述符, __set__
方法和 __get__
方法都实现了, 即便不定义设值方法也是如此。 特性的 __set__
方法默认抛出 AttributeError: can’t set attribute,因此创建只读属性最简单的方式是使用特性, 这能避免下一条所述的问题。
- 只读描述符必须有
__set__
方法
如果使用描述符类实现只读属性, 要记住, __get__
和 __set__
两个方法必须都定义, 否则, 实例的同名属性会遮盖描述符。 只读属性的 __set__
方法只需抛出 AttributeError 异常, 并提供合适的错误消息。
- 用于验证的描述符可以只有
__set__
方法
对仅用于验证的描述符来说, __set__
方法应该检查 value 参数获得的值, 如果有效, 使用描述符实例的名称为键, 直接在实例的 __dict__
属性中设置。 这样, 从实例中读取同名属性的速度很快, 因为不用经过 __get__
方法处理。
- 仅有
__get__
方法的描述符可以实现高效缓存
如果只编写了 __get__
方法, 那么创建的是非覆盖型描述符。 这种描述符可用于执行某些耗费资源的计算, 然后为实例设置同名属性,缓存结果。 同名实例属性会遮盖描述符, 因此后续访问会直接从实例的 __dict__
属性中获取值, 而不会再触发描述符的 __get__
方法。
- 非特殊的方法可以被实例属性遮盖
由于函数和方法只实现了 __get__
方法, 它们不会处理同名实例属性的赋值操作。 因此, 像 my_obj.the_method = 7 这样简单赋值之后, 后续通过该实例访问 the_method 得到的是数字 7——但是不影响类或其他实例。 然而, 特殊方法不受这个问题的影响。 解释器只会在类中寻找特殊的方法, 也就是说, repr(x) 执行的其实是 x.__class__.__repr__(x)
, 因此 x 的 __repr__
属性对 repr(x) 方法调用没有影响。 出于同样的原因, 实例的 __getattr__
属性不会破坏常规的属性访问规则。
类元编程
类元编程是指在运行时创建或定制类的技艺。元类是类元编程最高级的工具: 使用元类可以创建具有某种特质的全新类种, 例如我们见过的抽象基类。
类工厂函数
1 | class Dog: |
避免编写上述样板代码,我们下面创建一个类工厂函数,即可变对象版本的collections.namedtuple。
1 | def record_factory(cls_name, field_names): |
type 的实例是类。
1 | ty = type('MyClass', (object,), {'x': 1}) |
相当于
1 | class MyClass(): |
定制描述符的类装饰器
类装饰器与函数装饰器非常类似, 是参数为类对象的函数, 返回原来的类或修改后的类。
1 | def entity(cls): # 类装饰器的参数是一个类 |
类装饰器能以较简单的方式做到以前需要使用元类去做的事情——创建类时定制类。
类装饰器有个重大缺点: 只对直接依附的类有效。 这意味着, 被装饰的类的子类可能继承也可能不继承装饰器所做的改动,这个缺点在下面解决。
导入时和运行时比较
evaltime.py
1 | from evalsupport import deco_alpha |
evalsupport.py
1 | print('<[100]> evalsupport module start') |
场景1:import evaltime
1 | import evaltime |
场景2:python3 evaltime.py
1 | py evaltime.py |
元类基础知识
元类是制造类的工厂, 不过不是函数, 而是类。元类是用于构建类的类。
根据 Python 对象模型, 类是对象, 因此类肯定是另外某个类的实例。 默认情况下, Python 中的类是 type 类的实例。 也就是说, type 是大多数内置的类和用户定义的类的元类。
1 | print('spam'.__class__) # <class 'str'> |
type和object的关系
ABCMeta和type的关系
所有类都是 type 的实例, 但是元类还是 type 的子类, 因此可以作为制造类的工厂。 具体来说, 元类可以通过实现 __init__
方法定制实例。 元类的 __init__
方法可以做到类装饰器能做的任何事情, 但是作用更大。
元类计算时间的练习:
1 | from evalsupport import deco_alpha |
场景3:import evaltime_meta
1 | import evaltime_meta |
场景4:py evaltime_meta.py
1 | py evaltime_meta.py |
定制描述符的元类
替代 @entity 装饰器:
1 | class EntityMeta(type): |
元类的特殊方法 prepare
在某些应用中, 可能需要知道类的属性定义的顺序。
type 构造方法及元类的 __new__
和 __init__
方法都会收到要计算的类的定义体, 形式是名称到属性的映射。 然而在默认情况下, 那个映射是字典; 也就是说,元类或类装饰器获得映射时, 属性在类定义体中的顺序已经丢失了。
1 | class EntityMeta(type): |
调用:
1 | for name in LineItem.field_names(): |
__prepare__
:这个特殊方法只在元类中有用, 而且必须声明为类方法(即使用@classmethod 装饰器定义) 。 解释器调用元类的 __new__
方法之前会先调用 __prepare__
方法, 使用类定义体中的属性创建映射。 __prepare__
方法的第一个参数是元类, 随后两个参数分别是要构建的类的名称和基类组成的元组, 返回值必须是映射。 元类构建新类时,__prepare__
方法返回的映射会传给 __new__
方法的最后一个参数, 然后再传给 __init__
方法。
框架和库会使用元类协助程序员执行很多任务, 例如:
- 验证属性
- 一次把装饰器依附到多个方法上
- 序列化对象或转换数据
- 对象关系映射
- 基于对象的持久存储
- 动态转换使用其他语言编写的类结构
类作为对象
Python 数据模型为每个类定义了很多属性,除了 __mro__
、 __class__
和 __name__
属性之外,还有:
cls.__bases__
:由类的基类组成的元组
cls.__qualname__
:其值是类或函数的限定名称, 即从模块的全局作用域到类的点分路径。例如:内部类ClassTwo 的 __qualname__
属性, 其值是字符串 ‘ClassOne.ClassTwo’, 而 __name__
属性的值是 ‘ClassTwo’
cls.__subclasses__()
:包含类的直接子类。 这个方法的实现使用弱引用, 防止在超类和子类(子类在 __bases__
属性中储存指向超类的强引用) 之间出现循环引用。 这个方法返回的列表中是内存里现存的子类
cls.mro()
:超类元组
dir(…) 函数不会列出本节提到的任何一个属性。