流畅的 Python 读书笔记(一)

用了一段时间的 Python, 觉得还是有必要读一下《流畅的Python》这本书,它虽然是基于 Python 3.4 的,但 Python 自身的很多特性希望了解的更多,更深,或巩固,或扫扫死角。


对于少量属性的对象可以用 collections.namedtuple 快速构建一个类  Card = collections.namedtuple('Card', ['rank', 'suit']), 用 type(Card) 看到的就是一个  class, 第一个参数 Card 是类名,第二个参数列表里是属性名,然后用 card = Card('7', 'diamonds') 创建一个实例。PyCharm 也能正确识别出 Card 构建与使用对象时的属性 rank 和 suit.

现代从 Python 3.7 开始引入了 @dataclasses.dataclass 比 namedtuple 要方便些
@dataclasses.dataclass
class Card:
    rank: str
    suit: str = None
card = Card(rank='7', suit='diamonds')

或者用 pydantic 的 BaseModel 都比先前的 namedtuple 好用
1class Card(pydantic.BaseModel):
2    rank: str
3    suit: Optional[str] = None

pydantic 的 BaseModel 还自带校验功能(Python 3.7 的 @dataclass 是具备的),如果声明的 suit: str, 那么 Card(rand='8', suit=None) 也通不过。pydantic 也提供了 @pydantic.dataclasses.dataclass, 与 BaseModel 的效果差不多

以上三种方式(严格说四种), 都能为指定的属性生成相应的构造函数和 __repr__ 等函数

Python 的 dunder 函数

Python 的 dunder 方法与语法上的写法对应,如 abc[0] 对应  __getitem__ 方法,可用 len(abc) 的对象必须是实现了 __len__ 方法。

Python 的 random.choice(xyz) 也会调用 xyz 的 __len__(self) 和 __getitem__(self, position) 函数, 相当于是 xyz[random.randint(0, len(xyz)-1)]

切片操作 xyz[1:] 也会找到 __get_item__(self, position) 函数。还有其他的一些函数,如 reversed, sorted() 也是依赖于由置的 dunder 函数,这也是 Python 鸭子类型的好处。if 3 in xyz 的判断对应调用 __contains__ 方法。

可被  for i in xzy 迭代的对象,它的类叫做可迭代类, 必须是实现了 __getitem____iter__ 函数,以下用 __iter____next__ 实现一个迭代类
 1import random
 2
 3class Xyz:
 4    def __iter__(self):
 5        return self
 6
 7    def __next__(self):
 8        x = random.randint(1, 100)
 9        if x > 80:
10            raise StopIteration()
11        return x
12
13
14for i in Xyz():
15    print(i)

这个 Xyz 用 isinstance(Xyz(), collections.Iterable) 判断是 True,然而只实现了 __getitem__ 也可用 for i in xyz 迭代,但是却不 collections.Iterable 实例

自定义的类中不要去写 dunder 函数,dunder 函数约订是不直接调用,而是通过特殊函数或形式,如 len() 或 for in 来调用。

通过 dunder 函数,实现运算符的重载变得简单,如 +, -, *, / 都有相应的 dunder 函数,用 dir 查看一个可支持四则运算的对象就能列出那些 dunder 函数来,如 dir(1) 就会看到它们
>>> dir(1)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
上面还包括了更多的奥秘, 时常用 dir(x) 查看实例的 dunder 函数很实用, 比如dir(函数) 就能看到 __call__ 函数,所以一个类实现了该函数就能把实现以函数的形式来调 -- Xyz()()。 或者想用自定义的类实现某种 Python 内置类型的行为,就用 dir 查看它显著的 dunder 函数。

Python 是支持鸭子类型的,一个 Python 实例有没有什么特性,或能不能支持某种操作不是由它实现了什么接口或继承了什么类决定的,而是取决于它有没有实现某些 dunder 函数。

__repr____str__ 两函数在之前一篇博客中有讨论,再回顾一下,__repr__'%s or %r' % (xyz, xyz) 调用,print  或 str() 时会先尝试调用 __str__, 但找不到 __str__ 时还是会调用 __repr__, 所以 __repr__ 守住最后一关,只要 __repr__ 就足够。

布尔类型值的判断,bool(x) 先判断 x.__bool__() 的结果,如果它不存在,再尝试调用 x.__len__(), 返回 0 则为 False, 这也是为什么空字符串,空集合是 False.

