Python进阶
Python进阶
目录
[TOC]
Python数据模型
数据模型 ?:对Python框架的描述,规范了语言自身构建模块的接口。
一致性:任何获取对象长度的方法都是 len()
。 Pythonic
如何达到一致性的呢?
对任何对象 obj
,在使用 len(obj)
时,Python解释器会调用特殊方法,即 obj._len_()
。只需要重写 __len__
方法,就能够使用通用的 len()
来获取对象长度。
Magic Method
特殊方法。
__getitem & __len
len(obj)
andobj.__len__()
obj[key]
andobj.__getitem__(key)
Example 1:Card
1 | import collections |
collections.namedtuple(
, [ 用于构建只有少数属性而没有方法的对象。])
1 | # 牌堆类 |
''' French playing cards (jeu de cartes) are cards that use the French suits of trèfles (clovers or clubs♣), carreaux (tiles or diamonds♦), cœurs (hearts♥), and piques (pikes or spades♠). Each suit contains three face cards; the valet (knave or jack), the dame (lady or queen), and the roi (king). '''
可以看出,magic method
有以下好处:
1、对于任意类,直接使用 magic method
就可以完成类的标准操作,不需要考虑获取长度是.length()
还是.size()
等形式。
2、可以使用像 random
这样的内置库达到定义的类的标准操作,不需要重写方法。
由于 class deck
中 __getitem__
方法将操作 []
的对象定义为 self._cards
,因此__getitem__
方法对应的操作 []
能够在 class deck
上实现纸牌的切片,实际上是 self._cards
的切片。
1 | # deck的切片 |
同样,__getitem__
方法也导致 class deck
可迭代(__iter__
)
1 | # deck的迭代和反向迭代 |
如何实现排序?
按照点数:min = 2, max = A,按照花色:spades > hearts > diamonds > clubs
1 | # 定义纸牌的位置权重 |
洗牌功能使用
__setitem__
实现。
通过实现__len__
、__getitem__
,class FrenchDeck
就几乎等同于Python数据类型:列表,能够实现列表的排序、切片等操作。
Attention!
实现如此简便的纸牌类的关键有两个:
1、唯一内置隐藏属性对应着 List
这一数据类型,使得具有形式:deck = FrenchDeck()
、deck[:3]
,简洁;
2、重载 magic method
使得类的操作具有普遍性,Pathonic。
magic method
在除元编程以外的环境下很少用到,这是Python解释器调用的;
元编程:使用代码生成代码
对于__len__
来说,CPython中直接读取 PyVarObject
(可变内存对象)的C语言结构体的 ob_size
属性,更快;
运算符重载
Example 2:Vector
1 | class Vector: |
Vector类重定义了__add__
、__mul__
、__bool__
、__abs__
、__repr__
内置方法,使得利用运算符+
的向量的加法、利用*
的向量的数乘、利用abs()
的向量取模符合定义,并且可以利用bool()
判断向量是否为非零向量。
Attention!
- 使用
__repr__
表达对象:
1 | # 一般直接输出对象是这样的: |
“Difference between str and repr in Python”(http://stackoverflow.com/questions/1436703/differencebetween-str-and-repr-in-python)是 Stack Overflow 上的一个问题,Python 程序员 Alex Martelli 和 Martijn Pieters 的回答很精彩。
- 使用
__bool__
进行真值判断:
1 | return bool(self.x or self.y) |
比起使用以下语句更加高效
1 | return bool(abs(self)) |
需要注意的是,我们定义的对象总被认为是 True,除非该对象重载了 __bool__
或 __len__
。
调用顺序:__bool__
-> __len__
Magic Method List
非运算符魔术方法:
类别 | 方法名 |
---|---|
字符串/字节序列表示形式 | __repr__ 、 __str__ 、 __format__ 、 __byte__ |
数值转换 | __abs__ 、 __bool__ 、 __complex__ 、 __int__ 、 __format__ 、 __hash__ 、 __index__ |
集合模拟 | __len__ 、 __getitem__ 、 __setitem__ 、 __delitem__ 、 __comtains__ |
迭代枚举 | __iter__ 、 __reversed__ 、 __next__ |
可调用模拟 | __call__ |
上下文管理 | __enter__ 、 __exit__ |
实例创建和销毁 | __new__ 、 __init__ 、 __del__ |
属性管理 | __getattr__ 、 __getattribute__ 、 __setattr__ 、 __delattr__ 、 __dir__ |
属性描述符 | __get__ 、 __set__ 、 __delete__ |
跟类相关的服务 | __prepare__ 、 __instancecheck__ 、 __subcalsscheck__ |
运算符相关特殊方法:
类别 | 方法名及对应运算符 |
---|---|
一元运算符 | __neg__ -、__pos__ +、__abs__ |
比较运算符 | __lt__ <、__le__ <=、__eq__ ==、__ne__ !=、__gt__ >、__ge__ >= |
算术运算符 | __add__ +、__sub__ -、__mul__ *、__truediv__ /、__floordiv__ //、 __mod__ %、__divmod__ divmod()、__pow__ **、__round__ round() |
反向算术运算符 | __radd__ 、__rsub__ 、__rmul__ 、__rtruediv__ 、__rfloordiv__ 、__rmod__ 、__rdivmod__ 、__rpow__ |
增量赋值算术运算符 | __iadd__ 、__isub__ 、__imul__ 、__itruediv__ 、__ifloordiv__ <br 、__imod__ 、__ipow__ |
位运算符 | __invert__ ~、__lshift__ <<、__rshift__ >>、__and__ &、__or__ |
反向位运算符 | __rlshift__ 、__rrshift__ 、__rand__ 、__rxor__ 、__ror__ |
增量赋值位运算符 | __ilshift__ 、__irshift__ 、__iand__ 、__ixor__ 、__ior__ |
Advantages
为什么要使用魔术方法?
从FrenchDeck类的示例可以知道,Magic Method
使得自定义的类(数据模型,以特有方式构建、储存数据)能够在各种操作方法上表现得像内置数据类型一般,这种共同性是Python简洁明了、表达力强的原因之一。
Python语言参考手册,“Data Model”:https://docs.python.org/3/reference/datamodel.html
Martelli's Stack Overflow:http://stackoverflow.com/users/95810/alexmartelli
平衡的艺术
为什么将 len()
作为一个特殊方法?如果 x
是内置类型,那么 len(x)
会直接从C结构体中提取属性,不调用方法,速度非常快。如果不是内置类型,则使用 __len__
。因此 len()
作为特殊方法在内置类型的效率和语言的一致性上达成平衡。
数据结构
序列构成的数组
容器序列:储存对象的引用,可以存放多种类型的数据。
扁平序列:储存值,只能存放一种类型的数据。
可变序列:list、bytearray、memoryview、…
不可变序列:tuple 、str、bytes
列表推导式
list comprehension (listcomps)
1 | [ord(s) for s in words if ord(s) > 20] |
在
[]
、{}
、()
中,换行会被忽略。
也可以用 map
filter
完成:
1 | list(filter(lambda x:x > 20, map(ord, words))) |
哪一个效率更高?
通过以下程序可以粗略估计,列表推导式效率更高。
1 | import timeit |
输出:
1 | listcomp : 0.020 0.020 0.021 0.020 0.020 |
生成器表达式
generator comprehension (gencomps)
生成器 generator,逐个产出元素,能够节省内存。方便初始化元组、数组等序列。
圆括号
()
,作为参数时省略
1 | (ord(s) for s in words) |
元组拆包
元组重要的特点是“不可变”。
但是元组不仅储存了元素的信息,还储存了元素的位置信息,适合记录数据。
元组拆包 tuple unpacking:
- 平行赋值
1 | lax_coordinates = (33.9425, -118.408056) |
*
拆包可迭代对象
1 | t = (20, 8) |
*
获取剩余元素
1 | a, b, *rest = (1, 2, 3, 4) |
%
匹配元组元素
1 | traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')] |
- 嵌套元组拆包
1 | a, b, (c, d) = (1, 2, (3, 4)) |
关于占位符
_
:很多时候是一个很好的占位符,但是在国际化软件里,_
也是gettext.gettext
的常用别名。
具名元组
collections.namedtuple
:构建带字段名的元组和一个有名字的类。
用元组作为数据的记录,还少了字段的命名。因此使用具名元组。
1 | card = collections.namedtuple('Card', ['rank', 'suit']) |
具名元组还具有一些专有属性:
1 | from collections import namedtuple |
_fields
:字段名_make(iter)
:接受一个可迭代对象作为参数,构造具名元组_asdict()
:返回该实例的 collections.OrderedDict 形式。
序列切片
为什么切片会忽略最后一个元素?
1 | a = [1,2,3,4] |
- 方便知道切片中有多少元素
- 快速计算切片长度(末尾 - 起始)
- 用一个下标分为两部分
[:a] [a:]
切片对象 slice object
从 a
到 b
,以 c
为步长进行切片。
1 | A[a:b:c] |
1 | a:b:c => slice(a, b, c) |
多维切片
对于矩阵等二维数组,可以使用多维切片:
1 | [m:n, k:l] |
实质上是 __getitem__((m,n), (k,l))
省略号
1 | ... |
1 | a[i, ...] => a[i, :, :, :] |
切片实质上是对于原来序列中一部分元素的引用,修改切片会修改原序列。
序列的+和*
初始化一个由列表组成的列表?
1 | a = [[]] * 3 |
[[]] * n
实质上创建了 n 个指向同一个列表的引用。
该如何修改?
1 | a = [[] for i in range(3)] |
每次迭代都产生了新的列表实例。
序列的增量赋值
1 | a += b |
实质上是 __iadd__
和 __imul__
方法在起作用,如果第一个操作对象不具有这两个方法,那么就会调用 __add__
和 __mul__
。
Attention_1
__iadd__
和 __imul__
不改变变量的地址,而调用 __add__
和 __mul__
实质上执行了:
1 | a = a + b |
有一个赋值语句,将新的值赋给了新的对象,改变了 a 的地址。
Attention_2
1 | 1, 2, [3, 4]) a = ( |
会发生什么?
1 | Traceback (most recent call last): |
结果是既抛出了错误,又改变了不可变的元组!
查看字节码如下:
1 | "a[2] += [5, 6]") dis.dis( |
STORE_SUBSCR: a[2] = TOS
赋值操作抛出错误。
list.sort
与 sroted
区别:list.sort
为原址排序,不产生新的列表对象,返回 None
;而 sorted
返回新的排好序的列表。
连贯接口 fluent interface
参数:
reverse
:默认为False
,若为True
则降序输出key
:只有一个参数的函数,用于对序列中的每一个元素产生一个对比关键字。使用str.lower
实现忽略大小写的排序,使用len
实现根据字符串长度进行的排序……
算法使用的是 Timsort,会根据原始数据的顺序特点交替使用插入排序和归并排序。作者为 Tim Peters (Timbot),也是 Python之禅 的作者(
import this
)
bisect
操作已排好序的序列
bisect:二分,对半。
bisect
、insort
:利用二分查找算法在有序序列中查找或插入元素。
1 | # 在 haystack(干草垛)种搜索 needle(针)的位置 |
结果满足:把 needle 插入该位置后,haystack 还能够保持升序,
1 | import bisect |
输出:
1 | PS > python37 .\bisect_test.py |
1 | PS n> python37 .\bisect_test.py left |
还可以通过两个元素互相对应的有序列表,将一个列表中的值排序到对应的区间内 bisect_left
,再获取另一个列表中的对应值。
1 | def trans_grade(score, breakpoints=[60,70,80,90], grades="FDCBA"): |
bisect
将返回分数对应的第 i
个分数段,通过分数段序号取对应等级。
bisect = bisect_right
:所有的haystack[:i]
都小于等于 needle
bisect_left
:所有的haystack[:i]
都小于 needle,也就是遇上了相等元素,新元素放在前面
Attention!
str.format
,参数作为一个元组传入,format string 中的{m:nX}
表示:取参数元组中的第m
个,字段宽度为n
,按照数据类型X
右对齐输出。
.join()
接受一个序列类型,或者一个序列生成器。
sys.argv
获取命令行参数。
列表不是首选时
列表背后存储的是 int
、float
等对象,而数组 array
存储数字的字节表述。
如果需要频繁对序列进行先进先出操作,双端队列 deque
速度更快。
array
模块
1 | # 根据 类型码 和 初始值 创建存储为字节码的数组 |
需要注意的是,array.tofile
和 array.fromfile
用起来简单且快速,比从文本读入快多了。因为后者会调用 float
方法将每一行文字转换为浮点数。并且写入二进制文件 *.bin
时,占用的空间更少。不过, python3.4 之后,数组不支持就地排序,需要用 sorted
。
memoryview
内存视图。在不复制内容的情况下操作同一个数组的不同切片。处理大型数据集合时很重要。
1 | # 创建 短整型有符号整数 数组 'h' |
‘h’ 占 2 个字节,’B’ 占 1 个字节,因此使用
memvoryview.cast('B')
时,每个短整型有符号数字被划分为高位 + 低位。例如:-2
的字节码划分为高位和低位就是 254、255。
NumPy
和 SciPy
另有笔记进行学习。
双端队列
普通列表通过 append
和 pop
模拟栈。
collections.deque
支持更快速、线程安全的队列类型。maxlen
参数指明队列容纳的元素个数。
rotate(n)
:将队列的右侧(n>0)n个元素移动到左侧,反之移动到右侧。1
2
3
4
5
6
710), maxlen = 10) dq = collections.deque(range(
dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
3) dq.rotate(
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
-4) dq.rotate(
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)appendleft
:左侧添加元素,若队列已满则删除最右侧元素。1
2-1) dq.appendleft(
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)extend
:将序列中的元素依次添加到右侧,挤出左侧元素。1
211,22,33]) dq.extend([
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)extendleft
:将序列中的元素依次添加到左侧,挤出右侧元素。1
2>>> dq.extendleft([44,55,66,77])
deque([77, 66, 55, 44, 3, 4, 5, 6, 7, 8], maxlen=10)
append 和 popleft 都是原子操作,也就说是 deque 可以在多线程程序中安全地当作先进先出的栈使用,而使用者不需要担心资源锁的问题。
其他队列
queue
:同步类Queue、LifoQueue 和 PriorityQueue
multiproessing
:进程通信用、JoinableQueue
用于任务管理
asyncio
:包括前两者提供的 Queue,用于异步编程
heapq
:堆队列、优先队列
字典与集合
高度依赖散列表。
泛映射类型
collections.abc
中有 Mapping
和 MutableMapping
两个抽象基类,为 dict
和其他类似的类型定义了形式接口。
dict
继承自 object
对象,使用 __mro__
判断方法解析顺序,可以知道 dict
的基类:
1 | dict.__mro__ |
但是抽象基类能够注册虚拟子类。在 import collections
时,执行了 MutableMapping.register()
,将 dict
注册成了自己的虚拟子类,因此:
1 | from collections.abc import MutableMapping |
而Python代码版本的内置类型 dict
则实现了C代码版本的 dict
的所有接口,可以构建自己的映射类型:
1 | >> from collections import UserDict |
字典推导
1 | DIAL_CODES = [ |
setdefault
处理找不到的 key
快速失败理念:快速抛出异常。
d[k]
找不到正确的 key
的时候,Python会抛出异常。可以使用 d.get(k, default)
来给找不到的 key
一个默认的返回值。
1 | 1:2, 3:4} d = { |
但是这样无法很好地更新或者添加键值对。
下面的例子从文本文件中统计各个单词出现的位置(开头字母的行号和列号)。将新出现的键放入字典时,涉及到了两次查询和依次列表 append
操作,代码表达力不强。
1 | import sys |
使用 setdefault
有更好的写法:
1 | my_dict.setdefault(key, []).append(new_value) |
defaultdict
为找不到的 key
创建默认值
1 | import collecitons |
default_factory
只在__getitem__
里调用,只能使用my_dict[key]
,而使用my_dict.get(key)
会返回None
。
特殊方法 __missing__
__missing__
在映射类型找不到 key
的时候发挥作用,基类dict
没有定义该方法。与 default_factory
相同,__missing__
只会在 __getitem__
中调用,对于 get
或 __contains__
方法没有影响。
1 | class StrKeyDict0(dict): |
上述代码定义了一个用字符串作为键值的字典类 StrKeyDict0
。增加了 __missing__
方法,重写了 get
方法和 __contains__
方法。
当使用 d[key]
(也即 d.__getitem__(key)
)时,key
找不到的情况有两种:
key
是字符串但不在字典的键中;key
不是字符串。
上述情况都会调用 __missing__
方法,因此在 __missing__
方法中,当 key
不是字符串时,转化为字符串后再进行 __getitem__
调用。
注意,
isinstance
判断是必须的,如果不判断,则产生无限递归。> graph LR > subgraph "isinstance" > C["__getitem__(key)"] --"Failed"--> D["__missing__(key)"] > D --"str"--> E["Raise ERROR"] > D --"NO str"--> F["__getitem__(str(key))"] > end > subgraph "NO isinstance" > A["__getitem__(key)"] --"Failed"--> B["__missing__(key)"] > B --"Call"--> A > end >
上述代码中,当使用 get()
方法时,查找工作实际上被委托给了 __getitem__
方法,而 __getitem__
又会通过 __missing__
方法多给一次机会。
当使用 __contains__
方法时,若传入的 key
不是字符串,那么会转化为字符串再加以判断。
不使用
k in d
,反而使用k in d.keys()
,这是因为前者会造成__contains__
的递归调用。> graph LR > subgraph "k in d.keys()" > C["d.__contains__(key)"]--"return"--> D["k in d.keys()"] > D --"Call"--> E["d.keys().__contains__(keys)"] > end > subgraph "k in d" > A["d.__contains__(key)"]--"return"--> B["k in d"] > B --"Call"--> A > end >
Attention_1
1 | k in my_dict.keys() |
由于使用了 “Dictionary View Object”,上述查询在 Python 3 中是很快速的。
my_dict.keys()
返回的不是像 Python 2 中的列表,而是一个“视图”,类似于一个集合。
字典的变种
OrderedDict
记录添加键的顺序。使用 popitem
方法默认删除并返回字典的最后一个元素;popitem(last = False)
删除第一个添加的元素。
Python 3 中,字典的键实质上是有序的,但是基类
dict
的popitem
没有last
参数。
ChainMap
容纳多个不同的映射对象,在执行键的查找时,这些对象会逐个被查找,直到找到为止。
能够用它作为嵌套作用域的上下文:
1 | import builtins |
Counter
计数器,可用于文章的单词计数。每次更新一个键的时候都会增加对应的计数器。
可以使用 +
、-
运算符合并记录。
most_common(n)
返回前 n 个计数最多的键和它的计数。
UserDict
Python 实现的基类 dict
。用于被用户自定义的子类继承。
继承 UserDict
为什么不继承
dict
?主要因为内置类型的某些实现走了捷径,继承后不得不自己重写某些方法。
UserDict
是 dict
的 Python 实现,其中他有一个叫做 data
的属性是 dict
的实例,用于最终储存数据。这样做引入了新的操作对象,避免了对自身的操作,也就不会造成方法的递归。
1 | import collections |
__contains__
和 __setitem__
都是在对 self.data
属性进行操作,从而避免了对 self
操作造成的递归问题。
继承关系:
graph LR A["StrKeyDict"] --> B["UserDict"] --> C["MutableMapping"] --> D["Mapping"]
两个继承而来的实用方法:
MutableMapping.update
:利用传入的参数构造新的实例,背后使用的是 self[key] = value
。
Mapping.get
:基类 dict
中,__getitem__
会调用 __missing__
方法,而 get
不会。但是有时候,我们希望二者的行为是一致的,都会调用 __missing__
,这就要求我们重写 get
,把具体实现委托给 __getitem__
。而 Mapping.get
方法就是按照这样的思路定义的,省去了我们重写的工作。
TransformDict
不可变的映射类型
映射类型都是可变的,但是有时候我们不希望用户能够修改一个映射。可以通过 types.MappingProxyType
产生一个只读的映射视图,用户不能修改映射视图,但是原映射修改后,它的映射视图也会自动改变。
1 | from types import MappingProxyType |
集合
set
或 frozenset
空集必须写成
set()
,字面量则可以写成{1, 2}
基本操作:
示例 | 意义 |
---|---|
a | b |
并集 |
a & b |
交集 |
a - b |
差集 |
a ^ b |
对称差集 |
集合推导
setcomps
新建一个 Latin-1 字符集合,该集合中的每个字符的 Unicode 名字里都有 “SIGN” 单词:
1 | from unicodedata import name |
Attention_1!
unicodedata.name
:获取字符的名称
集合的操作
以交集运算为例:
s & z
==s.__and__(z)
z & s
==s.__rand__(z)
==s.intersecton(it, ...)
s &= z
==s.__iand__(z)
==s.intersection_update(it, ...)
比较运算:用偏序关系定义 >
、<
、>=
、<=
。s.isdisjoint(z)
查看而这是否有共同元素。
其他功能:
s.add(e)
元素添加s.clear()
移除所有元素s.copy()
浅复制s.discard(e)
若存在元素 e,则删除s.pop()
移除一个元素并返回它的值s.remove(e)
若存在元素 e,则删除;若不存在,则抛出异常
效率
字典和集合的查找效率:集合交集运算 > 集合内循环查找 > 字典内循环查找 > 列表内循环查找
字典背后是散列表,相当于一个稀疏数组,每个元素叫做表元(bucket)。
Python会保证大约三分之一的表元是空的,因此超过阈值时,散列表会被复制到新的大空间内。
因此,散列表用空间换时间,内存开销巨大。
往字典中添加新键,如果需要扩容,则需要复制散列表到新空间,这一过程可能产生散列冲突,导致键的次序打乱。
同理,不要同时字典进行迭代和修改。
扫描并修改一个字典:迭代获取需要修改的内容,放入新字典;迭代结束后更新原字典。
.keys()
,.items()
和.values()
都是返回视图,是动态变化的。
文本和字节序列
以字符 é
为例:
1 | 'é' s = |
graph TD A[字符 é] === B["唯一编码(Unicode): U+00e9"] B === C["UTF-8 字节序列编码:b'\xc3\xa9'"] C === D["物理储存:11000011 10101001"]
元编程
动态属性与特性(property)
特性:property,不改变类接口,使用存取方法修改数据属性。
属性:attribute
多进程
多线程
threading 模块
CPython 具有 GIL 锁。GIL 锁是互斥锁,在解释器层保护共享数据。
CPython 解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。
因此多线程适合用于 I/O 密集型任务,在 I/O 的等待时段,CPU 没有计算任务,因此其他线程可以执行。
附录
array
Type Code
Type code | C Type | Minimum size in bytes |
---|---|---|
b | signed integer | 1 |
B | unsigned integer | 1 |
u | character | 2 (see note) |
h | integer | 2 |
H | unsigned integer | 2 |
i | signed integer | 2 |
I | unsigned integer | 2 |
l | signed integer | 4 |
L | unsigned integer | 4 |
q | signed integer | 8 (see note) |
Q | unsigned integer | 8 (see note) |
f | floating point | 4 |
d | floating point | 8 |
NOTE: The 'u' typecode corresponds to Python's unicode character. On narrow builds this is 2-bytes on wide builds this is 4-bytes. NOTE: The 'q' and 'Q' type codes are only available if the platform C compiler used to build Python supports 'long long', or, on Windows, '__int64'.
Methods
- append() -- append a new item to the end of the array
- buffer_info() -- return information giving the current memory info
- byteswap() -- byteswap all the items of the array
- count() -- return number of occurrences of an object
- extend() -- extend array by appending multiple elements from an iterable
fromfile()
-- read items from a file object- fromlist() -- append items from the list
- frombytes() -- append items from the string
- index() -- return index of first occurrence of an object
- insert() -- insert a new item into the array at a provided position
- pop() -- remove and return item (default last)
- remove() -- remove first occurrence of an object
- reverse() -- reverse the order of the items in the array
tofile()
-- write all items to a file object- tolist() -- return the array converted to an ordinary list
- tobytes() -- return the array converted to a string