写在前面
-
读后感 优点:
-
翻译满昏!绝对满昏!????,你看下面黄色部分,这翻译绝了,感觉我才是文化沙漠,什么“亲者快,仇者痛”,我这辈子没见过这么高级的用法。
-
我被作者举的例子惊到了(见2.4),真的很有水平,翻译和作者本身都很厉害
比如下面这一句
t = (1, 2, [30, 40]) t[2] += [50, 60]这两行代码的执行结果:
- 抛出异常:
因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。 -
t[2]的值发生了修改:t = (1, 2, [30, 40, 50, 60])
- 抛出异常:
-
在讲排序的时候,讲到
sorted和list.sort背后使用的排序算法是Timsort,这个算法的作者是Tim Peters- 这个算法的相关代码在Google对Sun的侵权案中,当作了呈堂证供。
- 这个算法的作者也是
import this,python之禅的作者。 - 我靠,太离谱了,世界线收束,鸡皮疙瘩都起来了。
-
每一章的小结真的写的太好了,方便回顾这一章讲了啥,也方便自己查漏补缺。
-
延伸阅读也是惊艳啊,作者很明显博览群书,基础扎实。
-
作者吹了一波《Python Cookbook(第三版)》和《Python Cookbook(第二版)》,我准备去学习学习!
-
作者每章的小结写的很不错,每次因为知识点需要复查书籍的时候,可以先看对应章节的 本章小结 ,再查。
-
-
读后感 缺点:
-
读到11章和12章的时候,就开始有点吃力了,不知道是不是我的知识储备不够。一些抽象的知识点作者和翻译都有点力不从心(作为读者的角度),就是好像作者想把这个点用比喻的方式说清楚,但是又很难把他心中的理解表达出来,甚至很多地方都是直接进行教条化的描述,对于翻译来说,就更困难了。
【对不起,我面向对象学的太差了呜呜呜】
比如 P540 中:优先使用对象组合,而不是类继承,还有,组合和委托可以代替混入,把行为提供给不同的类,但是不能取代接口继承去定义类型层次结构。
对于 组合、委托、混入、继承等名词的解释不够到位,这几个名词,我就对继承还可以有深入的理解,其他的三个名词出现的时候,一脸懵逼
-
从16章协程开始,我就开始绝望了起来,有点整不明白,咬牙硬吃到18章asyncio的一些知识点的时候,就是懵懵懂懂的,在 yield 和 yield from 中学傻了。在18.5章的时候,不知道为什么突然出现了个semaphore,十分突兀,就开始不知所云了起来。
-
-
传送门:
1. Python数据模型
-
collections.nametuple:用来构建只有少数属性但是没有方法的对象,比如数据库条目。 -
__getitem__:方法可以实现切片效果def __getitem__(self, position): return self._cards[position]
1.1 特殊方法
-
如何使用特殊方法
-
首先,特殊方法的存在是为了被python解析器调用的,而不是被我们调用的
-
其次,
my_object.__len__()这种写法应该修正为len(my_object),在执行len(my_object)的时候,如果my_object是一个自定义类的对象,那么python会自己去调用其中的,由我们自己实现的__len__方法 -
如果是python内置的类型,如列表list、字符串str、字节序列bytearray等,Cpython会抄近道,
__len__实际上会直接返回PyVarObject里的ob_size属性。其中PyVarObject表示内存中长度可变的内置对象的C语言结构体。直接读取这个值比调用一个方法要快很多。
-
很多时候,特殊方法的调用是隐式的,比如
for i in x这个语句,背后其实调用的是iter(x),而这个函数的背后则是x.__iter__()方法。(前提是这个方法在x中被实现了) -
不要想当然的随意添加特殊方法,说不定以后python会用到这个名字。
-
-
repr:能把一个对象用字符串的形式表达出来以便辨认,「字符串表示形式」。__repr__所返回的字符串应该准确、无歧义,并且尽可能表达出如何用代码创建出这个被打印的对象。__repr__和__str__的区别在于,后者是在str()函数被使用,或者是在用print函数打印一个对象的时候才能被调用的,并且它返回的字符串对终端用户更友好。如果你只想实现这两个特殊方法中的一个,
__repr__是更好的选择,因为如果一个对象没有__str__函数,而 Python 又需要调用它的时候,解释器会用__repr__作为替代。 -
bool(x)的背后是调用x.__bool__()的结果;如果不存在__bool__方法,那么bool(x)会尝试调用x.__len__()。若返回 0,则 bool 会返回 False;否则返回True。 -
跟运算符无关的特殊方法
- 字符串/字节序列表示形式:
__repr__、__str__、__format__、__bytes__ - 数值转换:
__abs__、__bool__、__complex__、__int__、__float__、__hash__、__index__ - 集合模拟:
__len__、__getitem__、__setitem__、__delitem__、__contains__ - 迭代枚举:
__iter__、__reversed__、__next__ - 可调用模拟:
__call__ - 上下文管理器:
__enter__、__exit__ - 实例创建和销毁:
__new__、__init__、__del__ - 属性管理:
__getattr__、__getattribute__、__setattr__、__delattr__、__dir__ - 属性描述符:
__get__、__set__、__delete__ - 跟类相关的服务:
__prepare__、__instancecheck__、__subclasscheck__
- 字符串/字节序列表示形式:
-
跟运算符相关的特殊方pos法
- 一元运算符:
__neg__(-)、__pos__(+)、__abs__(abs()) - 众多比较运算符:
__lt__(<)、__le__(<=)、__eq__(==)、__ne__(!=)、__gt__(>)、__ge__(>=) - 算数运算符:
__add__(+)、__sub__(-)、__mul__(*)、__truediv__(/)、__floordiv__(//)、__mod__(%)、__divmod__(divmod())、__pow__(**或pow())、__round__(round()) - 反向算数运算符:
__radd__、__rsub__、__rmul__、__rtruediv__、__rfloordiv__、__rmod__、__rdivmod__ - 增量赋值算数运算符:
__iadd__、__isub__、__imul__、__itruediv__、__ifloordiv__、__imod__、__ipow__ - 位运算符:
__invert__(~)、__lshift__(<<)、__rshift__(>>)、__and__(&)、__or__(|)、__xor__(^) - 反向位运算符:
__rlshift__、__rrshift__、__rand__、__rxor__、__ror__ - 增量赋值位运算符:
__ilshift__、__irshift__、__iand__、__ixor__、__ior__
- 一元运算符:
-
为什么len不是一个普通方法:「实用胜于纯粹」,如果 x 是一个内置类型的实例,那么
len(x)的速度会非常快。背后的原因是 CPython 会直接从一个 C 结构体里读取对象的长度,完全不会调用任何方法。获取一个集合中元素的数量是一个很常见的操作,在str、list、memoryview等类型上,这个操作必须高效。
换句话说,len 之所以不是一个普通方法,是为了让 Python 自带的数据结构可以走后门,abs 也是同理。但是多亏了它是特殊方法,我们也可以把 len 用于自定义数据类型
2. 序列构成的数组
2.1 内置序列类型
-
容器序列:list、tuple、collections.deque。可以存放不同类型的数据。
-
扁平序列:str、bytes、bytearray、memoryview、array.array。只能容纳一种类型。
-
容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。
-
可变序列:list、bytearray、array.array、collections.deque、memoryview
-
不可变序列:tuple、str、bytes
-
可变序列(MutableSequence)和不可变序列(Sequence)的差异
2.2 列表推导和生成器表达式
-
Python 会忽略代码里 []、{} 和 () 中的换行,因此如果你的代码里有多行的列表、列表推导、生成器表达式、字典这一类的,可以省略不太好看的续行符 \。
-
列表推导不会再有变量泄漏的问题
x = 'ABC' dummy = [ord(x) for x in x] print(x, dummy) # ABC [65, 66, 67] -
生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。
2.3 元组不仅仅是不可变的列表
-
除了用作不可变的列表,它还可以用于没有字段名的记录。
-
元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。正是这个位置信息给数据赋予了意义。
-
在元组拆包中使用
*也可以帮助我们把注意力集中在元组的部分元素上。用*来处理剩下的元素a, b, *rest = range(5) print(a, b, rest) # 0 1 [2, 3, 4] -
在平行赋值中,
*前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置a, *body, c, d = range(5) print(a, body, c, d) # 0 [1, 2] 3 4 -
collections.namedtuple:创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串。-
_fields:一个包含这个类所有字段名称的元组。 -
_make():通过接受一个可迭代对象来生成这个类的一个实例,它的作用等价于类(*参数元组)是一样的。 -
_asdict():把具名元组以collections.OrderedDict的形式返回,我们可以利用它来把元组里的信息友好地呈现出来。
-
-
方法和属性 列表 元组 数组 双向队列 描述 s.__add__(s2)√ √ √ × s+s2,拼接s.__iadd__(s2)√ × √ √ s+=s2,就地拼接s.append(e)√ × √ √ 在尾部添加一个新元素 s.appendleft(e)× × × √ 添加一个元素到最左侧(到第一个元素之前) s.byteswap× × √ × 翻转数组内每个元素的字节序列,转换字节序列 s.clear()√ × × √ 删除所有元素 s.__contains__(e)√ √ √ × s是否包含e s.copy()√ × × × 列表的浅复制 s.__copy__()× × √ √ 对 copy.copy(浅复制)的支持s.count(e)√ √ √ √ e在s中出现的次数 s.__deepcopy__()× × √ × 对 copy.deepcopy(深复制)的支持s.__delitem__(p)√ × √ √ 把位于p的元素删除 s.extend(it)√ × √ √ 把可迭代对象it追加给s s.extendleft(i)× × × √ 将可迭代对象i中的元素添加到头部 s.frombytes(b)× × √ × 将压缩成机器值得字节序列读出来添加到尾部 s.fromfile(f,n)× × √ × 将二进制文件f内含有机器值读出来添加到尾部,最多添加n项 s.fromlist(l)× × √ × 将列表里的元素添加到尾部,如果其中任何一个元素导致了 TypeError异常,那么所有的添加都会取消s.__getitem__()√ √ √ √ s[p],获取位置p的元素s.__getnewargs__()× √ × × 在pickle中支持更加优化的序列化 s.index(e)√ √ √ × 在s中找到元素e第一次出现的位置 s.insert(p,e)√ × √ × 在位置p之前插入元素e s.itemsize× × √ × 数组中每个元素的长度是几个字节 s.__iter__()√ √ √ √ 获取s的迭代器 s.__len__()√ √ √ √ len(s),元素的数量s.__mul__()√ √ √ × s*n,n个s的重复拼接s.__imul__()√ × √ × s*=n,就地重复拼接s.__rmul__()√ √ √ × n*s,反向拼接*s.pop([p])√ × √ √ 删除最后或者是(可选的)位于p的元素,并返回它的值(注意,在双向队列中不接受参数) s.popleft()× × × √ 移除第一个元素并返回它的值 s.remove(e)√ × √ √ 删除s中第一次出现的e s.reverse()√ × √ √ 就地把s的元素倒序排列 s.__reversed__√ × × √ 返回s的倒序迭代器 s.rotate(n)× × × √ 把n个元素从队列的一段移到另一端 s.__setitem__(p,e)√ × √ √ s[p]=e,把元素e放在位置p,替代已经在那个位置的元素s.sort([key],[reverse])√ × × × 就地对s中的元素进行排序,可选的参数有键(key)和是否倒序(reverse) s.tobytes()× × √ × 把所有元素的机器值用bytes对象的形式返回 s.tofile(f)× × √ × 把所有元素以机器值的形式写入一个文件 s.tolist()× × √ × 把数组转换成列表,列表里的元素类型是数字对象 s.typecode× × √ × 返回只有一个字符的字符串,代表数组元素在C语言中的类型
2.4 切片
-
为什么切片和区间会忽略最后一个元素:
在切片和区间操作里不包含区间范围的最后一个元素是 Python 的风格,这个习惯符合 Python、C 和其他语言里以 0 作为起始下标的传统。这样做带来的好处如下。
- 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:
range(3)和my_list[:3]都返回 3 个元素。 - 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
- 这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成
my_list[:x]和my_list[x:]就可以了。
- 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:
-
slice(a, b, c):对seq[start:stop:step]进行求值的时候,Python 会调用seq.__getitem__(slice(start, stop, step))。 -
slice(start, stop, step):使用方法a = slice(6, 40) item[a] -
多维切片:如果要得到
a[i, j]的值,Python 会调用a.__getitem__((i, j)) -
x[i, ...]:x[i, :, :, :]的缩写 -
给切片赋值:如果把切片放在赋值语句的左边,或把它作为
del操作的对象,我们就可以对序列进行 嫁接 、 切除 或 就地修改 操作。- 如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。
- 即便只有单独一个值,也要把它转换成可迭代的序列。
2.5 增量赋值
-
+和*的陷阱:如果要生成二维序列:- 不能:
[['_']*3]*3 - 而要:
[['_']*3 for i in range(3)]
- 不能:
-
a += b- 如果a实现了
__iadd__方法,就相当于调用了a.extend(b) - 如果a没有实现
__iadd__的话,a += b就跟a = a + b一样了。首先计算a + b,得到一个新的对象,然后赋值给a。 - 也就是说,在这个表达式中,变量名会不会被关联到新的对象,完全取决于这个类型有没有实现
__iadd__方法。
- 如果a实现了
-
对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先赋值到新的对象里,然后再追加新的元素。
str是一个例外,因为对字符串做+=实在是太普遍了,所以CPython对它做了优化,为str初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。
2.6 排序
-
Python中的排序算法:Timesort 是稳定的,意思是就算两个元素比不出大小,在每次排序的结果里他们的相对位置是固定的。
-
list.sort:就地排序列表,也就是说不会把原列表复制一份,返回值为None -
sorted:会新建一个列表作为返回值 -
key参数能让你对一个混有数字字符和数值的列表进行排序。l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19] sorted(l, key=int) # [0, '1', 5, 6, '9', 14, 19, '23', 28, '28'] -
sorted和list.sort背后的排序算法是 Timsort,它是一种自适应算法,会根据原始数据的顺序特点交替使用插入排序和归并排序,以达到最佳效率。这样的算法被证明是很有效的,因为来自真实世界的数据通常是有一定的顺序特点的。 -
用
bisect来管理 已排序 的序列:二分法-
bisect函数其实是bisect_right函数的别名,后者还有个姊妹函数叫bisect_left -
bisect_left返回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会被放置于它相等的元素的前面 -
bisect_right返回的则是跟它相等的元素之后的位置
-
-
利用
bisect进行评价分组def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'): i = bisect.bisect(breakpoints, score) return grades[i] [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]] # ['F', 'A', 'C', 'C', 'B', 'A', 'A'] -
bisect.insort(seq, item)把变量item插入到序列seq中,并能保持seq的升序顺序。
2.7 数组、内存视图、NumPy和队列
- 如果我们需要一个只包含数字的列表,那么
array.array比list更高效。通过array.tofile和array.fromfile进行文件的保存和读取。 -
memoryview:是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。 -
memoryview.cast的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。memoryview.cast会把同一块内存里的内容打包成一个全新的memoryview对象给你。 -
numpy:- 将一维数组转化为二维:
array.shape=(x, y) - 将数组转置:
array.T、array.transpose()
- 将一维数组转化为二维:
-
collections.deque类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。 -
dq = deque(range(10), maxlen=10),maxlen是一个可选参数,带别找个队列可以容纳的元素的数量。 -
dq.rotate(n):队列的旋转操作接受一个参数n,当n>0时,队列的最右边的n个元素会被移动到队列的左边。当n<0时,最左边的n个元素会被移动到右边。 -
append和popleft都是原子操作,也就说是 deque 可以在多线程程序中安全地当作先进先出的栈使用,而使用者不需要担心资源锁的问题。 - 其他队列的实现:
-
queue提供了Queue、LifoQueue、PriorityQueue。在满员的时候,这些类不会扔掉旧的元素来腾出位置。相反,如果队列满了,它就会被锁住,直到另外的线程移除了某个元素而腾出了位置。这一特性让这些类很适合用来控制活跃线程的数量。 -
multiprocessing实现了自己的Queue,跟queue.Queue相似,是涉及给进程间通信用的。multiprocessing.JoinableQueue可以让任务管理变得更方便。 -
asyncio里面有Queue、LifoQueue、PriorityQueue、JoinableQueue,这些类受到queue和multiprocessing模块的影响,但是为异步编程里的任务管理提供了专门的便利。 -
heapq没有队列类,而是提供了heappush和heappop方法,让用户可以把可变序列当作堆队列或者优先队列来使用。
-
- 列表倾向于存放有通用特性的元素;元组则恰恰相反,经常用来存放不同类型的元素。
3. 字典和集合
3.1 泛映射类型和字典推导
-
collections.abc模块中有Mapping和MutableMapping这两个抽象基类,它们的作用是为 dict 和其他类似的类型定义形式接口 -
可散列的数据类型(hashable)
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现
__hash__()方法。另外可散列对象还要有__eq__()方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的。简单来说,如果一个对象是可散列的数据类型的话,那它应是不可变的。
-
list等可变对象是不可散列的,因为随着数据的改变他们的哈希值会变化导致进入错误的哈希表。
-
元组的话,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的。
-
一般用户自定义的类型的对象都是可散列的,散列值就是它们的
id()函数的返回值,所以所有这些对象在比较的时候都是不相等的。如果一个对象实现了__eq__()方法,并且在方法中用到了这个对象的内部状态的话,那么只有当所有这些内部状态都是不可变的情况下,这个对象才是可散列的。 -
Python 里所有的不可变类型都是可散列的,这个说法其实是不准确的,比如虽然元组本身是不可变序列,它里面的元素可能是其他可变类型的引用。 -
字典推导:
{code: country.upper() for country, code in country_code.items() if code < 66}
3.2 常见的映射方法
dict、collections.defaultdict和collections.OrderedDict的方法列表
| 方法 | dict | defaultdict | OrderedDIct | 描述 |
|---|---|---|---|---|
d.clear() |
√ | √ | √ | 移除所有元素 |
d.__contains__(k) |
√ | √ | √ | 检查k是否在d中 |
d.copy() |
√ | √ | √ | 浅复制 |
d.__copy()__ |
× | √ | × | 用于支持copy.copy
|
d.default_factory |
× | √ | × | 在__missing__函数中被调用的函数,用以给未找到的元素设置值 |
d.__delitem__(k) |
√ | √ | √ |
del d[k],移除键位k的元素 |
d.fromkeys(it, [initial]) |
√ | √ | √ | 将迭代器it里的元素设置为映射里的键,如果initial参数,就把它作为这些键对应的值(默认是None) |
d.get(k, [default]) |
√ | √ | √ | 返回键k对应的值,如果字典里没有键k,则返回None或者default |
d.__getitem__(k) |
√ | √ | √ | 让字典d能用d[k]的形式返回键k对应的值 |
d.items() |
√ | √ | √ | 返回d里所有的键值对 |
d.__iter__() |
√ | √ | √ | 获取键的迭代器 |
d.keys() |
√ | √ | √ | 获取所有的键 |
d.__len__() |
√ | √ | √ | 可以用len(d)的形式得到字典里键值对的数量 |
d.__missing__(k) |
× | √ | × | 当__getitem__找不到对应键的时候,这个方法会被调用 |
d.move_to_end(k, [last]) |
× | × | √ | 把键位k的元素移动到最靠前或者最靠后的位置(last的默认值是True) |
d.pop(k, [default]) |
√ | √ | √ | 返回键k所对应的值,然后移除这个键值对。如果没有这个键,返回None或者default |
d.popitem() |
√ | √ | √ | 随机返回一个键值对并从字典里移除它 |
d.__reversed__() |
× | × | √ | 返回倒序的键的迭代器 |
d.setdefault(k, [default]) |
√ | √ | √ | 若字典里有键k,则把它对应的值设置位default,然后返回这个值;若无,则让d[k]=default,然后返回default |
d.__setitem__ |
√ | √ | √ | 实现d[k]=v操作,把k对应的值设为v |
d.update(m, [**kwargs]) |
√ | √ | √ | m可以是映射或者键值对迭代器,用来更新d里对应的条目 |
d.values |
√ | √ | √ | 返回字典里的所有值 |
-
用setdefault处理找不到的键
-
使用
d.get(k, default)来代替d[k],可以防止报错 -
字典处理优化
-
好的方法
my_dict.setdefault(key, []).append(new_value) -
差的方法
if key not in my_dict: my_dict[key] = [] my_dict[key].append(new_value) -
二者的效果是一样的,只不过后者至少要进行两次键查询——如果键不存在的话,就是三次,用
setdefault只需要一次就可以完成整个操作。
-
-
3.3 映射的弹性键查询
-
场景:有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的时候能得到一个默认值。
-
方法:
-
collections.defaultdict类- 把list构造方法作为
default_factory来创建一个defaultdict - 如果在创建
defaultdict的时候没有指定default_factory,查询不存在的键会触发KeyError -
defaultdict里的default_factory只会在__getitem__里被调用,在其他的方法里完全不会发挥作用。比如dd[k]会创建默认值并返回该默认值,dd.get(k)就会返回None - 所有这一切的背后是基于特殊方法
__missing__实现的,它会在defaultdict遇到找不到的键的时候调用default_factory,而实际上这个特性是所有映射类型都可以选择去支持的
"""创建一个从单词到其出现情况的映射""" import sys import re from collections import defaultdict WORD_RE = re.compile(r'\w+') # index = {} index = defaultdict(list) with open(sys.argv[1], encoding='utf-8') as fp: for line_no, line in enumerate(fp, 1): for match in WORD_RE.finditer(line): word = match.group() column_no = match.start() + 1 location = (line_no, column_no) ''' 这其实是一种很不好的实现,这样写只是为了证明论点 occurrences = index.get(word, []) occurrences.append(location) index[word] = occurrences ''' ''' 下面是好的实现,用到了setdefault函数 index.setdefault(word, []).append(location) ''' ''' 通过collections.defaultdict实现,取得key没有的时候,新建这个key,并赋予默认值''' index[word].append(location) # 以字母顺序打印出结果 for word in sorted(index, key=str.upper): print(word, index[word]) - 把list构造方法作为
-
自定义一个
dict子类,然后在子类中实现__missing__方法-
像
k in my_dict.keys()这种操作在Python3中是很快的,而且即便映射类型对象很庞大也没关系。这是因为dict.keys()的返回值是一个视图。 -
视图就像一个集合,而且跟字典类似的是,在视图里查找一个元素的速度很快。
class StrKeyDict0(dict): def __missing__(self, key): if isinstance(key, str): raise KeyError(key) return self[str(key)] def get(self, key, default=None): try: return self[key] except KeyError: return default def __contains__(self, key): return key in self.keys() or str(key) in self.keys()
-
-
3.4 字典的变种
-
collections.OrderedDict:这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict的popitem方法默认删除并返回的是字典里的最后一个元素,但是如果像my_odict.popitem(last=False)这样调用它,那么它删除并返回第一个被添加进去的元素。 -
collections.ChainMap:该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。这个功能在给有嵌套作用域的语言做解释器的时候很有用,可以用一个映射对象来代表一个作用域的上下文。 -
collections.Counter:这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用——多重集合就是集合里的元素可以出现不止一次。 -
my_dict.most_common(n):统计字典中出现次数最多的数据ct = Counter({'a': 10, 'b': 2, 'r': 2, 'c': 1, 'd': 1, 'z': 3}) ct.most_common(2) # [('a', 10), ('z', 3)] -
colllections.UserDict:这个类其实就是把标准dict用纯 Python 又实现了一遍。跟OrderedDict、ChainMap和Counter这些开箱即用的类型不同,UserDict是让用户继承写子类的。- 更倾向于从
UserDict而不是从dict继承的主要原因是,后者有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是 UserDict 就不会带来这些问题。
- 更倾向于从
-
不可变映射类型:
types.MappingProxyType。如果给这个类一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。这意味着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原映射做出修改。
3.5 集合
-
集合可以去重
-
集合中的元素必须是可散列的,set 类型本身是不可散列的,但是frozenset 可以。因此可以创建一个包含不同 frozenset 的 set。
-
求交集:
&,intersection -
空集:
set(),而不是{},因为{}是一个空字典 -
除了空集,集合的字符串表示形式总是以
{...}的形式出现。 -
{1, 2, 3}的速度快于set([1, 2, 3]),因为后者的话,Python必须先从set这个名字来查询构造方法,然后新建一个列表,最后再把这个列表传入到构造方法里。但是如果是像{1, 2, 3这样的字面量,Python 会利用一个专门的叫作 BUILD_SET 的字节码来创建集合。 -
MutableSet和它的超类的UML类图
-
集合的数学运算:这些方法或者会生成新集合,或者会在条件允许的情况下就地修改集合
数学符号 Python运算符 方法 描述 $S \cap Z$ s&zs.__and__(z)s和z的交集 $S \cap Z$ z&ss.__rand__(z)反向&操作 $S \cap Z$ z&ss.intersection(it, ...)把可迭代的it和其他所有参数转化为集合,然后求它们与s的交集 $S \cap Z$ s&=zs.__iand__(z)把s更新为s和z的交集 $S \cap Z$ s&=zs.intersection_update(it, ...)把可迭代的it和其他所有参数转化为集合,然后求得它们与s的交集,然后把s更新成这个交集 $S \cup Z$ `s z` s.__or__(z)$S \cup Z$ `z s` s.__ror__(z)$S \cup Z$ `z s` s.union(it, ...)$S \cup Z$ `s =z` s.__ior__(z)$S \cup Z$ `s =z` s.update(it, ...)$S \setminus Z$ s-zs.__sub__(z)s和z的差集,或者叫作相对补集 $S \setminus Z$ z-ss.__rsub__(z)-的反向操作 $S \setminus Z$ z-ss.difference(it, ...)把可迭代的it和其他所有参数转化为集合,然后求它们和s的差集 $S \setminus Z$ s-=zs.__isub__(z)把s更新为它与z的差集 $S \setminus Z$ s-=zs.difference_update(it, ...)把可迭代的it和其他所有参数转化为集合,求它们和s的差集,然后把s更新成这个差集 $S \setminus Z$ s-=zs.symmetric_difference(it)求s和set(it)的对称差集 $S \bigoplus Z$ s^zs.__xor__(z)求s和z的对称差集 $S \bigoplus Z$ z^ss.__rxor__(z)^的反向操作 $S \bigoplus Z$ z^ss.symmetric_difference_update(it, ...)把可迭代的it和其他所有参数转化为集合,然后求它们和s的对称差集,最后把s更新成该结果 $S \bigoplus Z$ s^=zs.__ixor__(z)把s更新成它与z的对称差集 -
集合的比较运算符,返回值是布尔类型
数学符号 Python运算符 方法 描述 s.isdisjoint(z)查看s和z是否不相交(没有共同元素) $e \in S$ e in ss.__contains__(e)元素e是否属于s $S \subseteq Z$ s <= zs.__le__(z)s是否为z的子集 $S \subseteq Z$ s <= zs.issubset(it)把可迭代的it转化为集合,然后查看s是否为它的子集 $S \subset Z$ s < zs.__lt__(z)s是否为z的真子集 $S \supseteq Z$ s >= zs.__ge__(z)s是否为z的父集 $S \supseteq Z$ s >= zs.issuperset(it)把可迭代的it转化为集合,然后查看s是否为它的父集 $S \supset Z$ s > zs.__gt__(z)s是否为z的真父集 -
集合类型的其他方法
方法 set frozenset 描述 s.add(e)√ × 把元素e添加到s中 s.clear()√ × 移除掉s中的所有元素 s.copy()√ √ 对s浅复制 s.discard(e)√ × 如果s里有e这个元素的话,把它移除 s.__iter__()√ √ 返回s的迭代器 s.__len__()√ √ len(s) s.pop()√ × 从s中移除一个元素并返回它的值,若s为空,则抛出KeyError异常 s.remove(e)√ × 从s中移除e元素,若e元素不存在,则抛出KeyError异常
3.6 dict和set的背后
-
如果两个对象在比较的时候相等,那么它们的散列值必须相等。
-
散列函数用于将键值经过处理后转化为散列值。具有以下特性:
- 散列函数计算得到的散列值是非负整数
- 如果 key1 == key2,则 hash(key1) == hash(key2)
- 如果 key1 != key2,则 hash(key1) != hash(key2)
-
散列冲突:简单来说,指的是 key1 != key2 的情况下,通过散列函数处理,hash(key1) == hash(key2),这个时候,我们说发生了散列冲突。设计再好的散列函数也无法避免散列冲突,原因是散列值是非负整数,总量是有限的,但是现实世界中要处理的键值是无限的,将无限的数据映射到有限的集合,肯定避免不了冲突。
-
从字典中取值的算法流程图
-
用元组取代字典就能节省空间的原因有两个:
- 是避免了散列表所耗费的空间
- 无需把记录中字段的名字在每个元素里都存一遍
-
在用户自定义的类型中,
__slots__属性可以改变实例属性的存储方式,由dict变成tuple -
使用散列表给dict带来的优势和限制都有哪些
-
键必须是可散列的
-
字典在内存上的开销巨大
-
键查询很快
-
键的次序取决于添加顺序
-
往字典里添加新键可能会改变已有键的顺序
无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。
-
-
集合的特点:
- 集合里的元素必须是可散列的
- 集合很消耗内存
- 可以很高效地判断元素是否存在于某个集合
- 元素的次序取决于被添加到集合里的次序
- 往集合里添加元素,可能会改变集合里已有元素的次序
4. 文本和字节序列
4.1 字符和字节
-
把码位转换成字节序列的过程是编码;把字节序列转换成码位的过程是解码。
-
把字节序列变成人类可读的文本字符串就是解码(
decode),而把字符串变成用于存储或传输的字节序列就是编码(encode)。 -
bytes对象可以从str对象使用给定的编码构造,各个元素是range(256)内的整数。bytes对象的切片还是bytes对象,即使是只有一个字符的切片。 -
bytearray对象没有字面量句法,而是以bytearray()和字节序列字面量参数的形式显示。bytearray对象的切片还是bytearray对象。 -
这里比较特殊,因为
my_bytes[0]获取的是一个整数,而my_bytes[:1]返回的是一个长度为1的bytes对象。 -
虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有ASCII 文本。因此,各个字节的值可能会使用下列三种不同的方式显示。
- 可打印的 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身。
- 制表符、换行符、回车符和
\对应的字节,使用转义序列\t、\n、\r和\\。 - 其他字节的值,使用十六进制转义序列(例如,
\x00是空字节)。
-
二进制序列有个类方法是
str没有的,名为fromhex,它的作用是解析十六进制数字对(数字对之间的空格是可选的),构建二进制序列:bytes.fromhex('31 4B CE A9') # b'1K\xce\xa9' -
使用缓冲类对象构建二进制序列是一种低层操作,可能涉及类型转换:
import array
numbers = array.array('h', [-2, -1, 0, 1, 2])
octets = bytes(numbers)
# b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'
-
使用缓冲类对象创建
bytes或bytearray对象时,始终复制源对象中的字节序列。与之相反,memoryview对象允许在二进制数据结构之间共享内存。 -
memoryview对象的切片是一个新memoryview对象,而且不会
复制字节序列 -
如果使用
mmap模块把图像打开为内存映射文件,那么会复制少量字节
4.2 编码和解码
-
某些编码(如
ASCII和多字节的GB2312)不能表示所有Unicode字符 -
UTF 编码的设计目的就是处理每一个
Unicode码位 -
典型编码:
- latin1(即 iso8859_1):一种重要的编码,是其他编码的基础,例如 cp1252 和Unicode(注意,latin1 与 cp1252 的字节值是一样的,甚至连码位也相同)。
- cp1252:Microsoft 制定的 latin1 超集,添加了有用的符号,例如弯引号和€(欧元);有些 Windows 应用把它称为“ANSI”,但它并不是 ANSI 标准。
- cp437:IBM PC 最初的字符集,包含框图符号。与后来出现的 latin1 不兼容。
- gb2312:用于编码简体中文的陈旧标准;这是亚洲语言中使用较广泛的多字节编码之一。
- utf-8:目前 Web 中最常见的 8 位编码;与 ASCII 兼容(纯 ASCII 文本是有效的 UTF-8 文本)。
- utf-16le:UTF-16 的 16 位编码方案的一种形式;所有 UTF-16 支持通过转义序列(称为“代理对”,surrogate pair)表示超过 U+FFFF 的码位。
-
UnicodeEncodeError:多数非 UTF 编解码器只能处理 Unicode 字符的一小部分子集。把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出
UnicodeEncodeError 异常,除非把 errors 参数传给编码方法或函数,对错误进行特殊处理。无法编码时:
-
error='ignore'处理方式悄无声息地跳过无法编码的字符;这样做通常很是不妥。 - 编码时指定
error='replace',把无法编码的字符替换成 '?';数据损坏了,但是用户知道出了问题。 -
error='xmlcharrefreplace'把无法编码的字符替换成 XML 实体。 - 编解码器的错误处理方式是可扩展的。你可以为 errors 参数注册额外的字符串,方法是把一个名称和一个错误处理函数传给
codecs.register_error函数。
-
-
UnicodeDecodeError:把二进制序列转换成文本时,遇到无法转换的字节序列时会抛出UnicodeDecodeError;另一方面,很多陈旧的 8 位编码——如 'cp1252'、'iso8859_1' 和'koi8_r'——能解码任何字节序列流而不抛出错误,例如随机噪声。因此,如果程序使用错误的 8 位编码,解码过程悄无声息,而得到的是无用输出。 -
乱码字符称为鬼符(gremlin)或 mojibake(文字化け,“变形文本”的日文)。
-
使用预期之外的编码加载模块时抛出的SyntaxError
- Python 3 为所有平台设置的默认编码都是 UTF-8
- Python 3 允许在源码中使用非 ASCII 标识符
-
Chardet:识别所支持的30中编码。解决问题:如何找出字节序列的编码? -
二进制序列编码文本通常不会明确指明自己的编码,但是 UTF 格式可以在文本内容的开头添加一个字节序标记。
-
BOM:字节序标记,byte-order-mark,指明编码时使用 Intel CPU 的小字节序
-
在小字节序设备中,各个码位的最低有效字节在前面:字母 'E' 的码位是 U+0045(十进制数 69),在字节偏移的第 2 位和第 3 位编码为 69 和0。
-
在大字节序 CPU 中,编码顺序是相反的;'E' 编码为 0 和 69。
-
为了避免混淆,UTF-16 编码在要编码的文本前面加上特殊的不可见字符 ZERO WIDTH NO-BREAK SPACE(U+FEFF)。在小字节序系统中,这个字符编码为 b'\xff\xfe'(十进制数 255, 254)。因为按照设计,U+FFFE 字符不存在,在小字节序编码中,字节序列 b'\xff\xfe' 必定是 ZERO WIDTH NO-BREAK SPACE,所以编解码器知道该用哪个字节
序。 -
UTF-16 有两个变种:UTF-16LE,显式指明使用小字节序;UTF-16BE,显式指明使用大字节序。如果使用这两个变种,不会生成 BOM。
-
与字节序有关的问题只对一个字(word)占多个字节的编码(如 UTF-16 和 UTF-32)有影响。UTF-8 的一大优势是,不管设备使用哪种字节序,生成的字节序列始终一致,因此不需要 BOM。
-
尽管如此,某些Windows 应用(尤其是 Notepad)依然会在 UTF-8 编码的文件中添加
BOM;而且,Excel 会根据有没有 BOM 确定文件是不是 UTF-8 编码,否则,它假设内容使用 Windows 代码页(codepage)编码。UTF-8 编码的 U+FEFF 字符是一个三字节序列:b'\xef\xbb\xbf'。因此,如果文件以这三个字节开头,有可能是带有 BOM 的 UTF-8 文件。然而,Python 不会因为文件以 b'\xef\xbb\xbf' 开头就自动假定它是 UTF-8编码的。
4.3 处理文本文件
-
处理文本的最佳实践是 Unicode三明治 。
- 要尽早把输入(例如读取文件时)的字节序列解码成字符串。
- 在程序的业务逻辑中只能处理字符串对象。在其他处理过程中,一定不能编码或解码。
- 对输出来说,则要尽量晚地把字符串编码成字节序列。
-
如果打开文件是为了写入,但是没有指定编码参数,会使用区域设置中的默认编码,而且使用那个编码也能正确读取文件。
-
如果脚本要生成文件,而字节的内容取决于平台或同一平台中的区域设置,那么就可能导致兼容问题。
-
探索编码默认值
import sys, locale expressions = """ locale.getpreferredencoding() type(my_file) my_file.encoding sys.stdout.isatty() sys.stdout.encoding sys.stdin.isatty() sys.stdin.encoding sys.stderr.isatty() sys.stderr.encoding sys.getdefaultencoding() sys.getfilesystemencoding() """ my_file = open('dummy', 'w') for expression in expressions.split(): value = eval(expression) print(expression.rjust(30), '->', repr(value))输出:
locale.getpreferredencoding() -> 'cp936' type(my_file) -> <class '_io.TextIOWrapper'> my_file.encoding -> 'cp936' sys.stdout.isatty() -> False sys.stdout.encoding -> 'UTF-8' sys.stdin.isatty() -> False sys.stdin.encoding -> 'cp936' sys.stderr.isatty() -> False sys.stderr.encoding -> 'UTF-8' sys.getdefaultencoding() -> 'utf-8' sys.getfilesystemencoding() -> 'utf-8' -
如果打开文件时没有指定
encoding参数,默认值由locale.getpreferredencoding()提供 -
如果设定了
PYTHONIOENCODING环境变量,sys.stdout/stdin/stderr的编码使用设定的值;否则,继承自所在的控制台;如果输入/输出重定向到文件,则由locale.getpreferredencoding()定义 -
Python在二进制数据和字符串之间转换时,内部使用
sys.getdefaultencoding()获得的编码;Python3很少如此,但仍有发生。这个设置不能修改。 -
sys.getfilesystemencoding()用于编解码文件名(不是文件内容)。把字符串参数作为文件名传给open()函数时就会使用它;如果传入的文件名参数是字节序列,那就不经改动直接传给 OS API。
4.4 规范化Unicode字符串
-
因为 Unicode 有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。
s1 = 'café' s2 = 'cafe\u0301' s1, s2 len(s1), len(s2) s1 == s2 # ('café', 'café') # (4, 5) # False -
在Unicode 标准中,'é' 和 'e\u0301' 这样的序列叫“标准等价物”(canonical equivalent),应用程序应该把它们视作相同的字符。但是,Python 看到的是不同的码位序列,因此判定二者不相等。
-
这个问题的解决方案是使用
unicodedata.normalize函数提供的Unicode 规范化。这个函数的第一个参数是这 4 个字符串中的一个:'NFC'、'NFD'、'NFKC' 和 'NFKD'。from unicodedata import normalize len(normalize('NFC', s1)), len(normalize('NFC', s3)) # (4, 4) len(normalize('NFD', s1)), len(normalize('NFD', s3)) # (5, 5) normalize('NFC', s1) == normalize('NFC', s2) # True normalize('NFD', s1) == normalize('NFD', s3) # True -
西方键盘通常能输出组合字符,因此用户输入的文本默认是 NFC 形式。不过,安全起见,保存文本之前,最好使用
normalize('NFC', user_text)清洗字符串。 -
NFC 也是 W3C 的“Character Model for the World Wide Web: String Matching and Searching”规范推荐的规范化形式。
-
使用 NFC 时,有些单字符会被规范成另一个单字符。这两个字符在视觉上是一样的,但是比较时并不相等,因此要规范化,防止出现意外。
-
在另外两个规范化形式(NFKC 和 NFKD)的首字母缩略词中,字母 K 表示 “compatibility”(兼容性)。这两种是较严格的规范化形式,对“兼容字符”有影响。虽然 Unicode 的目标是为各个字符提供 “规范的” 码位,但是为了兼容现有的标准,有些字符会出现多次。
微符号是一个 “兼容字符”。
-
使用 NFKC 和 NFKD 规范化形式时要小心,而且只能在特殊情况中使用,例如搜索和索引,而不能用于持久存储,因为这两种转换会导致数据损失。
-
大小写折叠:
str.casefold(),就是把所有文本变成小写,再做些其他转换,与str.lower()基本一致,但是存在例外:微符号 'μ' 会变成小写的希腊字母“μ”(在多数字体中二者看起来一样);德语 Eszett(“sharp s”,ß)会变成“ss” -
去掉变音符号的优势:
- 搜索方便:人们有时很懒,或者不知道怎么正确使用变音符号,而且拼写规则会随时间变化,因此实际语言中的重音经常变来变去。
- URL可读性:去掉变音符号还能让 URL 更易于阅读,至少对拉丁语系语言是如此。
-
变音符号对排序有影响的情况很少发生,只有两个词之间唯有变音符号不同时才有影响。此时,带有变音符号的词排在常规词的后面。
-
在 Python 中,非 ASCII 文本的标准排序方式是使用
locale.strxfrm
函数,根据 locale 模块的文档,这个函数会“把字符串转换成适合所在区域进行比较的形式”。 -
PyUCA:Unicode 排序算法(Unicode Collation Algorithm,UCA)的纯 Python 实现。PyUCA 没有考虑区域设置。如果想定制排序方式,可以把自定义的排序表路径传给 Collator() 构造方法。PyUCA 默认使用项目自带的
allkeys.txt。import pyuca coll = pyuca.Collator() fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] sorted_fruits = sorted(fruits, key=coll.sort_key) sorted_fruits -
Unicode 标准提供了一个完整的数据库(许多格式化的文本文件),不仅包括码位与字符名称之间的映射,还有各个字符的元数据,以及字符之间的关系。
Unicode 数据库记录了字符是否可以打印、是不是字母、是不是数字,或者是不是其他数值符号。unicodedata 模块中有几个函数用于获取字符的元数据。例如,字符在标准中的官方名称是不是组合字符(如结合波形符构成的变音符号等),以及符号对应的人类可读数值(不是码位)。
-
import unicodedata import re re_digit = re.compile(r'\d') sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285' for char in sample: print( 'U+%04x' % ord(char), char.center(6), 're_dig' if re_digit.match(char) else '-', 'isdig' if char.isdigit() else '-', 'isnum' if char.isnumeric() else '-', format(unicodedata.numeric(char), '5.2f'), unicodedata.name(char), sep='\t' )
4.5 支持字符串和字节序列的双模式API
-
可以使用正则表达式搜索字符串和字节序列,但是在后一种情况中,ASCII 范围外的字节不会当成数字和组成单词的字母。
- 字符串模式 r'\d+' 能匹配泰米尔数字和 ASCII 数字。
- 字节序列模式 rb'\d+' 只能匹配 ASCII 字节中的数字。
- 字符串模式 r'\w+' 能匹配字母、上标、泰米尔数字和 ASCII 数字。
- 字节序列模式 rb'\w+' 只能匹配 ASCII 字节中的字母和数字。
-
字符串正则表达式有个 re.ASCII 标志,它让\w、\W、\b、\B、\d、\D、\s 和 \S 只匹配 ASCII 字符。
-
为了便于手动处理字符串或字节序列形式的文件名或路径名,os 模块提供了特殊的编码和解码函数。
-
fsencode(filename):如果 filename 是 str 类型(此外还可能是 bytes 类型),使用sys.getfilesystemencoding()返回的编解码器把 filename 编码成字节序列;否则,返回未经修改的 filename 字节序列。 -
fsdecode(filename):如果 filename 是 bytes 类型(此外还可能是 str 类型),使用sys.getfilesystemencoding()返回的编解码器把 filename 解码成字符串;否则,返回未经修改的 filename 字符串。 -
surrogateescape:在 Unix 衍生平台中,这些函数使用 surrogateescape 错误处理方式以避免遇到意外字节序列时卡住。Windows 使用的错误处理方式是 strict。这种错误处理方式会把每个无法解码的字节替换成 Unicode 中 U+DC00 到 U+DCFF 之间的码位(Unicode 标准把这些码位称为“Low Surrogate Area”),这些码位是保留的,没有分配字符,供应用程序内部使用。编码时,这些码位会转换成被替换的字节值。
-
在 Python 3.3 之前,编译 CPython 时可以配置在内存中使用 16 位或 32 位存储各个码位。16 位是“窄构建”(narrow build),32 位是“宽构建”(wide build)。如果想知道用的是哪个,要查看
sys.maxunicode的值:65535 表示“窄构建”,不能透明地处理U+FFFF 以上的码位。“宽构建”没有这个限制,但是消耗的内存更多:每个字符占 4 个字节,就算是中文象形文字的码位大多数也只占 2 个字节。这两种构建没有高下之分,应该根据自己的需求选择。 -
灵活的字符串表述类似于 Python 3 对 int 类型的处理方式:如果一个整数在一个机器字中放得下,那就存储在一个机器字中;否则解释器切换成变长表述,类似于 Python 2 中的 long 类型。
-
5. 一等函数
- 在 Python 中,函数是一等对象。一等对象的定义:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
- 人们经常将“把函数视作一等对象”简称为“一等函数”。这样说并不完美,似乎表明这是函数中的特殊群体。在 Python 中,所有函数都是一等对象。
5.1 把函数视作对象
-
__doc__是函数对象众多属性中的一个,显示函数的备注 -
help(f):输出的文本来自函数对象的__doc__属性 -
函数对象的 一等 本性:
- 把 factorial 函数赋值给变量 fact,然后通过变量名调用
- 把它作为参数传给map 函数,返回一个可迭代对象,里面的元素是把第一个参数
(一个函数)应用到第二个参数(一个可迭代对象)中各个元素上得到的结果
-
高阶函数:higher-order function,接受函数为参数,或者把函数作为结果返回的函数
这样的函数例如:map,sorted,reduce,filter,apply(已移除)
map、filter 和 reduce 这三个高阶函数还能见到,不过多数使用场景下都有更好的替代品。
-
列表推导 或 生成器表达式 具有map和filter两个函数的功能,且更易于阅读
list(map(fact, range(6))) [fact(n) for n in range(6)]list(map(factorial, filter(lambda n: n % 2, range(6)))) [fact(n) for n in range(7) if n % 2 != 0] -
reduce 是内置函数,最常用于求和,现在最好使用内置的 sum 函数,在可读性和性能方面,这是一项重大改善
sum 和 reduce 的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个值。
from functools import reduce from operator import add reduce(add, range(100)) sum(range(100)) -
all 和 any 也是内置的归约函数
-
all(iterable):如果 iterable 的每个元素都是真值,返回 True;all([])返回True。 -
any(iterable):只要 iterable 中有元素是真值,就返回 True;any([])返回False。
-
-
-
匿名函数:lambda 关键字在 Python 表达式内创建匿名函数。
lambda 句法只是语法糖:与 def 语句一样,lambda 表达式会创建函数对象。这是 Python 中几种可调用对象的一种。
-
调用类的时候会运行类的
__new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实例返回给调用方。- 因为Python没有new运算符,所以调用类相当于调用函数。
- 如果类定义了
__call__方法,那么它的实例可以作为函数调用。
-
生成器函数:使用
yield关键字的函数或方法。调用生成器函数返回的是生成器对象。(生成器函数还可以作为协程) -
Python 中有各种各样可调用的类型,因此判断对象能否调用,最安全的方法是使用内置的
callable()函数[callable(obj) for obj in (abs, str, 13)] # [True, True, False] -
任何 Python 对象都可以表现得像函数。为此,只需实现实例方法
__call__ -
函数内省:除了
__doc__,函数对象还有其他属性:dir(factorial)['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']-
与用户定义的常规类一样,函数使用
__dict__属性存储赋予它的用户属性。这相当于一种基本形式的注解。 -
列出 常规对象没有 而 函数对象有 的属性:
class C: pass obj = C() def func(): pass str(sorted(set(dir(func)) - set(dir(obj))))['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__'] -
名称 类型 说明 __annotations__dict 参数和返回值的注解 __call__method-wrapper 实现()运算符;即可调用对象协议 __closure__tuple 函数闭包,即自由变量的绑定(通常是None) __code__code 编译成字节码的函数元数据和函数定义体 __defaults__tuple 形式参数的默认值 __get__method-wrapper 实现只读描述符协议 __globals__dict 函数所在模块中的全局变量 __kwdefaults__dict 仅限关键字形式参数的默认值 __name__str 函数名称 __qualname__str 函数的限定名称,如 Random.choice
-
5.2 函数的参数和注解
-
仅限关键字参数:keyword-only argument,调用函数时使用
*和**展开可迭代对象,映射到单个参数。 -
在传参的时候,将字典加上
**作为参数传递,实现的是字典中所有元素作为单个参数传入,同名的键回绑定到对应的具名参数上,余下的则被**attrs捕获。 -
仅限关键字参数:它一定不会捕获未命名的定位参数。
此种参数只能由关键字提供,绝对不会被位置参数自动填充。
- 定义函数时若想指定仅限关键字参数,要把它们放到前面有
*的参数后面。 - 如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放
一个* - 允许常规参数出现在可变参数之后:此时这个常规参数就是一个仅限关键字参数。强制性的,它只能通过关键字传参
- 仅限关键字参数不需要有默认值, 由于Python需要将所有的参数都绑定一个值,而且将值绑定到关键字参数的唯一方法是通过这个关键字,因此这种参数是 需要关键字的参数 。所以这些参数必须通过调用方提供,且必须通过关键字提供值。
- 语法上的更改是允许省略可变参数的参数名。这意味着对于一个有仅限关键字参数的函数来说,它不会再接受一个可变参数。
- 定义函数时若想指定仅限关键字参数,要把它们放到前面有
-
获取关于参数的信息
-
使用HTTP微框架Bobo中有个使用函数内省的好例子。
启动方式:
bobo -f hello.pyimport bobo @bobo.query('/') def hello(person): return "Hello %s" % person -
函数对象有个
__defaults__属性,它的值是一个元组,里面保存着定位参数和关键字参数的默认值。仅限关键字参数的默认值在__kwdefaults__属性中。然而,参数的名称在__code__属性中,它的值是一个 code 对象引用,自身也有很多属性。
-
-
-
inspect.signature函数返回一个inspect.Signature对象,它有一个parameters属性,这是一个有序映射,把参数名和inspect.Parameter对象对应起来。各个Parameter属性也有自己的属性,例如name、default和kind。特殊的inspect._empty值表示没有默认值,考虑到None是有效的默认值(也经常这么做),而且这么做是合理的。 -
kind的属性的值,为_ParameterKind类中的5个值之一:-
POSITIONAL_OR_KEYWORD:可以通过定位参数和关键字参数传入的形参(多数 Python 函数的参数属于此类)。 -
VAR_POSITIONAL:定位参数元组。 -
VAR_KEYWORD:关键字参数元组。 -
KEYWORD_ONLY:仅限关键字参数(Python 3 新增)。 -
POSITIONAL_ONLY:仅限定位参数;目前,Python 声明函数的句法不支持,但是有些使用 C 语言实现且不接受关键字参数的函数(如 divmod)支持。
-
-
-
函数注解
- 函数声明中的各个参数可以在 : 之后增加注解表达式。如果参数有默认值,注解放在参数名和 = 号之间。
- 如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 -> 和一个表达式。那个表达式可以是任何类型。
- 注解中最常用的类型是类(如 str 或 int)和字符串(如 'int > 0')
- 注解不会做任何处理,只是存储在函数的
__annotations__属性中 - Python 对注解所做的唯一的事情是,把它们存储在函数的
__annotations__属性里。仅此而已,Python 不做检查、不做强制、不做验证,什么操作都不做。换句话说,注解对 Python 解释器没有任何意义。 - 函数注解的最大影响或许不是让 Bobo 等框架自动设置,而是为 IDE 和
lint 程序等工具中的静态类型检查功能提供额外的类型信息。
5.3 支持函数式编程的包
-
operator模块
-
使用reduce函数和一个匿名函数计算阶乘
def fact(n): return reduce(lambda a,b: a*b, range(1, n+1)) -
operator 模块为多个算术运算符提供了对应的函数,从而避免编写
lambda a, b: a*b这种平凡的匿名函数def fact2(n): return reduce(mul, range(1, n+1)) -
使用 itemgetter 排序一个元组列表
-
itemgetter 使用
[]运算符,因此它不仅支持序列,还支持映射和任何实现__getitem__方法的类。 -
itemgetter(1)的作用与lambda fields:fields[1]一样
metro_data = [ ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)), ] from operator import itemgetter for city in sorted(metro_data, key=itemgetter(2)): print(city) -
-
如果把多个参数传给 itemgetter,它构建的函数会返回提取的值构成的元组:
cc_name = itemgetter(1, 0) for city in metro_data: print(cc_name(city)) -
attrgetter 与 itemgetter 作用类似,它创建的函数根据名称提取对象的属性
from collections import namedtuple LatLong = namedtuple('LatLong', 'lat long') Metropolis = namedtuple('Metropolis', 'name cc pop coord') metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) for name, cc, pop, (lat, long) in metro_data] metro_areas metro_areas[0].coord.lat from operator import attrgetter name_lat = attrgetter('name', 'coord.lat') for city in sorted(metro_areas, key=attrgetter('coord.lat')): print(name_lat(city)) -
attrgetter 与 itemgetter 个人总结:
- itemgetter:偏向于数组的切片操作
- attrgetter:偏向于类的属性获取操作
-
methodcaller 的作用与 attrgetter 和 itemgetter 类似,它会自行创建函数,methodcaller 创建的函数会在对象上调用参数指定的方法:
from operator import methodcaller s = 'The tiem has come' upcase = methodcaller('upper') upcase(s) hiphenate = methodcaller('replace', ' ', '-') hiphenate(s) -
methodcaller还可以冻结某些参数,也就是部分应用(partial application),这与
functoosl.partial函数的作用类似。
-
-
使用
functools.partial冻结参数-
functools.partial 这个高阶函数用于部分应用一个函数。部分应用是指,基于一个函数创建一个新的可调用对象,把原函数的某些参数固定。使用这个函数可以把接受一个或多个参数的函数改编成需要回调的API,这样参数更少。
from operator import mul from functools import partial triple = partial(mul, 3) triple(7) list(map(triple, range(1, 10))) -
固定nfc函数,标准化字符串编码处理
import unicodedata, functools nfc = functools.partial(unicodedata.normalize, 'NFC') s1 = 'café' s2 = 'cafe\u0301' s1, s2 s1 == s2 nfc(s1) == nfc(s2) -
functools.partialmethod函数的作用与partial一样,不过是用于处理方法的。- 对于类方法,partial就没办法了,所以新引用了
partialmethod - 参考链接
- Python functools 模块
- 对于类方法,partial就没办法了,所以新引用了
-
functools 模块中的 lru_cache 函数令人印象深刻,它会做备忘(memoization),这是一种自动优化措施,它会存储耗时的函数调用结果,避免重新计算。
-
6. 使用一等函数实现涉及模式
-
实现“策略” 模式:
- 经典“策略”模式
- 使用函数实现“策略”模式
-
策略对象通常是很好的享元:
- 享元:flyweight,享元是可共享的对象,可以同时再多个上下文中使用
- 可以避免运行时消耗
- 函数比用户定义的类的实例轻量,而且无需使用“享元”模式,因为各个策略函数在 Python 编译模块时只会创建一次。普通的函数也是“可共享的对象,可以同时在多个上下文中使用”。
-
在 Python 中,模块也是一等对象,而且标准库提供了几个处理模块的函数
-
globals():返回一个字典,表示当前的全局符号表。这个符号表始终针对当前模块(对函数或方法来说,是指定义它们的模块,而不是调用它们的模
块)。
-
-
命令模式
- 可以通过把函数作为参数传递而简化。
- “命令”模式的目的是解耦调用操作的对象(调用者)和提供实现的对象(接收者)。
- 这个模式的做法是,在二者之间放一个 Command 对象,让它实现只有一个方法(execute)的接口,调用接收者中的方法执行所需的操作。这样,调用者无需了解接收者的接口,而且不同的接收者可以适应不同的 Command 子类。调用者有一个具体的命令,通过调用 execute 方法执行。
class MacroCommand: """一个执行一组命令的命令""" def __init__(self, commands): self.commands = list(commands) def __call__(self): for command in self.commands: command() -
复习
- 一等对象:指的是满足下述条件的程序实体
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
- 整数、字符串和字典都是一等对象。在面向对象编程中,函数也是对象,并满足以上条件,所以函数也是一等对象,称为"一等函数"
- 普通函数 & 高阶函数:接受函数为参数的函数为高阶函数,其余为普通函数
- 《设计模式:可复用面向对象软件的基础》书中的两个设计原则:
- 对接口编程,而不是对实现编程
- 优先使用对象组合,而不是类继承
- 一等对象:指的是满足下述条件的程序实体
7. 函数装饰器和闭包
- 函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。
- 除了在装饰器中有用处之外,闭包还是回调式异步编程和函数式编程风格的基础。
7.1 装饰器基础
-
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
下述两个代码等价:
-
装饰器
@decorate def target(): print('running target()') -
另一种写法
def target(): print('running target()') target = decorate(target)
-
-
两种写法的最终结果一样:上述两个代码片段执行完毕后得到的 target 不一定是原来那个 target 函数,而是 decorate(target) 返回的函数。
-
装饰器通常把函数替换成另一个函数
def deco(func): def inner(): print('running inner()') return inner @deco def target(): print('running target()') target() # running inner() target # <function __main__.deco.<locals>.inner()> -
严格来说,装饰器只是语法糖。
-
装饰器的特性:
- 一大特性是,能把被装饰的函数替换成其他函数。
- 第二个特性是,装饰器在加载模块时立即执行。
-
Python何时执行装饰器:在被装饰的函数定义之后立即运行
registry = [] def register(func): print('running register(%s)' % func) registry.append(func) return func @register def f1(): print('running f1()') @register def f2(): print('running f2()') def f3(): print('running f3()') def main(): print('running main()') print('registry ->', registry) f1() f2() f3() if __name__ == '__main__': main()running register(<function f1 at 0x00000246ABFAC708>) running register(<function f2 at 0x00000246AC0D4CA8>) running main() registry -> [<function f1 at 0x00000246ABFAC708>, <function f2 at 0x00000246AC0D4CA8>] running f1() running f2() running f3() -
如果导入上述代码:
import registration,函数的装饰器再导入模块时立即执行,而被装饰的函数只再明确调用时运行。 -
装饰器在真实代码中的常用方式:
- 装饰器通常在一个模块中定义,然后应用到其他模块中的函数上
- 大多数装饰器会在内部定义函数,然后将其返回
-
register 装饰器原封不动地返回被装饰的函数,但是这种技术并非没有用处。
- 很多 Python Web 框架使用这样的装饰器把函数添加到某种中央注册处,例如把 URL 模式映射到生成 HTTP 响应的函数上的注册处。
- 这种注册装饰器可能会也可能不会修改被装饰的函数。
-
使用装饰器改进“策略”模式
- 多数装饰器会修改被装饰的函数
- 通常,它们会定义一个内部函数,然后将其返回,替换被装饰的函数
- 使用内部函数的代码几乎都要靠闭包才能正确运作
7.2 闭包
-
变量作用域规则
- Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量
- 如果在函数中赋值时想让解释器把 b 当成全局变量,要使用
global声明 - 通过
from dis import dis查看程序的字节码
-
假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格
-
avg使用方法:
>>> avg(10) 10.0 >>> avg(11) 10.5 >>> avg(12) 11.0 -
实现方式:
-
计算移动平均的类
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) -
计算移动平均值的高阶函数
def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total/len(series) return averager -
注意:这两个示例有共通之处:调用
Averager()或make_averager()得到一个可调用对象 avg,它会更新历史值,然后计算当前均值-
在
averager函数中,series时自由变量(free variable),这是一个技术术语,指:未在本地作用域中绑定的变量 -
averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定
-
审查返回的 averager 对象,我们发现 Python 在
__code__属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称avg.__code__.co_varnames # ('new_value', 'total') avg.__code__.co_freevars # ('series',) -
series 的绑定在返回的 avg 函数的
__closure__属性中。avg.__closure__中的各个元素对应于avg.__code__.co_freevars中的一个名称。这些元素是 cell 对象,有个cell_contents属性,保存着真正的值avg.__closure__ # (<cell at 0x00000246AC517108: list object at 0x00000246AC124A88>,) avg.__closure__[0].cell_contents # [10, 11, 12]
-
-
-
-
闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
-
注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
-
nonlocal声明
-
上面的操作时引入了list(可变对象) series,用来保存每一次的值,然后这个series 是 所谓的 自由变量
-
但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新,就不是所谓的 自由变量了,因此不会保存在闭包中。
-
nonlocal声明可以把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。 -
如果为
nonlocal声明的变量赋予新值,闭包中保存的绑定会更新。 -
优化后的闭包
def make_averager(): count = 0 total = 0 def averager(new_value): nonlocal count, total count += 1 total += new_value return total/count return averager -
nonlocal是python3的特性,在python2中需要把内部函数需要修改的变量存储为可变对象(如字典或简单的实例)的元素或属性,并且把那个对象绑定给一个自由变量。
-
7.3 装饰器
-
实现一个简单的装饰器:定义了一个装饰器,它会在每次调用被装饰的函数时计时,然后把经过的时间、传入的参数和调用的结果打印出来。
import time def clock(func): def clocked(*args): # 记录初始时间t0 t0 = time.perf_counter() # 调用原来的 factorial 函数,保存结果 result = func(*args) # 计算经过的时间 elapsed = time.perf_counter() - t0 # 格式化收集的数据,然后打印出来 name = func.__name__ arg_str = ', '.join(repr(arg) for arg in args) print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result)) # 返回第 2 步保存的结果 return result return clocked @clock def snooze(seconds): time.sleep(seconds) @clock def factorial(n): return 1 if n < 2 else n * factorial(n - 1) if __name__ == '__main__': print('*' * 40, 'Calling snooze(.123)') snooze(.123) print('*' * 40, 'Calling factorial(6)') print('6! = ', factorial(6))**************************************** Calling snooze(.123) [0.12354480s] snooze(0.123) -> None **************************************** Calling factorial(6) [0.00000040s] factorial(1) -> 1 [0.00003500s] factorial(2) -> 2 [0.00004590s] factorial(3) -> 6 [0.00005500s] factorial(4) -> 24 [0.00006390s] factorial(5) -> 120 [0.00007470s] factorial(6) -> 720 6! = 720 -
这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。
装饰器:动态地给一个对象添加一些额外的职责
——《设计模式:可复用面向对象软件的基础》
-
上述实现的clock装饰器有几个缺点:
- 不支持关键字参数
- 遮盖了被装饰函数的
__name__和__doc__属性
-
使用
functools.wraps装饰器把相关属性从func复制到clocked中,同时还可以正确处理关键字参数import time from functools import wraps def clock(func): @wraps(func) def clocked(*args, **kwargs): t0 = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - t0 name = func.__name__ arg_lst = [] if args: arg_lst.append(', '.join(repr(arg) for arg in args)) if kwargs: pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())] arg_lst.append(', '.join(pairs)) arg_str = ', '.join(arg_lst) print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result)) return result return clocked -
标准库中的装饰器
- python 内置了三个用于装饰方法的函数:property、classmethod、staticmethod
- functools:wraps、lru_cache、singledispatch
-
使用
functools.lru_cache做备忘,memoization,这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。- LRU:Least Recently Used,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。
- 注意,必须像常规函数哪一行调用
lru_cache:@functools.lru_cache(),这是因为lru_cache可以接受配置参数。 - 除了优化递归算法之外,
lru_cache在从Web中获取信息的应用中也能发挥巨大作用。
-
functools.lru_cache(maxsize=128, typed=False)- maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的幂。
- typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。
- lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的。
-
单分派泛函数
-
因为 Python 不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize 的变体,也无法使用不同的方式处理不同的数据类型。
-
在Python 中,一种常见的做法是把 htmlize 变成一个分派函数,使用一串 if/elif/elif,调用专门的函数,如htmlize_str、htmlize_int,等等。这样不便于模块的用户扩展,还显得笨拙:时间一长,分派函数 htmlize 会变得很大,而且它与各个专门函数之间的耦合也很紧密。
-
functools.singledispatch装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。 -
使用
@singledispatch装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。这才称得上是单分派。如果根据多个参数选择专门的函数,那就是多分派了。
-
singledispatch 创建一个自定义的htmlize.register 装饰器,把多个函数绑在一起组成一个泛函数
from functools import singledispatch from collections import abc import numbers import html @singledispatch def htmlize(obj): content = html.escape(repr(obj)) return '<pre>{}</pre>'.format(content) @htmlize.register(str) def _(text): content = html.escape(text).replace('\n', '<br>\n') return '<p>{0}</p>'.format(content) @htmlize.register(numbers.Integral) def _(n): return '<pre>{0} (0x{0:x})</pre>' @htmlize.register(tuple) @htmlize.register(abc.MutableSequence) def _(seq): inner = '</li>\n<li>'.join(htmlize(item) for item in seq) return '<ul>\n<li>' + inner + '</li>\n</ul>' -
numbers.Integral是int的虚拟超类 -
可以叠放多个
register装饰器,让同一个函数支持不同类型 -
只要可能,注册的专门函数应该处理抽象基类(如 numbers.Integral和 abc.MutableSequence),不要处理具体实现(如 int 和 list)。这样,代码支持的兼容类型更广泛。
-
使用抽象基类检查类型,可以让代码支持这些抽象基类现有和未来的具体子类或虚拟子类
-
@singledispatch不是为了把 Java 的那种方法重载带入Python -
在一个类中为同一个方法定义多个重载变体,比在一个函数中使用一长串 if/elif/elif/elif 块要更好。
-
但是这两种方案都有缺陷,因为它们让代码单元(类或函数)承担的职责太多。
-
@singledispath的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数。 -
装饰器是函数,因此可以组合起来使用(即,可以在已经被装饰的函数上应用装饰器)
-
-
叠放装饰器
下述代码等价
@d1 @d2 def f(): print('f') f = d1(d2(f)) -
参数化装饰器
registry = set() def register(active=True): def decorate(func): print('running register(active=%s )->decorate(%s)' % (active, func)) if active: registry.add(func) else: registry.discard(func) return func return decorate @register(active=False) def f1(): print('running f1()') @register() def f2(): print('running f2()') def f3(): print('running f3()')- 这里decorate时装饰器,必须返回一个函数
- register是装饰器工厂函数,因此返回的是一个装饰器decorate
- 即使不传入参数,register也必须作为函数调用:
@register() - 如果不使用 @ 句法,那就要像常规函数那样使用 register;若想把 f 添加到 registry 中,则装饰 f 函数的句法是
register()(f);不想添加(或把它删除)的话,句法是register(active=False)(f)
-
参数化clock装饰器
import time DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}' def clock(fmt=DEFAULT_FMT): def decorate(func): def clocked(*_args): t0 = time.time() _result = func(*_args) elapsed = time.time() - t0 name = func.__name__ args = ", ".join(repr(arg) for arg in _args) result = repr(_result) print(fmt.format(**locals())) return _result return clocked return decorate if __name__ == '__main__': @clock() def snooze(seconds): time.sleep(seconds) for i in range(3): snooze(.123)[0.12328148s] snooze(0.123) -> None [0.12369442s] snooze(0.123) -> None [0.12362432s] snooze(0.123) -> None- clock:参数化装饰器工厂函数
- decorate:真正的装饰器
- clocked:包装被装饰的函数
-
_result:被装饰函数返回的真正结果 -
_args:clocked的参数,args用于显示的字符串 - result:
_result的字符串表现形式,用于显示
-
装饰器的参数
@clock('{name}: {elapsed}s') def snooze(seconds): time.sleep(seconds) for i in range(3): snooze(.123)snooze: 0.12300252914428711s snooze: 0.12314867973327637s snooze: 0.1238546371459961s@clock('{name}({args}) dt={elapsed:0.3f}s') def snooze(seconds): time.sleep(seconds) for i in range(3): snooze(.123)snooze(0.123) dt=0.124s snooze(0.123) dt=0.123s snooze(0.123) dt=0.123s
8. 对象引用、可变性和垃圾回收
8.1 变量不是盒子
-
如果把变量想象为盒子,那么无法解释 Python 中的赋值;应该把变量视作便利贴
-
对引用式变量来说,说把变量分配给对象更合理
-
==运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识 -
元组的相对不可变性:元组的不可变性其实是指
tuple数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。元组里面有一个list,这个list就可以append,但是id还是不变。
这也是有些元组不可散列的原因。
-
str、bytes 和 array.array 等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)。
-
浅复制(copy.copy)和深复制(copy.deepcopy)的区别:深复制中副本不共享内部对象的引用
8.2 函数的参数作为引用
-
不要使用可变类型作为参数的默认值
-
不可以默认值为类似于
[],因为如果不传参的话,这个[]就是类的内部变量,多个实例会共用这个变量 -
这也是为什么通常使用 None 作为接收可变值的参数的默认值的原因。
-
-
防御可变参数
- 类中的操作可能会修改传入类的可变参数
- 修正的方法:初始化时,把参数值的副本赋值给成员变量
8.3 del、垃圾回收和弱引用
注意,这一章节的一些代码要在cmd中才能实现,jupyterlab中的实验结果与书中给的结果不一致。
-
del 语句删除名称,而不是对象
-
del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。
-
重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。
-
弱引用:正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。
- 弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。
- 弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。
-
weakref.ref 类其实是低层接口,供高级用途使用,多数程序最好使用 weakref 集合和 finalize。也就是说,应该使用
WeakKeyDictionary、WeakValueDictionary、WeakSet和finalize(在内部使用弱引用),不要自己动手创建并处理 weakref.ref 实例。 -
WeakValueDictionary
- WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。
- 被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从 WeakValueDictionary 中删除。
- 因此,WeakValueDictionary 经常用于缓存。
-
与 WeakValueDictionary 对应的是 WeakKeyDictionary,后者的键是弱引用
-
WeakSet 类:保存元素弱引用的集合类。元素没有强引用时,集合会把它删除
- 如果一个类需要知道所有实例,一种好的方案是创建一个WeakSet 类型的类属性,保存实例的引用。
- 如果使用常规的 set,实例永远不会被垃圾回收,因为类中有实例的强引用,而类存在的时间与 Python 进程一样长,除非显式删除类。
-
弱引用的局限
- 不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict 实例不能作为所指对象,但是它们的子类可以
- set 实例可以作为所指对象
- 用户定义的类型也没问题
- 但是,int 和 tuple 实例不能作为弱引用的目标,甚至它们的子类也不行
- 这些局限基本上是 CPython 的实现细节,在其他 Python 解释器中情况可能不一样。这些局限是内部优化导致的结果
-
Python对不可变类型施加的把戏
-
对元组 t 来说,
t[:]不创建副本,而是返回同一个对象的引用。此外,tuple(t)获得的也是同一个元组的引用。(str、bytes 和 frozenset 实例也有这种行为)- frozenset 实例不是序列,因此不能使用
fs[:](fs 是一个 frozenset 实例),但是,fs.copy()具有相同的效果:返回同一个对象的引用,而不是创建一个副本
- frozenset 实例不是序列,因此不能使用
-
字符串字面量可能会创建共享的对象
s1 = 'ABC' s2 = 'ABC' s2 is s1 # True- 共享字符串字面量是一种优化措施,称为驻留(interning)。CPython 还会在小的整数上使用这个优化措施,防止重复创建“热门”数字
- CPython 不会驻留所有字符串和整数
-
-
杂谈
- java的
==运算符比较的是对象(不是基本类型)的引用,而不是对象的值。否则的话要用到.equals方法,如果调用方法的变量为null,会得到一个 空指针异常 - 在python中,
==比较对象的值,is比较引用 - 最重要的是,python支持重载运算发,
==能正确处理标准库中的所有对象,包括None,与java的null不同
- java的
9. 符合Python风格的对象
9.1 对象的表示形式
-
获取对象的字符串表示形式的标准方式
-
repr():开发者理解的方式返回对象的字符串表示形式 -
str():用户理解的方式返回对象的字符串表示形式
-
-
在 Python 3 中,
__repr__、__str__和__format__都必须返回 Unicode 字符串(str 类型)。只有__bytes__方法应该返回字节序列(bytes 类型) -
定义
__iter__方法,把类的实例编程可迭代的对象,这样才能拆包。x, y = my_vector这一行也可以写成
yield self.x; yield self.y -
eval()函数用来执行一个字符串表达式,并返回表达式的值。 -
classmethod:第一个参数是类本身,最常见的用途是定义备选构造方法
-
staticmethod:第一个参数不是特殊的值,其实静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义
-
作者对staticmethod的态度是:”不是特别有用“,因为如果想定义不需要与类进行交互的函数,只需要在模块中定义就好了。
9.2 格式化显示
-
内置的
format()函数和str.format()方法把各个类型的格式化方式委托给相应的.__format__(format_spec)方法-
format(my_obj, format_spec)里的第二个参数 -
str.format()方法的格式字符串,{}里代换字段中冒号后面的部分
-
-
{0.mass:5.3e}这样的格式字符串其实包含两部分- 冒号左边的
.mass在代换字段句法中是字段名 - 冒号后面的
5.3e是格式说明符 - 格式说明符使用的表示法叫 格式规范微语言 ,Format Specification Mini-Language
- 冒号左边的
-
格式规范微语言为一些内置类型提供了专用的表示代码
- b 和 x 分别表示二进制和十六进制的 int 类型
- f 表示小数形式的 float 类型
- % 表示百分数形式
-
格式规范微语言是可扩展的,因为各个类可以自行决定如何解释format_spec 参数
-
如果类没有定义
__format__方法,从 object 继承的方法会返回str(my_object) -
在格式规范微语言中,整数使用的代码有
bcdoxXn,浮点数使用的代码有eEfFgGn%,字符串使用的代码有s -
使用两个前导下划线(尾部没有下划线,或者有一个下划线),把属性标记为私有的
-
@property装饰器把读值方法标记为属性 -
要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需正确地实现
__hash__和__eq__方法即可 -
如果定义的类型有标量数值,可能还要实现
__int__和__float__方法(分别被int()和float()构造函数调用),以便在某些情况下用于强制转换类型。此外,还有用于支持内置的complex()构造函数的__complex__方法from array import array import math class Vector2d: typecode = 'd' def __init__(self, x, y): self.__x = float(x) self.__y = float(y) @property def x(self): return self.__x @property def y(self): return self.__y def __iter__(self): return (i for i in (self.x, self.y)) def __repr__(self): class_name = type(self).__name__ return '{}({!r}, {!r})'.format(class_name, *self) def __str__(self): return str(tuple(self)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))) def __eq__(self, other): return tuple(self) == tuple(other) def __abs__(self): return math.hypot(self.x, self.y) def __bool__(self): return bool(abs(self)) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(*memv) def angle(self): return math.atan2(self.y, self.x) def __hash__(self): return hash(self.x) ^ hash(self.y) def __format__(self, fmt_spec=''): if fmt_spec.endswith('p'): fmt_spec = fmt_spec[: -1] coords = (abs(self), self.angle()) outer_fmt = '<{}, {}>' else: coords = self outer_fmt = '({}, {})' components = (format(c, fmt_spec) for c in coords) return outer_fmt.format(*components)
9.3 Python的私有属性和受保护的属性
-
私有属性需要在前面加两根下划线,Python 会把属性名存入实例的
__dict__属性中,而且会在前面加上一个下划线和类名- 父类:_Dog__mood
- 子类:_Beagle__mood
- 这个语言特性叫 名称改写 (name mangling)
-
Python的私有属性是可以被修改的,通过上述名称修改
-
受保护属性:
- Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员严格遵守的约定,他们不会在类外部访问这种属性。
- 不过在模块中,顶层名称使用一个前导下划线的话,的确会有影响:对
from mymod import *来说,mymod 中前缀为下划线的名称不会被导入。然而,依旧可以使用from mymod import _privatefunc将其导入。
-
使用
__slots__类属性节省空间-
为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过
__slots__类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。 -
定义
__slots__的方式是,创建一个类属性,使用__slots__这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。__slots__ = ('__x', '__y') -
在类中定义
__slots__属性的目的是告诉解释器:“这个类中的所有实例属性都在这儿了!”这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的__dict__属性。如果有数百万个实例同时活动,这样做能节省大量内存。 -
如果类中定义了
__slots__属性,而且想把实例作为弱引用的目标,那么要把'__weakref__'添加到__slots__中。
-
-
如果使用得当,
__slots__能显著节省内存- 每个子类都要定义
__slots__属性,因为解释器会忽略继承的__slots__属性 - 实例只能拥有
__slots__中列出的属性,除非把'__dict__'加入__slots__中(这样做就失去了节省内存的功效) - 如果不把
'__weakref__'加入__slots__,实例就不能作为弱引用的目标
- 每个子类都要定义
-
覆盖类属性
- 类属性可用于为实例属性提供默认值
- 如果为不存在的实例属性赋值,会新建实例属性
- 假如我们为
typecode实例属性赋值,那么同名 类属性 不受影响 - 然而,自此之后,实例读取的
self.typecode是实例属性typecode,也就是把同名类属性遮盖了 - Python风格的修改方法:
- 类属性是公开的,因此会被子类继承
- 于是经常会创建一个子类,只用于定制类的数据属性
-
总结:
- 所有用于获取字符串和字节序列表示形式的方法:
__repr__、__str__、__format__和__bytes__。 - 把对象转换成数字的几个方法:
__abs__、__bool__和__hash__。 - 用于测试字节序列转换和支持散列(连同
__hash__方法)的__eq__运算符。
- 所有用于获取字符串和字节序列表示形式的方法:
10. 序列的修改、散列和切片
-
reprlib.repr:展示变量的程序员能明白的语句 -
协议和鸭子类型
- 在 Python 中创建功能完善的序列类型无需使用继承,只需实现符合序列协议的方法
- 在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义
- 例如,Python 的序列协议只需要
__len__和__getitem__两个方法 - 协议是非正式的,没有强制力,因此如果你知道类的具体使用场景,通常只需要实现一个协议的部分。
-
slice.indices:indices 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法会“整顿”元组,把 start、stop 和 stride 都变成非负数,而且都落在指定长度序列的边界内。 -
__getattr__():对my_obj.x表达式,Python 会检查my_obj实例有没有名为x的属性;如果没有,到类(my_obj.__class__)中查找;如果还没有,顺着继承树继续查找。如果依旧找不到,调用my_obj所属类中定义的__getattr__方法,传入self和属性名称的字符串形式(如 'x') -
不建议只为了避免创建实例属性而使用
__slots__属性。__slots__属性只应该用于节省内存,而且仅当内存严重不足时才应该这么做。 -
归约函数(reduce、sum、any、all)把序列或有限的可迭代对象变成一个聚合结果
-
functools.reduce()的关键思想是,把一系列值归约成单个值。reduce()函数的第一个参数是接受两个参数的函数,第二个参数是一个可迭代的对象。import functools functools.reduce(lambda a, b: a * b, range(1, 6)) # 120 -
reduce(function, iterable, initializer):使用的时候最好提供第三个参数,这样能避免异常。如果序列为空,initializer是返回的结果;否则,在归约中使用它作为第一个参数,因此应该使用恒等值。比如,对 +、| 和 ^ 来说,initializer应该是 0;而对 * 和 & 来说,应该是 1。 -
zip:使用 zip 函数能轻松地并行迭代两个或更多可迭代对象,它返回的元组可以拆包成变量,分别对应各个并行输入中的一个元素。- zip 有个奇怪的特性:当一个可迭代对象耗尽后,它不发出警告就停止
-
itertools.zip_longest函数的行为有所不同:使用可选的fillvalue(默认值为 None)填充缺失的值,因此可以继续产出,直到最长的可迭代对象耗尽
from array import array import reprlib import math import numbers import functools import operator import itertools class Vector: typecode = 'd' def __init__(self, components): self._components = array(self.typecode, components) def __iter__(self): return iter(self._components) def __repr__(self): components = reprlib.repr(self._components) components = components[components.find('['):-1] return 'Vector({})'.format(components) def __str__(self): return str(tuple(self)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(self._components)) def __eq__(self, other): return tuple(self) == tuple(other) def __abs__(self): return math.sqrt(sum(x * x for x in self)) def __bool__(self): return bool(abs(self)) def __len__(self): return len(self._components) # def __getitem__(self, index): # return self._components[index] def __getitem__(self, index): cls = type(self) # 如果传入的参数是切片,就返回改切片生成的Vector类 if isinstance(index, slice): return cls(self._components[index]) # 如果传入的参数是数,就返回列表中的元素 elif isinstance(index, numbers.Integral): return self._components[index] # 否则就报错啦 else: msg = '{cls.__name__} indices must be integers' raise TypeError(msg.format(cls=cls)) shortcut_names = 'xyzt' def __getattr__(self, name): cls = type(self) if len(name) == 1: pos = cls.shortcut_names.find(name) if 0 <= pos < len(self._components): return self._components[pos] msg = '{.__name__!r} object has no attribute {!r}' raise AttributeError(msg.format(cls, name)) def __setattr__(self, name, value): cls = type(self) if len(name) == 1: if name in cls.shortcut_names: error = 'readonly attribute {attr_name!r}' elif name.islower(): error = "can't set attributes 'a' to 'z' in {cls_name!r}" else: error = '' if error: msg = error.format(cls_name=cls.__name__, attr_name=name) raise AttributeError(msg) super().__setattr__(name, value) def __eq__(self, other): # return tuple(self) == tuple(other) # 为了提高比较的效率,使用zip函数 # if len(self) != len(other): # return False # for a, b in zip(self, other): # if a != b: # return False # return True # 过用于计算聚合值的整个 for 循环可以替换成一行 all 函数调用: # 如果所有分量对的比较结果都是 True,那么结果就是 True。 return len(self) == len(other) and all(a == b for a, b in zip(self, other)) def __hash__(self): # 创建一个生成器表达式,惰性计算各个分量的散列值 # hashes = (hash(x) for x in self._components) # 这里换成map方法 hashes = map(hash, self._components) # 把hashes提供给reduce函数,第三个参数0是初始值 return functools.reduce(operator.xor, hashes, 0) def angle(self, n): r = math.sqrt(sum(x * x for x in self[n:])) a = math.atan2(r, self[n-1]) if (n == len(self) - 1) and (self[-1] < 0): return math.pi * 2 - a else: return a def angles(self): return (self.angle(n) for n in range(1, len(self))) def __format__(self, fmt_spec=''): if fmt_spec.endswith('h'): # 超球面坐标 fmt_spec = fmt_spec[:-1] coords = itertools.chain([abs(self)], self.angles()) outer_fmt = '<{}>' else: coords = self outer_fmt = '({})' components = (format(c, fmt_spec) for c in coords) return outer_fmt.format(', '.join(components)) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(memv)
11. 接口:从协议到抽象基类
鸭子类型:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
程序设计中,鸭子类型(英语:duck typing)是 动态类型 的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法" (计算机科学)和属性的集合决定。
在鸭子类型中,关注点在于对象的行为,能做什么;而不是关注对象所属的类型。
-
Python文化中的接口和协议
- 受保护的属性和私有属性不在接口中
- 接口 的补充定义:对象公开方法的子集,让对象在系统中扮演特定的角色
- 协议与继承没有关系。一个类可能会实现多个接口,从而让实例扮演多个角色。
- 协议是接口,但不是正式的,因此协议不能像正式接口那样施加限制
- 一个类可能只实现部分接口,这是允许的
- 对 Python 程序员来说,“X 类对象”、“X 协议”和“X 接口”都是一个意思
- 序列协议是 Python 最基础的协议之一。即便对象只实现了那个协议最基本的一部分,解释器也会负责任地处理。
-
Python喜欢序列
-
定义为抽象基类的Sequence正式接口
classDiagram Container <|-- Sequence Iterable <|-- Sequence Sized <|-- Sequence class Container { __contains__ } class Iterable { __iter__ } class Sized { __len__ } class Sequence { __getitem__ __contains__ __iter__ __reversed__ index count } -
定义
__getitem__方法,只实现序列协议的一部分,这样足够访问元素、迭代和使用 in 运算符了class Foo: def __getitem__(self, pos): return range(0, 30, 10)[pos]- 虽然没有
__iter__方法,但是 Foo 实例是可迭代的对象,因为发现有__getitem__方法时,Python 会调用它,传入从 0 开始的整数索引,尝试迭代对象 - 鉴于序列协议的重要性,如果没有
__iter__和__contains__方法,Python 会调用__getitem__方法,设法让迭代和 in 运算符可用。
- 虽然没有
-
shuffle 函数要调换集合中元素的位置,只实现了
__getitem__即是只实现了不可变的序列协议,可变的序列还必须提供__setitem__方法。 -
猴子补丁:在运行时修改类或模块,而不改动源码
-
协议是动态的:
random.shuffle函数不关心参数的类型,只要那个对象实现了部分可变序列协议即可。即便对象一开始没有所需的方法也没关系,后来再提供也行。 -
“鸭子类型”:对象的类型无关紧要,只要实现了特定的协议即可。
-
-
Alex Martelli的水禽