x += 1 之类增量赋运算对应的函数分别是 __iadd__, __isub__, __imul__, itruediv__, __ifloordiv__, __imod__, __ipow__, 实现一个能进行增量运算的类
1class Xyz:
2    _n = 1
3    def __iadd__(self, other):
4        self._n = self._n + other
5        return self
6
7xyz = Xyz()
8xyz += 10
9print(xyz._n)  # 11

其实 Python 内部普遍使用 dunder 函数的方式在其他语言里也常见,比如 Ruby, Groovy, Scala 通常会叫做 Magic 函数。

CategoryMethod names
String/bytes representation__repr__, __str__, __format__, __bytes__
Conversion to number __abs__, __bool__, __complex__, __int__, __float__, __hash__, __index__
Emulating collections__len__, __getitem__, __setitem__, __delitem__, __contains__
Iteration __iter__,  __reversed__, __next__
Emulating callables__call__
Context management__enter__, __exit__
Instance creation and destruction__new__, __init__, __del__
Attribute management__getattr__, __getattribute__, __setattr__, __delattr__, __dir__
Attribute descriptors__get__, __set__, __delete__
Class services__prepare__,__instancecheck__,__subclasscheck__
Unary numeric operators__neg__-,  __pos__+,  __abs__abs()
Rich comparison operators__lt__>, __le__<=, __eq__==, __ne__!=, __gt__>, __ge__>=
Arithmetic operators__add__+, __sub__-, __mul__*, __truediv__/, __floordiv__//, __mod__ %, __divmod__divmod(), __pow__** orpow(), __round__round()
Reversed arithmetic operators__radd__, __rsub__, __rmul__, __rtruediv__, __rfloordiv__, __rmod__, __rdivmod__, __rpow__
Augmented assignment arithmetic operators__iadd__, __isub__,__imul__,__itruediv__,__ifloordiv__,__imod__, __ipow__
Bitwise operators__invert__~, __lshift__<<, __rshift__>>, __and__&, __or__|, __xor__ ^
Reversed bitwise operators__rlshift__, __rrshift__, __rand__, __rxor__, __ror__
Augmented assignment bitwise operators__ilshift__,__irshift__,__iand__,__ixor__,__ior__
 

关于 Python 中的序列类型

序列有几种方式的分类,

  1. 容器序列:list, tuple 和 collections.deque, 可以放不同的类型
  2. 扁平序列: str, bytes, bytearray, memoryview 和 array.array, 只能放同一种类型数据本身,而非引用,所以它是连续的内存空间
  3. 可变序列:list, bytearray, array.array, collection.deque 和 memoryview
  4. 不可变序列:tuple, str 和 bytes

本人平时基本还没用过 deque, bytearray, array.array, 以及  memoryview. 要用内存字节时都是用的 io.BytesIO。以后应该适当的考虑用其他的类型

bytearray
1byte_array = bytearray([2,3,5,7])
2byte_array.append(11)
3print(byte_array)  # bytearray(b'\x02\x03\x05\x07\x0b')

array.array
1from array import array
2
3ff = array('u', 'hello') # type, b, B, u, h, H, i, I, l, L, q, Q, f, d
4for c in ' world':
5    ff.append(c)
6print(ff.tolist()) # ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']

array.array 使用起来很奇怪,创建一个 array 时必须指定一个类型码,所以它比 Python 的 list 更高效。它像 C 语言的数组一样精简。

memoryview
1mv = memoryview(bytearray('hello'.encode()))
2mv[1] = 2
3print(mv.tolist()) # [104, 2, 108, 108, 111]
4
5mv1 = memoryview('hello'.encode())
6mv1[1] = 2  # TypeError: cannot modify read-only memory

以上这三种类型,bytearray, array.array 和 memoryview 还真的不怎么好用,难怪少见

Python 中用 ord()chr 在 ASCII 码与字符间转换,相应的用 unichr()

推导与生成器

Python 的列表推导很快捷,但复杂的多行的推导用 for 循环可能又更合适,自己把握度。Python 会忽略 [], {} 和 () 的换行,所以在那里面不需要续行符 \

推导中加入条件表达式就有了 map/filter 的功能 [x for x in [1,2,3] if x >=2]。 还有同时对多个列表的推导
1abc = [(a, b, c) for a in [1, 2, 3] for b in ['a', 'b', 'c'] for c in ['x', 'y']]
2for i in abc:
3    print(i)
(1, 'a', 'x')
(1, 'a', 'y')
(1, 'b', 'x')
(1, 'b', 'y')
(1, 'c', 'x')
(1, 'c', 'y')
(2, 'a', 'x')
(2, 'a', 'y')
(2, 'b', 'x')
(2, 'b', 'y')
(2, 'c', 'x')
(2, 'c', 'y')
(3, 'a', 'x')
(3, 'a', 'y')
(3, 'b', 'x')
(3, 'b', 'y')
(3, 'c', 'x')
(3, 'c', 'y')
这就是迪卡尔乘积

推导的作用只有一个,就是生成列表,它会立即建立一个完整的列表,而生成器表达式遵循了迭代器协议,只有在迭代生成器的时候才逐个的产出元素,所以它省内存。

生成器表达式与推导的语法差不多,只是把方括号换成圆括号, 下面是生成器的一些例子
 1abc = (x+1 for x in [1, 3, 5])
 2print(abc) # <generator object <genexpr> at 0x106486820>
 3for i in abc:
 4    print(i)  # 2\n\4\n6\n
 5
 6print([i for i in abc])  # []
 7print(list(abc)) # []
 8
 9abc = (x+1 for x in [1, 3, 5])
10print([i for i in abc])  # [2, 4, 6]
11
12abc = (x+1 for x in [1, 3, 5])
13print(list(abc)) # [2, 4, 6]
14
15def foo(gen):
16    for i in gen:
17        print(i) # 2\n\4\n6\n
18
19foo(x+1 for x in [1, 3, 5])

生成器被用完后不能被再次使用,当函数只有一个参数时,传入的生成器可以省略两边的圆括号

温习元组(tuple)拆包

元组拆包用的很频繁,也是 Python 带给编程简明快捷的一个特性
 1print('%s %s' % (1, 2)) # 1 2
 2
 3x = (2, 4)
 4a, b = x   # a=2, b=4
 5
 6a, b = b, a  # a=4, b=2
 7
 8pow(*x)  # 16
 9
10_, filename = os.path.split('/Users/yanbin/.ssh/idrsa.pub') # filename=idrsa.pu
11
12a, b, *rest = [0, 1, 2, 3] # a=0, b=1, rest=[2,3]
13a, *body, b = [0, 1, 2, 3] # a=0, body=[1,2], b=3
14
15a, (b, c), d = [0, (1, 2), 3] # a=0, b=1, c=2, d=3 嵌套元组拆包

具名元组(namedtuple) 的两种写法
1# City = namedtuple('City', ['name', 'country'])
2City = namedtuple('City', 'name country')
3tokyo = City('Tokyo', 'JP')

它的实例比普通的类例要小一些。

切片

Python 切片是按下标进行的,abc[2:4], 从下标的左边算,[1,2,3,4,5,6],不包含右端的数字所在的索引,所以说 Python 的切片忽略最后一个元素,除非不指定右端的值,如  abc[2:]。

s[a:b:c] 形式的切片,a 与 b 之间以 c 为间隔取值
1s='bicycle'
2s[::3] # bye
3s[::-1] # elcycib
4s[1:5:-2] #ic
5s.__getitem__(slick(1, 5, -2)) # 和上面是一样的
6
7s[slice(2, None))
8s[slice(2,5)]

负值意味着反向取值,s[::-1] 完成了字符串的倒转。切片实际调用的时 __getitem__(slice(start, stop, step)), 也可以直接使用 s[slice(2,5)] 的形式。

numpy 中使用多维切片与 ellipsis(...), numpy.ndarray 的二维切片 a[n:n, k:l], 省略号作为函数参数 f(a, ..., z),  切片中  a[i:...]

切片的赋值,s[2:5] = [20, 30] 会把原列表中 2:5 区间的三个值换成 [20, 30] 两个值

 a += b 首先试图调用 __iadd__ 函数,没有的话调用 __add__ 函数
 1class Xyz:
 2    def __init__(self, i):
 3        self.i = i
 4
 5    # def __iadd__(self, other):
 6    #     print('__iadd__')
 7    #     self.i += other.i
 8    #     return self
 9
10    def __add__(self, other):
11        print('__add__')
12        return Xyz(self.i + other.i)
13
14a = Xyz(1)
15b = Xyz(2)
16a += b
17print(a.i)

__iadd____add__ 都启用的话,a += b 调用的是 __iadd__ 方法,注释掉 __iadd__ 方法就调用 __add__ 方法,全注释,会提示 TypeError: unsupported operand type(s) for +=: 'Xyz' and 'Xyz'.

Python 中对字符串的 += 操作可能得到的是同一个地址,有点像 Redis 的字符串实现,但 Python 的字符串仍然是不可变的,因为不能 s[1] = 'c' 改变其中的值。

list 的 sort() 方法返回值是 None, 是为了提醒该方法不会新建一个列表,而是就地修改的元素,许多其他的函数也遵循了这一惯例。相应的 sorted() 函数返回了排序后的值,也意味着内部未修改。

二分查找和插入的类 bisect(binary section)

 相应的方法有,bisect(), bisect_left(), bisect_right(): 查找值要被插入的位置, insort(), insort_left(), insort_right() 插入值到正确的位置,要求被操作的列表必须是有序的
 1import bisect
 2
 3a = [0, 1, 2, 5, 5, 8, 10]
 4
 5bisect.bisect(a, 5)  # 5
 6bisect.bisect_left(a, 5) # 3
 7bisect.bisect_right(a, 5) # 5
 8bisect.bisect(a, 11) # None
 9
10bisect.bisect(a, 9) # 6
11
12bisect.insort(a, 9)  # [0, 1, 2, 5, 5, 8, 9, 10]
13bisect.insort_left(a, 5) # [0, 1, 2, 5, 5, 8, 9, 10]
14bisect.insort_right(a, 5) # [0, 1, 2, 5, 5, 5, 8, 9, 10]
15bisect.insort(a, 11) # [0, 1, 2, 5, 5, 5, 5, 8, 9, 10, 11]

left, right 是确定位置是在找到值的左边还是右边,不带 left, right 的 bisect()  和 insort() 默认是在右边。上面插入的 5 分不出是左边还是右边,必须把 __repr__, 和比较函数区分开来,比如下面的代码就能看出 insort_left() 和 insort_right() 的区别

 1import bisect
 2
 3
 4class Xyz:
 5    def __init__(self, i, desc):
 6        self.i = i
 7        self.desc = desc
 8
 9    def __repr__(self):
10        return f'{self.i}:{self.desc}'
11
12    def __lt__(self, other):
13        return self.i < other.i
14
15
16abc = [Xyz(1, '11'), Xyz(2, '22')]
17bisect.insort_left(abc, Xyz(2, '22-1'))
18print(abc) # [1:11, 2:22-1, 2:22]
19
20abc = [Xyz(1, '11'), Xyz(2, '22')]
21bisect.insort_right(abc, Xyz(2, '22-1'))
22print(abc) # [1:11, 2:22, 2:22-1]

bisect() 和 insort() 及它们的 left, right 的变种方法的完全参数是(以 bisect 为例)
bisect(a, x, lo, hi)   # lo, hi 用来控制查询范围
列表很方便,何时不用列表,当存放 1000 万个浮点数,array 效率就高多了,因为它不是存 float 对象而是字节表述,因为 array 是有类型的。频繁的先进先出,用 deque。经常 contains 比较操作用 set, 这和 Java 是一样的。

创建一个 array 时需要指定元素的类型,所以就它就 C 里的数组,如 char[], int[], float[] 等,存储的不在是有额外元数据信息的对象,所以也就节省空间。array.tofile() 和 array.fromfile() 能非常高效的处理数组到文件和从文件序列化出数组.

NumPy, 以 NumPy 为基础的 SciPy, Pandas 和 Blaze  又是以 NumPy 和 SciPy 为基石。

利用 list 的  append  和 pop 可以模拟出栈或队列来,但它们操作的效率很低,有队列需求就可以考虑用 collections.deque, 并且它是线程安全的。利用 deque 的 maxlen, rotate 可试着做一个环形队列, deque 在插入时队列满后会清除另一端的元素腾出空间。Python  还提供其他几种 queue.Queue, queue.LifoQueue, queue.PriorityQueue 线程安全的专用队列, 它们都支持 maxsize 参数,在队列满了后不会自动删除旧元素,而是阻塞直到另外的线程移除了元素。

专用的 multiprocessing.JoinableQueue, asyncio 中有自己的 Queue, LifoQueue, PriorityQueue。 特殊的 heapq 尚未遇到有什么具体的应用场景。 永久链接 https://yanbin.blog/fluent-python-reading-notes-1/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。