第1章 Python 数据结构 一摞 Python 风格的纸牌 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import collections Card = collections.namedtuple('Card' , ['rank' , 'suit' ])class FrenchDeck : ranks = [str (n) for n in range (2 , 11 )] + list ('JQKA' ) suits = 'spades diamonds clubs hearts' .split() def __init__ (self ): self ._cards = [Card(rank, suit) for suit in self .suits for rank in self .ranks] def __len__ (self ): return len (self ._cards) def __getitem__ (self, position ): return self ._cards[position]
上面这个代码示例惊艳到我了,让我对 Python 的类刮目相看;此刻我才开始开始意识到内置方法的存在;
例如它仅仅因为实现了 _len_ 和 _getitem_ 两个特殊方法,便使得这个类能够自动使用 Python 的内置函数,例如
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 31 32 deck = FrenchDeck() len (deck) deck[0 ] deck[-1 ]from random import choice choice(deck) deck[:3 ] deck[12 ::13 ]for card in deck: print (card) Card("Q" , "hearts" ) in deck suit_values = dict (spades=3 , hearts=2 , diamonds=1 , clubs=0 )def spades_high (card ): rank_value = FrenchDeck.rands.index(card.rand) return rank_value * len (suit_values) + suite_values[card.suit]for card in sorted (deck, key = spades_high): print (card)
如何使用特殊方法 特殊方法是为了给解释器调用,从而实现一些内置的功能,而不是为了自己调用;如果是自己调用,那么只需写普通方法即可,无须写特殊方法;
另外,也尽量避免随意添加特殊方法,因为有可能出解释器内置的方法出现命名冲突,导致发生不可预知的情况;
特殊方法还可以用来重载运算符,例如转成字符串,加号,乘号,取绝对值等,示例如下:
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 from math import hypotclass Vector : def __init__ (self, x=0 , y=0 ): self .x = x self .y = y def __repr__ (self ): return 'Vector(%r, %r)' % (self .x, self .y) def __abs__ (self ): return hypot(self .x, self .y) def __bool__ (self ): return bool (self .x or self .y) def __add__ (self, other ): x = self .x + other.x y = self .y + other.y return Vector(x, y) def __mul__ (self, scalar ): return Vector(self .x * scalar, self .y * scalar)
特殊方法一览 特殊方法挺多的,有80 多个,其中有 40 个多用于实现算术运算、位运算和比较操作;
为什么 len 不是普通 方法 len 的目的是为了读取对象的长度,对于内置类型的对象,它们是用 C 语言的 struct 表示的,struct 里面有个属性存储着长度值,因此在这种情况下,len 会直接去读取 struct 的长度值,而不是调用 _len _ 来计算长度;主要是出于性能考量
本章小结 通过实现特殊方法,能够让自定义类型表现跟内置类型一样,从而能够直接使用 Python 的很多内置函数,让代码更容易阅读;
第2章 序列构成的数组 内置序列类型 Python 有两种序列类型,一种存放的是对象的引用,因此它可以容纳任何类型,称为容器序列;一种存放值,而不是引用,因此只能放相同类型的值,称为扁平序列;
序列按照能否修改,可分为可变序列和不可变序列
可变序列(Mutable Sequence):list, bytearray, array.array, collections.deque, memoryview
不可变序列(Sequence):tuple, str, bytes
列表推导和生成器表达式 列表推导式(list comprehension)非常适合用来创建新的列表,这种写法更容易读懂;如果列表推导太长,则可以改用传统的 for 循环;
1 2 3 4 5 6 >>> colors = ['black' , 'white' ]>>> sizes = ['S' , 'M' , 'L' ]>>> tshirts = [(color, size) for color in colors for size in sizes]>>> tshirts [('black' , 'S' ), ('black' , 'M' ), ('black' , 'L' ), ('white' , 'S' ), ('white' , 'M' ), ('white' , 'L' )]
生成器表达式用来其他类型的序列;生成器表达式使用圆括号,而不是方括号;
1 2 3 >>> symbols = '$¢£¥€¤' >>> tuple (ord (symbol) for symbol in symbols) (36 , 162 , 163 , 165 , 8364 , 164 )
1 2 3 >>> import array>>> array.array('I' , (ord (symbol) for symbol in symbols)) array('I' , [36 , 162 , 163 , 165 , 8364 , 164 ])
生成器表达式每次产生一个运算结果,而不是一下生成整个列表,这样可以节省内存,尤其是元素多的时候,非常明显
1 2 3 4 5 6 7 8 9 10 11 >>> colors = ['black' , 'white' ]>>> sizes = ['S' , 'M' , 'L' ]>>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes): ... print (tshirt) ... black S black M black L white S white M white L
元组不仅仅是不可变的列表 元组是不可变列表,但其实它存放的数据,也可以基于顺序来表达不同的含义
1 2 traveler_ids = [('USA' , '31195855' ), ('BRA' , 'CE342567' ), ('ESP' , 'XDA205856' )]
1 2 >>> lax_coordinates = (33.9425 , -118.408056 )>>> latitude, longitude = lax_coordinates
1 2 3 4 >>> divmod (20 , 8 ) (2 , 4 )>>> t = (20 , 8 )>>> divmod (*t)
星号* 可用来存放拆包的余下元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 >>> a, b, *rest = range (5 ) >>> a, b, rest (0 , 1 , [2 , 3 , 4 ]) >>> a, b, *rest = range (3 )>>> a, b, rest (0 , 1 , [2 ])>>> a, b, *rest = range (2 )>>> a, b, rest (0 , 1 , [])>>> a, *body, c, d = range (5 )>>> a, body, c, d (0 , [1 , 2 ], 3 , 4 )>>> *head, b, c, d = range (5 )>>> head, b, c, d ([0 , 1 ], 2 , 3 , 4 )
嵌套元组拆包 1 2 3 4 5 6 7 8 9 10 11 metro_areas = [ ('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 )), ]for name, cc, pop, (latitude, longitude) in metro_areas: if longitude <= 0 : print (fmt.format (name, latitude, longitude))
具名元组 1 2 3 4 5 6 7 8 9 10 11 12 >>> from collections import namedtuple>>> City = namedtuple('City' , 'name country population coordinates' ) >>> tokyo = City('Tokyo' , 'JP' , 36.933 , (35.689722 , 139.691667 )) >>> tokyo City(name='Tokyo' , country='JP' , population=36.933 , coordinates=(35.689722 ,139.691667 )) >>> tokyo.population ➌36.933 >>> tokyo.coordinates (35.689722 , 139.691667 )>>> tokyo[1 ]'JP'
具名元组有一些内置的属性和方法,包括:
_fields 属性,用来查看所有字段的名称
_make() 方法,用来创建实例
_asdict() 方法,用来返回 OrderDict
1 2 3 4 5 6 7 >>> City._fields ('name' , 'country' , 'population' , 'coordinates' )>>> LatLong = namedtuple('LatLong' , 'lat long' )>>> delhi_data = ('Delhi NCR' , 'IN' , 21.935 , LatLong(28.613889 , 77.208889 ))>>> delhi = City._make(delhi_data) >>> delhi._asdict() OrderedDict([('name' , 'Delhi NCR' ), ('country' , 'IN' ), ('population' , 21.935 ), ('coordinates' , LatLong(lat=28.613889 , long=77.208889 ))])
相对列表,元组没有添加和删除元素的方法,其他方法则都差不多;
切片 切片有个特殊的用法,即 s[a : b : c],它表示在 a ~ b 的区间内,以 c 为间隔取值;即 s[start : stop : step]
1 2 3 4 5 6 7 8 9 >>> s = 'bicycle' >>> s[::3 ] 'bye' >>> s[::-1 ] 'elcycib' >>> s[::-2 ] 'eccb' >>> deck[12 ::13 ]
切片有个很有意思的用法,它可以让代码更易读
1 2 3 4 5 6 7 8 9 10 11 12 13 >>> SKU = slice (0 , 6 )>>> DESCRIPTION = slice (6 , 40 )>>> UNIT_PRICE = slice (40 , 52 )>>> QUANTITY = slice (52 , 55 )>>> ITEM_TOTAL = slice (55 , None )>>> for item in line_items:... print (item[UNIT_PRICE], item[DESCRIPTION]) ... $17.50 Pimoroni PiBrella $4.95 6mm Tactile Switch x20 $28.00 Panavise Jr. - PV-201 $34.95 PiTFT Mini Kit 320x240
切片也可用来赋值,或者删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 >>> l = list (range (10 ))>>> l [0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ]>>> l[2 :5 ] = [20 , 30 ] >>> l [0 , 1 , 20 , 30 , 5 , 6 , 7 , 8 , 9 ]>>> del l[5 :7 ] >>> l [0 , 1 , 20 , 30 , 5 , 8 , 9 ]>>> l[3 ::2 ] = [11 , 22 ] >>> l [0 , 1 , 20 , 11 , 5 , 22 , 9 ]>>> l[2 :5 ] = 100 Traceback (most recent call last): File "<stdin>" , line 1 , in <module> TypeError: can only assign an iterable>>> l[2 :5 ] = [100 ] >>> l [0 , 1 , 100 , 22 , 9 ]
对序列使用 + 和 * 加号 + 用来表示将两个序列拼接起来,并返回一个新的序列;
乘号 * 表示重复多份序列并拼接起来
1 2 3 4 5 >>> l = [1 , 2 , 3 ]>>> l * 5 [1 , 2 , 3 , 1 , 2 , 3 , 1 , 2 , 3 , 1 , 2 , 3 , 1 , 2 , 3 ]>>> 5 * 'abcd' 'abcdabcdabcdabcdabcd'
特别注意,在 [a] * n 这个表达式中,如果 a 是一个引用,那么复制出来的是 n 个引用,并且这 n 个引用实际上指向同一个对象;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 >>> board = [['_' ] * 3 for i in range (3 )] >>> board [['_' , '_' , '_' ], ['_' , '_' , '_' ], ['_' , '_' , '_' ]]>>> board[1 ][2 ] = 'X' >>> board [['_' , '_' , '_' ], ['_' , '_' , 'X' ], ['_' , '_' , '_' ]]>>> weird_board = [['_' ] * 3 ] * 3 >>> weird_board [['_' , '_' , '_' ], ['_' , '_' , '_' ], ['_' , '_' , '_' ]]>>> weird_board[1 ][2 ] = 'O' >>> weird_board [['_' , '_' , 'O' ], ['_' , '_' , 'O' ], ['_' , '_' , 'O' ]]
序列的增量赋值 自增 += 或者自乘 *= 实际上调用的是 _iadd _ 和 _imul _ 方法,如果一个类没有实际 iadd 方法,那么解释器就会调用 add 方法来计算,此时相当于 a = a + b,因此,如果 a + b 返回的是一个新的对象,那么 a 将指向该新的对象,而不是改变旧对象的值;
1 2 3 4 5 6 7 8 >>> l = [1 , 2 , 3 ]>>> id (l)4311953800 ➊>>> l *= 2 >>> l [1 , 2 , 3 , 1 , 2 , 3 ]>>> id (l)4311953800 ➋
元组是不可变的,当在元组里面放入一个可变序列时,会出现异常情况,即该可变序列可被改变,但是无法将改变后的新序列,赋值给元组的引用;
1 2 3 4 5 6 7 >>> t = (1 , 2 , [30 , 40 ])>>> t[2 ] += [50 , 60 ] Traceback (most recent call last): File "<stdin>" , line 1 , in <module> TypeError: 'tuple' object does not support item assignment >>> t (1 , 2 , [30 , 40 , 50 , 60 ])
list.sort 方法和内置函数 sorted list.sort 会就地修改列表,返回 None
sorted 则不会修改原列表,而是会返回一个新的列表;
用 bisect 来管理已排序的序列 bisect 用来从有序列表中查找某个值的插入位置,满足插入后原序列的顺序不变;
insort 用来将元素插入到有序列表中,插入后顺序保持不变;
当列表不是首选时 数组 array.array:数组里面存储的不是对象,而是字面值(例如数字,在内存中直接用字节表示即可);因此它的读定性能要高很多;但因此它能够存储的类型也比较有限,只有少数几种;
创建数组时,需要通过参数指定类型,以便解释器能够决定如何分配内存空间;
内存视图
memory view
在不复制内容的情况下,操作数组的切片,例如 Numpy;
1 2 3 4 5 6 7 8 9 10 11 12 >>> numbers = array.array('h' , [-2 , -1 , 0 , 1 , 2 ])>>> memv = memoryview (numbers) >>> len (memv)5 >>> memv[0 ] -2 >>> memv_oct = memv.cast('B' ) >>> memv_oct.tolist() [254 , 255 , 255 , 255 , 0 , 0 , 1 , 0 , 2 , 0 ]>>> memv_oct[5 ] = 4 >>> numbers array('h' , [-2 , -1 , 1024 , 1 , 2 ])
Numpy 和 SciPy 操作高阶数组和矩阵的利器;
1 2 3 4 5 6 7 8 9 10 11 12 13 >>> import numpy ➊>>> a = numpy.arange(12 ) ➋>>> a array([ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 ])>>> type (a) <class 'numpy.ndarray' >>>> a.shape ➌ (12 ,)>>> a.shape = 3 , 4 ➍>>> a array([[ 0 , 1 , 2 , 3 ], [ 4 , 5 , 6 , 7 ], [ 8 , 9 , 10 , 11 ]])
双向队列和其他形式的队列 虽然可以用列表在模拟队列,但是性能并不好,尤其是在头部插入新元素时;双向队列更方便,而且可以指定长度,在超出长度时,会自动删除较早的内容;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 >>> from collections import deque>>> dq = deque(range (10 ), maxlen=10 ) ➊>>> dq deque([0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ], maxlen=10 )>>> dq.rotate(3 ) >>> dq deque([7 , 8 , 9 , 0 , 1 , 2 , 3 , 4 , 5 , 6 ], maxlen=10 )>>> dq.rotate(-4 ) >>> dq deque([1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 0 ], maxlen=10 )>>> dq.appendleft(-1 ) >>> dq deque([-1 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ], maxlen=10 )>>> dq.extend([11 , 22 , 33 ]) >>> dq deque([3 , 4 , 5 , 6 , 7 , 8 , 9 , 11 , 22 , 33 ], maxlen=10 )>>> dq.extendleft([10 , 20 , 30 , 40 ]) >>> dq deque([40 , 30 , 20 , 10 , 3 , 4 , 5 , 6 , 7 , 8 ], maxlen=10 )
注:append 和 popleft 是原子操作,因此是线程安全的;
除了双向队列,还有以下几种队列,分别是:
queue:如果队列满了,不会自动删除旧元素,而是会被锁住;因此可用来控制活跃线程的数量;
multiprocessing:用于进程间的通信
asyncio:用于异步编程
heapq:堆队列
第3章 字典和集合 字典构造方法
如果一个对象是可散列的,那么它的散列值需要不可变,而且这个对象需要实现 hash 和 eq 方法,以便可以计算散列值并和其他对象做比较;
字典有很多种构造方法
1 2 3 4 5 6 7 >>> a = dict (one=1 , two=2 , three=3 )>>> b = {'one' : 1 , 'two' : 2 , 'three' : 3 }>>> c = dict (zip (['one' , 'two' , 'three' ], [1 , 2 , 3 ]))>>> d = dict ([('two' , 2 ), ('one' , 1 ), ('three' , 3 )])>>> e = dict ({'three' : 3 , 'one' : 1 , 'two' : 2 })>>> a == b == c == d == eTrue
字典推导 字典可以从任何以键值对作为元素的可迭代对象中构造出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 >>> DIAL_CODES = [ ➊... (86 , 'China' ),... (91 , 'India' ),... (1 , 'United States' ),... (62 , 'Indonesia' ),... (55 , 'Brazil' ),... (92 , 'Pakistan' ),... (880 , 'Bangladesh' ),... (234 , 'Nigeria' ),... (7 , 'Russia' ),... (81 , 'Japan' ),... ]>>> country_code = {country: code for code, country in DIAL_CODES} >>> country_code {'China' : 86 , 'India' : 91 , 'Bangladesh' : 880 , 'United States' : 1 ,'Pakistan' : 92 , 'Japan' : 81 , 'Russia' : 7 , 'Brazil' : 55 , 'Nigeria' :234 , 'Indonesia' : 62 }>>> {code: country.upper() for country, code in country_code.items() ... if code < 66 } {1 : 'UNITED STATES' , 55 : 'BRAZIL' , 62 : 'INDONESIA' , 7 : 'RUSSIA' }
常见的映射方法 有个 setdefault 方法不常用,但其实很不错。它的用法如下
1 2 3 4 >>> a = {"abc" : 123 }>>> b = a.setdefault("abc" , 456 ) >>> b123
映射的弹性键查询 通常情况下,当我们使用 dict[key] 的方式访问时,如果该 key 不存在,会出现报错;而 collencts.defaultdict 可以处理这种情况;它会将该键为一个预先设定好的默认值,并返回该值
1 2 3 4 5 6 7 8 9 10 11 12 import sysimport reimport collections WORD_RE = re.compile (r'\w+' ) index = collections.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) index[word].append(location)
defaultdict 仅在 dict[key] 下有效,在 dict.get(key) 是无效的,后者不会调用预设的工作方法;defaultdict 背后的工作原理是因为实现了 _missing _ 方法;当 _getitem _ 找不到键名时,默认会调用 missing 方法;因此,只要有实现该方法,即可以实现默认值的初始化和返回;
考虑到 missing 会被调用,那么就可以在这里设置手脚;例如将键名由数值转换字符串,以支持不管传入哪种类型,都可以找到对应的键;
字典的变种 collections.OrderDict 会记录每个键的添加顺序,然后可以删除最晚或者晚早添加的键;
collections.ChainMap ChainMap 会将多个 dict 组合成一个 chain,让它表现起来,像是一个 dict
1 2 3 4 5 6 7 8 9 10 11 12 13 >>> baseline = {'music' : 'bach' , 'art' : 'rembrandt' }>>> adjustments = {'art' : 'van gogh' , 'opera' : 'carmen' }>>> cm = ChainMap(baseline, adjustments)>>> cm ChainMap({'music' : 'bach' , 'art' : 'rembrandt' }, {'art' : 'van gogh' , 'opera' : 'carmen' })>>> list (cm) ['art' , 'opera' , 'music' ]>>> cm['music' ]'bach' >>> cm['art' ]'rembrandt' >>> cm.values() ValuesView(ChainMap({'music' : 'bach' , 'art' : 'rembrandt' }, {'art' : 'van gogh' , 'opera' : 'carmen' }))
collections.Counter 1 2 3 4 5 6 7 8 >>> ct = collections.Counter('abracadabra' )>>> ct Counter({'a' : 5 , 'b' : 2 , 'r' : 2 , 'c' : 1 , 'd' : 1 }) >>> ct.update('aaaaazzz' ) >>> ct Counter({'a' : 10 , 'z' : 3 , 'b' : 2 , 'r' : 2 , 'c' : 1 , 'd' : 1 })>>> ct.most_common(2 ) [('a' , 10 ), ('z' , 3 )]
1 2 3 4 5 >>> cnt = Counter()>>> for word in ['red' , 'blue' , 'red' , 'green' , 'blue' , 'blue' ]:>>> cnt[word] += 1 >>> cnt Counter({'blue' : 3 , 'red' : 2 , 'green' : 1 })
collections.UserDict 用于让用户继承来编写子类,与 dict 的不同之处在于它是纯 Python 实现;而 dict 为了性能,某些功能的实现并不完全按照规范;
子类化 UserDict 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import collectionsclass StrKeyDict (collections.UserDict): def __missing__ (self, key ): if isinstance (key, str ): raise KeyError(key) return self [str (key)] def __contains__ (self, key ): return str (key) in self .data def __setitem__ (self, key, item ): self .data[str (key)] = item
一些好用的方法
update
1 2 3 4 5 6 >>> td1 = {'name' : 'Zara' , 'age' : 7 }>>> td2 = {'sex' : 'female' }>>> td1.update(td2)>>> td1 {'name' : 'Zara' , 'age' : 7 , 'sex' : 'female' } >>>
不可变映射类型 Python 的标准库并不支持不可变映射类型,但是有个变通的办法来实现相同的效果,即通过 MappingProxyType,从名字可以看得出来它是一个代理,这个代理是只读的;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 >>> from types import MappingProxyType>>> d = {1 :'A' }>>> d_proxy = MappingProxyType(d) >>> d_proxy mappingproxy({1 : 'A' })>>> d_proxy[1 ] 'A' >>> d_proxy[2 ] = 'x' Traceback (most recent call last): File "<stdin>" , line 1 , in <module> TypeError: 'mappingproxy' object does not support item assignment>>> d[2 ] = 'B' >>> d_proxy mappingproxy({1 : 'A' , 2 : 'B' })>>> d_proxy[2 ]'B'
集合论 集合 set 是一些对象的集合,它可以用来去重;集合中的元素必须是可散列的;
1 2 3 4 5 >>> l = ['spam' , 'spam' , 'eggs' , 'spam' ]>>> set (l) {'eggs' , 'spam' }>>> list (set (l)) ['eggs' , 'spam' ]
集合有一些自己的运算符,以便计算合集、交集、差集等;
1 2 found = len (needles & haystack)
集合字面量 创建空集需要使用 set(), 而不是 { },不然就变成了字典了;
s1 = {1, 2, 3} 的性能比 s2 = set([1, 2, ,3]),因为后者涉及先构造列表的动作;
集合推导 集合推导和列表推导的唯一差别在于方括号 [ ] 还是花括号 { }
1 2 3 4 >>> from unicodedata import name ➊>>> {chr (i) for i in range (32 , 256 ) if 'SIGN' in name(chr (i),'' )} ➋ {'§' , '=' , '¢' , '#' , '¤' , '<' , '¥' , 'μ' , '×' , '$' , '¶' , '£' , '©' ,'°' , '+' , '÷' , '±' , '>' , '¬' , '®' , '%' }
集合的操作 集合有不少专属的操作,这些操作很多是通过对运算符的重载来实现的;
dict 和 set 背后 dict 和 set 背后的实现原理是散列,这样性能就不会因为元素数量的增长出现大多波动;散列本质上是以空间换时间;列表则是以时间换空间;
dict 的实现及其结果
注:所有由用户自定义的对象,都是可散列的;因为它的散列值是由 id() 来生成的,跟对象本身的值没有关系。因此所有这些自定义对象,即使值相同,由于 id 不同,它们也是不相等的;
相比列表,元组会比较节省空间;一方面是因为它无须重复存储键名,另一方面是它不需要用到散列;
应避免在迭代的过程中,对字典进行修改,它会给迭代带来扰乱,有可能导致出错,或者结果错乱;
字典 dict 是不可散列的,所以无法直接将 dict 添加到 set 中;
字典 dict 的键名顺序是有可能会变化的,例如当出现散列冲突时或者扩容时;
第4章 文本和字节序列 字符问题 在 Python3,字符统一使用 Unicode 进行表示(称为码位),这样能够涵盖所有的已知字符,而且这个字符的 Unicode 也是固定的;但是在存储的时候,可以有多种编码方法(将码位转成字节序列),例如 UTF8, UTF16 等;使用不同的编码方法存储,就需要使用相应的解码方法读取,这样出来的结果才是正确的;
字节概要 在 Python3,有 bytes 和 bytearray 两种字节序列类型,其内部的元素是 0~255 的整数;
bytes[0],返回一个元素;
bytes[:1],返回一个切片,即一段新的序列
虽然二进制序列在底层是整数序列,但是显示的字面量有多种可能,包括:
ASCII 字符
制表符、换行符、回车符、斜杠等特殊符号;
十六进制转义表示
基本的编解码器 了解编解码问题 处理文本文件 为了正确比较而规范化 Unicode 字符串 Unicode 文本排序 Unicode 数据库 支持字符串和字节序列的双模式 API 第5章 一等函数 把函数视为对象 first class 函数满足以下条件:
能够在运行时创建
能够赋值给变量或者数据结构中的元素;
能够做为参数传递给函数;
能够做为结果从函数调用中返回;
简而言之,函数就像一个对象一样(事实上在底层实现也是如此,函数即对象);
高阶函数 高阶函数:higher-order function,接受函数做为参数,或者返回结果为参数;
常用的 map 和 filter,可以用列表推导式和生成器表达式进行替代,看起来更容易理解,示例如下:
1 2 3 4 5 map (func, range (6 )) [func(i) for i in range (6 )]filter (lambda n : n % 2 , range (6 )) [i for i in range (6 ) if i % 2 ]
匿名函数 由于 python 的 lambda 函名函数只能写单行的表达式,因此表达能力非常有限,导致使用场景非常少;常用于高阶函数的函数参数;类似下面这个样子
1 filter (lambda n : n % 2 , range (6 ))
可调用对象 调用运算符,即一对括号,不仅可以运用在函数上,其实也可以运用在普通对象上;
可用 callable 函数来判断某个对象是否可以调用
用户定义的可调用类型 事实上所有对象都是可以调用的,只要对象有实现 call 方法即可;
函数内省
函数内省,function introspection,这个翻译名称有点奇怪;
由于函数是一个对象,因此其实这个对象内部存储着很多与函数有关的信息,示例如下:
从定位参数到仅限关键字参数 python 的函数参数处理机制非常灵活强大,既支持固定位置的参数形式,也支持按关键字进行匹配的参数形式。同时还支持使用 * 单星号或者 ** 双星号,将不固定数量的任意个参数,打包成一个可迭代对象,以便在函数体内部进行访问;其中单个星号打包成 tuple 元组的形式;两个星号打包成字典 dict 的形式;
获取关于参数的信息 函数内部的属性,可用来做一起有用的事情,示例如下:
此处使用了装饰器,装饰器会检查 hello 函数内部属性中存储的与参数有关的信息。检查后,它会发现 hello 函数需要一个 person 函数;因此,它可以用 query 对象中,获取相应的 person 值,然后作为参数,传递给 hello 函数;
_default_ 存储函数参数的默认值;
_code_ 是一个对象,它也存储着函数的相关信息,例如:
co_varnames 存储着参数名称 + 局部变量名称
co_argcount 存储着函数的参数个数;
直接访问 code 对象或者 default 不是很方便,不过有个 inspect 库提供了方便的查看方式;
函数注解 函数注解可用来给参数和返回值备注类型
1 2 def clip (text:str , max_len:'int > 0' =80 ) -> str : """省略"""
注解会存储在函数的 _annotations _ 属性中;
注解本身不会做任何事情,有注解跟没有注解是一样的;但是注解可以给第三方工具(例如框架、装饰器等)提供有用的信息,例如 IDE 或者 Lint 工具可以利用注解来检查;
支持函数式编程的包 operator 模块 operator 模块提供了一些算术运算符函数,它让代码更加简单易懂;
1 2 3 4 5 from functools import reducefrom operator import mul def fact (n ): return reduce(mul, range (1 , n+1 ))
itemgetter 可用来读取元组中的元素
1 2 3 4 5 6 7 8 9 10 11 >>> 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(1 )): ... print (city)
1 2 3 4 5 6 7 8 9 10 >>> cc_name = itemgetter(1 , 0 )>>> for city in metro_data:... print (cc_name(city)) ... ('JP' , 'Tokyo' ) ('IN' , 'Delhi NCR' ) ('MX' , 'Mexico City' ) ('US' , 'New York-Newark' ) ('BR' , 'Sao Paulo' )
attrgetter 与 itemgetter 的不同之处在于它使用名称来提取对象的属性;
methodcaller 接受一个参数,表示要调用的函数名称,然后它可以在之后传入的对象中调用相应的方法;
1 2 3 4 5 6 7 8 9 >>> from operator import methodcaller>>> s = 'The time has come' >>> upcase = methodcaller('upper' ) >>> upcase(s) 'THE TIME HAS COME' >>> hiphenate = methodcaller('replace' , ' ' , '-' ) >>> hiphenate(s) 'The-time-has-come'
partial 可用来将某个函数的参数设置为固定值
1 2 3 4 5 6 7 >>> from operator import mul>>> from functools import partial>>> triple = partial(mul, 3 ) >>> triple(7 ) 21 >>> list (map (triple, range (1 , 10 ))) [3 , 6 , 9 , 12 , 15 , 18 , 21 , 24 , 27 ]
第6章 使用一等函数实现设计模式 策略模式 在函数作为一等公民时,很多设计模式就有了更简单的实现方法了;例如策略模式中,每个策略对应一个类;实际上它们都可以简单替换成函数即可,完全没有必要单独为了调用它而去实例化一个对象;
命令模式 命令模式的本意是想在命令的调用者(操作对象)和接收者(实现对象)之间进行解耦,这样调用者无须了解各个接收者具体是什么接口,而让它们对接口进行统一命名;但其实有更简单的做法,即直接将各个实现绑定到调用者身上就可以了,有点像回调那样;
面向对象之所以要搞成那么复杂,完全是因为它们不能接受函数作为参数,而是只能接受对象做为参数,然后再去调用对象的方法,这样就不得不对所调用的方法有个规范命名,不然就不知道如何调用;但如果能够接受函数作为参数,那就完全不一样了,直接将形参当作函数调用即可,非常简单直观,容易理解;
第7章 函数装饰器和闭包 装饰器基础知识 装饰器是一个可调用的对象,类似函数,它的参数是另外一个函数,它的目的是对该函数进行打包封装,干些额外的工作;它的执行结果有可能会返回参数函数,也有可能是返回另外一个新的函数或可调用对象,并赋值给原本作为参数的函数名称,这样调用者并不知道这个函数可能已经被替换了;
1 2 3 4 5 6 7 8 9 @decorate def target (): print ("running target()" ) def target (): print ("running target()" ) target = decorate(target);
Python 何时执行装饰器 注意,在定义装饰器的代码文件被加载时,装饰器会被立即执行,此时被装饰的函数还没有被调用;
使用装饰器改进策略模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 promos = [] def promotion (promo_func ): promos.append(promo_func) return promo_func@promotion def fidelity (order ): """为积分为1000或以上的顾客提供5%折扣""" return order.total() * .05 if order.customer.fidelity >= 1000 else 0 @promotion @promotion def bulk_item (order ): """单个商品为20个或以上时提供10%折扣""" discount = 0 for item in order.cart: if item.quantity >= 20 : discount += item.total() * .1 return discount
变量作用域规则 python 在编译函数定义时,它会先检查函数中声明的局部变量;
如果变量存在,那么之后使用变量时,解释器只会在本地作用域中寻找;
如果不存在,那么就会到函数的定义环境中寻找全局变量;
global 关键字可用来告知某个变量为全局的,以引导解释器到正确的位置查找
1 2 3 4 5 6 >>> b = 6 >>> def f3 (a ):... global b... print (a)... print (b)... b = 9
闭包 如果函数引用了某个变量,该不在其定义内部定义,而是在函数外部定义的,那么解释器会在函数对象中,保留一个指向该外部变量的引用,以便在使用该变量时,能够取到相应的值;
1 2 3 4 5 6 7 8 def make_averager (): series = [] def averager (new_value ): series.append(new_value) total = sum (series) return total/len (series) return averager
nonlocal 声明 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def make_averager (): count = 0 total = 0 def averager (new_value ): count += 1 total += new_value return total / count return averagerdef make_averager (): count = 0 total = 0 def averager (new_value ): nonlocal count, total count += 1 total += new_value return total / count return averager
实现一个简单的装饰器 1 2 3 4 5 6 7 8 9 10 11 import timedef clock (func ): def clocked (*args ): t0 = time.perf_counter() 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)) return clocked
支持关键字参数的版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import timeimport functoolsdef clock (func ): @functools.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 clocked
标准库中的装饰器 使用 lru_cache 缓存 lru_cache 可以帮助缓存函数的计算结果,如果下次再传入相同的参数,则直接从缓存中返回计算结果,不再重复计算,这会极大的提高性能,尤其是存在大量重复计算的场景,例如计算斐波契那数列;
lru 的全称 least recently used
单分派泛函数
所谓的分泛函数是指这个函数的功能用于分别派分任务,它根据参数值,使用一串 if elif else 来分别调用相应的函数;在 OO 的语言中一般叫重载,但 Python 不支持重载;
singledispatch 装饰器,可以将多个函数组合成一个泛函数;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from functools import singledispatchfrom collections import abcimport numbersimport 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>' .format (n)@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>'
singledispatch 可以用来装饰自己编写的函数,也可以用来装饰他人编写的函数;
叠放装饰器 装饰器支持叠放
1 2 3 4 5 6 7 @d1 @d2 def f (): print ("f" ) f = d1(d2(f))
参数化装饰器 通过创建一个装饰器工厂函数,便可使装饰器支持传入参数;调用该装饰器工厂函数时,返回的是真正的装饰器;
第8章 对象引用、可变性和垃圾回收 变量不是盒子 变量本身是一个独立的东西,我们借助它,让它指向某个对象,以方便实现引用该对象;
标识、相等性和别名 在 Python 中,判断两个对象是否相同,有两种方法,一种是 == 两个等号,一种是使用关键字 is,它们的意思是不一样的;== 会调用对象的 __eq __ 方法进行判断,它比的是值相等即可,is 等是判断对象的 id,相当于内存的地址;
由于 is 比较的是地址,因为使用 is 进行判断它的性能很好;因为使用 == 进行判断的话,需要遍历对象的属性值;
object 类型的 eq 方法比较的是 id,但是其他大多数内置类型的 eq 方法比较的是值;
当元组用于保存对象时,它保存的是对象的引用。虽然元组本身不可变,但这个引用背后的对象自身是可以变的;
默认做浅复制 如果要做深复制,需要使用 deepcopy 方法;浅复制则使用 copy 方法;
函数的参数作为引用时 千万不要将函数参数的默认值设置为可变对象,而应该设置为 None;因为如果是可变对象,那么在函数载入时,会自动创建出来;这样导致多次不传参数的调用该函数时,多个函数都会指向该默认值,造成相互影响;
1 2 3 4 5 6 7 8 9 10 class HauntedBus : def __init__ (self, passengers=[] ): self .passengers = passengers def pick (self, name ): self .passengers.append(name) def drop (self, name ): self .passengers.remove(name)
如果函数的参数是一个可变对象,那么让函数对该对象进行修改,会直接作用到外部的实参对象上。有时候,这是想要的结果,有时候则不是非预期的结果。如果是非预期的结果,那么函数内部应对该实参进行复制;
del 和垃圾回收 del 关键字并不是用来销毁对象的,而仅仅是切割变量和对象之间的引用关系;当对象的引用数量为零时,销毁的工作会垃圾回收器处理;
弱引用 弱引用不会增加对象的引用计数,这样不会对对象的垃圾回收带来干扰;一般用于有生命周期限制的缓存管理中;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 >>> import weakref>>> a_set = {0 , 1 }>>> wref = weakref.ref(a_set) ➊>>> wref <weakref at 0x100637598 ; to 'set' at 0x100636748 >>>> wref() ➋ {0 , 1 }>>> a_set = {2 , 3 , 4 } ➌>>> wref() ➍ {0 , 1 }>>> wref() is None ➎False >>> wref() is None ➏True
WeakValueDictionary 是一种可变映射(字典也是一种可变映射),映射指向的值是对象的弱引用;当对象被回收时,对应的键会自动从 WeakValueDictionary 中被删除;因此,它很适合用来做缓存;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Cheese : def __init__ (self, kind ): self .kind = kind def __repr__ (self ): return 'Cheese(%r)' % self .kind >>> import weakref>>> stock = weakref.WeakValueDictionary() >>> catalog = [Cheese('Red Leicester' ), Cheese('Tilsit' ),... Cheese('Brie' ), Cheese('Parmesan' )] ...>>> for cheese in catalog:... stock[cheese.kind] = cheese ...>>> sorted (stock.keys()) ['Brie' , 'Parmesan' , 'Red Leicester' , 'Tilsit' ] ➌>>> del catalog>>> sorted (stock.keys()) ['Parmesan' ] >>> del cheese >>> sorted (stock.keys()) []
不是每个 python 对象都可以被弱引用,例如常用的 list 和 dict 实例无法被弱引用,但是它们的子类可以;set 实例也可以
1 2 3 4 5 6 7 class MyList (list ):"""list的子类,实例可以作为弱引用的目标""" a_list = MyList(range (10 )) wref_to_a_list = weakref.ref(a_list)
Python 对不可变类型施加的把戏 使用一个元组构建另外一个元组,结果得到的是同一个元组
1 2 3 4 >>> t1 = (1 , 2 , 3 )>>> t2 = tuple (t1)>>> t2 is t1True
在 CPython 中,当对象的引用数量为零,会立即触发垃圾回收。但其他 Python 实现则不一定如此;这里涉及到性能的权衡;
第9章 符合 Python 风格的对象 对象表示形式 Python 默认使用两个函数来表示对象的字符串形式,它们分别是 repr() 和 str() 函数。它们实际上调用的是对象的 _repr _ 和 _str _
另外还有一个 bytes() 函数会调用 _bytes _ 方法来返回字节序列;
再谈向量类 以下是自定义向量类的待实现功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 >>> v1 = Vector2d(3 , 4 )>>> print (v1.x, v1.y) 3.0 4.0 >>> x, y = v1 >>> x, y (3.0 , 4.0 )>>> v1 Vector2d(3.0 , 4.0 )>>> v1_clone = eval (repr (v1)) >>> v1 == v1_clone True >>> print (v1) (3.0 , 4.0 )>>> octets = bytes (v1) >>> octetsb'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@' >>> abs (v1) 5.0 >>> bool (v1), bool (Vector2d(0 , 0 )) (True , False )
备选构造方法 classmethod 与 staticmethod classmethod 修饰的函数,调用时,不需要实例化对象;该函数的第一个参数是类本身,从而可以借助该函数,访问类的相关成员;
classmethod 的一个常见用途时定义额外的构造方法,一般该构造方法会对传入的数据进行清洗,之后再构造对象;
1 2 3 4 5 @classmethod def frombytes (cls, octets ): typecode = chr (octets[0 ]) memv = memoryview (octets[1 :]).cast(typecode) return cls(*memv)
格式化显示 1 2 >>> '1 BRL = {rate:0.2f} USD' .format (rate=brl) '1 BRL = 0.41 USD'
关于如何格式化,python 有一套自己的语法规则,可称之为微语言;这套微语言是可扩展的,可以自定义如何解释 forma_spec 参数
可散列的 Vector2d 通过实现 _hash _ 和 _eq _ 方法,可将一个不可散列的自定义类的对象,变成可散列的;
Python 的私有属性和受保护的属性 python 没有类似 Java 中的 private 关键字,而是通过给类成员的名称添加两个下划线前缀,将该成员标记为私有成员,类似这样:__x,但也有一些人喜欢使用一个下划线来表示;
对于私有属性,解释器在实例化对象时,会给这些属性加上类名作为前缀,这样一来,直接用双下划线访问私有属性时,会提示该属性并不存在,从而实现访问控制;但实际上是可以访问的,只是曲折一点,需要加上类名前缀来访问;
使用 slots 类属性节省空间 默认情况下,类的实例在 _dict_ 字段中使用字典来存储属性成员,如果成员比较多的,会占据较大的内存,此时可考虑使用 _slot_ 属性来存储以节省内存;它的原理是使用元组来存储,所以节省内存;
1 2 3 class Vector2d : __slots__ = ('__x' , '__y' ) typecode = 'd'
覆盖类属性 Python 的类属性可以为实例属性提供默认值,这个默认值在实例中可以被重新赋值;
如果要批量处理,则可以考虑定义一个子类,该子类的属性重写,之后使用子类来实例化对象;
第10章 序列的修改、散列和切片 鸭子类型:只要实现一些约定的接口,即可当作拥有目标类型的特征,并可以像目标类型一样被处理;例如一个类只需要实现 getitem 和 len 两个接口,那么它就可以被当作序列类型一样处理,至于它是谁的子类,并不重要;
zip 函数可用于并行迭代多个序列,它会将多个序列的对象打包成元组,然后可以拆包赋值给各个变量;
第11章 接口:从协议到抽象基类 接口与协议 一个类只需要实现了某些特定的接口,它就可以被当作特定的类型进行操作(即鸭子类型);
当一个类实现了 getitem 接口时,即使它没有实现 contains 和 iter 接口,它也是可迭代,并且支持 in 运算符的;因为解释器会调用 getitem 接口来实现以上两项功能;
Python 类中的方法,第一个参数叫 self 纯粹是一种惯例,其实叫个其他名字也无妨;
猴子补丁 猴子补丁:如果一个类在定义时,没有定义某个方法;之后在运行时,可以在外部单独定义一个函数,然后把这个函数绑定到类的某个属性上,这样就让类动态获得了某个方法;
抽象基类 抽象基类一般用于编写框架的场景,如果是业务场景,几乎不太可能需要自己编写抽象基类,而是使用现成的就可以了;
当继承抽象基类,就需要手工实现抽象基类中规定的所有方法,不管该方法是否用得到;
标准库中的 ABC
ABC:抽象基类,abstract base class
不可变集合:Sequence, Mapping, Set
可变集合:MutableSequence, MutableMapping, MutableSet
数字塔 numbers 包定义了数字抽象基类的线性层次结构:Number < Complex < Real < Rational < Integral;
第12章 继承的优缺点 子类化内置类型很麻烦 内置类型的方法不会调用子类覆盖后的方法,它只会调用内置类型原本的方法;因此,不会子类化内置类型,Python 有专门给用户子类化的类型,以 User 开头,例如 UserDict、UserList、UserString 等;
猜测原因在于内置类型的很多方法,出于性能考虑,是用 C 语言专门优化过的,因此不严格遵行继承的定义;
多重继承和方法解析顺序 多重继承会面临菱形问题,即子类继承多个父类中,存在同名的方法,导致子类无法确定应该执行哪个父类的同名方法;
多重继承的真实应用 处理多重继承 Django 示例 第13章 正确重载运算符 第14章 可迭代对象、迭代器和生成器
迭代器模式:惰性加载数据,处理时加载,这样可以用较小的内存,处理很大的数据集;
Python 中使用生成器来实现迭代器模式;生成器也是为了迭代数据,因此可将它当作迭代器来使用,唯一的区别在于它的惰性;
在 Python3 中,生成器是很普遍的,只是使用的时候没有觉察,例如 range(10) 返回的是一个类似生成器的对象;如果想要获得完整的列表,需要写成 list(range(10));
可迭代对象和迭代器对比 区别:从可迭代对象中,获取迭代器;迭代器如果迭代完毕,则不再可用,需要重新构建;
所谓的迭代器,可以理解为一个对象,每次调用它的 next 方法,可返回一个元素;如果空了,会报错;
通常迭代器还有一个 iter 方法,调用这个方法,可返回迭代器本身;理论上不实现它,也不会影响迭代功能。但如果实现了它,issubclass 方法可将其判断为 Iterator 的子类;
可迭代对象也有一个 iter 方法,调用它,会返回一个新的迭代器;
生成器函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import reimport reprlib RE_WORD = re.compile ('\w+' )class Sentence : def __init__ (self, text ): self .text = text self .words = RE_WORD.findall(text) def __repr__ (self ): return 'Sentence(%s)' % reprlib.repr (self .text) def __iter__ (self ): for word in self .words: yield word
生成器函数的工作原理 yield 关键字有点像 await,每次执行到 yield 所在的语句时,会暂停等待;
for 循环语句会自动捕获并处理迭代器抛出的异常;
惰性实现 re 模块除了 findall 函数,还有一个生成器版本的 finditer 函数;它每次只返回一个匹配项;当数据量很大时,可以节省很多内存;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import reimport reprlib RE_WORD = re.compile ('\w+' )class Sentence : def __init__ (self, text ): self .text = text def __repr__ (self ): return 'Sentence(%s)' % reprlib.repr (self .text) def __iter__ (self ): for match in RE_WORD.finditer(self .text): yield match .group()
生成器表达式 生成器表达式有点像是列表推导的惰性版本;
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 >>> def gen_AB (): ... print ('start' )... yield 'A' ... print ('continue' )... yield 'B' ... print ('end.' ) ...>>> res1 = [x*3 for x in gen_AB()] startcontinue end.>>> for i in res1: ... print ('-->' , i) ... --> AAA --> BBB>>> res2 = (x*3 for x in gen_AB()) >>> res2 <generator object <genexpr> at 0x10063c240 >>>> for i in res2: ... print ('-->' , i) ... start --> AAAcontinue --> BBB end.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import reimport reprlib RE_WORD = re.compile ('\w+' )class Sentence : def __init__ (self, text ): self .text = text def __repr__ (self ): return 'Sentence(%s)' % reprlib.repr (self .text) def __iter__ (self ): return (match .group() for match in RE_WORD.finditer(self .text))
何时用生成器表达式 生成器表达式是构建生成器的简捷方式,无需通过 def 定义函数来实现;但限于一些简单场景,一行可以搞定的那种;如果业务逻辑比较复杂,一行代码搞不定的话,则仍然需要使用函数来定义;
标准库 itertools 模块中有很多现成的生成器;
itertools.count(start, step):创建一个数字生成器
itertools.takewhile 给生成器添加条件限制
1 2 3 >>> gen = itertools.takewhile(lambda n: n < 3 , itertools.count(1 , .5 ))>>> list (gen) [1 , 1.5 , 2.0 , 2.5 ]
标准库中的生成器 在创建任何生成器前,很有必要先查一下标准库中有哪些生成器可用,以避免重复造轮子;
yield from yield from 可用来作为可迭代对象的生成器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 >>> def chain (*iterables ):... for it in iterables:... for i in it:... yield i ...>>> s = 'ABC' >>> t = tuple (range (3 ))>>> list (chain(s, t)) ['A' , 'B' , 'C' , 0 , 1 , 2 ]>>> def chain (*iterables ):... for i in iterables:... yield from i ...>>> list (chain(s, t)) ['A' , 'B' , 'C' , 0 , 1 , 2 ]
可迭代的归约函数 归约函数:接受一个可迭代的对象,返回一个值,例如 reduce 函数;
深入分析 iter 函数 iter 函数用于生成迭代器,一般接收一个可迭代对象作为参数;但是它还有个用法是接收两个参数,第一个参数是个可迭代对象,第二个参数是个 predicate,当可迭代对象产生的值满足 predicate 时,就停止产出;
1 2 3 4 5 6 7 8 9 10 11 12 13 >>> def d6 ():... return randint(1 , 6 ) ...>>> d6_iter = iter (d6, 1 )>>> d6_iter <callable_iterator object at 0x00000000029BE6A0 >>>> for roll in d6_iter:... print (roll) ...4 3 6 3
生成器很适合用来处理大数据集,这样可以利用有限的内存,处理无限大的数据,例如大型数据库;
生成器当作协程 生成器对象有个 send 方法,该方法允许给生成器对象发送消息;
第15章 上下文管理器和 else 块 else else 不仅可以跟 if 搭配使用,还可以跟 for, while, try 搭配使用;在这些场景中,else 实际上是 then 的意思,表示某个动作如果顺利完成了,那么就执行 else 里面的语句;如果没有顺利完成,就不执行;
1 2 3 4 5 for item in my_list: if item.flavor == 'banana' : break else : raise ValueError('No banana flavor found!' )
1 2 3 4 5 6 try : dangerous_call()except OSError: log('OSError...' )else : after_call()
with with 的目标是安全的实现 try…finally;with 之后的表达式(例如 open 函数)会创建一个上下文管理器对象。该对象有两个方法,分别是 enter 和 exit;with 语句开始执行时,会调用 enter 方法;执行结束后,会调用 exit 方法,类似 finally 的作用;
1 2 3 4 5 6 >>> with open ('mirror.py' ) as fp: ... src = fp.read(60 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class LookingGlass : def __enter__ (self ): import sys self .original_write = sys.stdout.write sys.stdout.write = self .reverse_write return 'JABBERWOCKY' def reverse_write (self, text ): self .original_write(text[::-1 ]) def __exit__ (self, exc_type, exc_value, traceback ): import sys sys.stdout.write = self .original_write if exc_type is ZeroDivisionError: print ('Please DO NOT divide by zero!' ) return True
contextlib 模块中有一些现成的工作,可用来创建自定义的 context 类(上下文管理器);
@contextmanager contextmanager 装饰器可以简化上下文管理器的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import contextlib@contextlib.contextmanager def looking_glass (): import sys original_write = sys.stdout.write def reverse_write (text ): original_write(text[::-1 ]) sys.stdout.write = reverse_write msg = '' try : yield 'JABBERWOCKY' except ZeroDivisionError: msg = "Please DO NOT divide by zero" finally : sys.stdout.write = original_write if msg: print (msg)
个人感觉所谓的上下文管理器,本质上也像是一个实现了约定协议的鸭子类型,只要按照协议实现 enter 和 exit 方法即可;
第16章 协程 yield 单词本身有两个意思,一个是生成,一个是退让;这两个意思刚好是协程的描述;
当 yield 放在表达式的左边时,它做为生成器使用;
当 yield 放在表达式的右边时,它做为协程使用,等待传入值;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 >>> def simple_coroutine (): ... print ('-> coroutine started' )... x = yield ... print ('-> coroutine received:' , x) ...>>> my_coro = simple_coroutine()>>> my_coro <generator object simple_coroutine at 0x100c2be10 >>>> next (my_coro) -> coroutine started>>> my_coro.send(42 ) -> coroutine received: 42 Traceback (most recent call last): ... StopIteration
协和在 yield 关键字所在的位置暂停执行
使用协程重新设计平均值计算器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def averager (): total = 0.0 count = 0 average = None while True : term = yield average total += term count += 1 average = total/count >>> coro_avg = averager() >>> next (coro_avg) >>> coro_avg.send(10 ) 10.0 >>> coro_avg.send(30 )20.0 >>> coro_avg.send(5 )15.0
使用协程时,经常容易忘记要先激活它。为了避免这种错误,可考虑定义一个帮忙激活的装饰器
1 2 3 4 5 6 7 8 9 10 from functools import wrapsdef coroutine (func ): """定义一个装饰器:帮忙预激`func`""" @wraps(func ) def primer (*args,**kwargs ): ➊ gen = func(*args,**kwargs) ➋ next (gen) return gen ➍ return primer
在调用生成器的 send 函数时,如果给它传递的参数类型有误,会导致它抛出异常,从而终止协程;
生成器有一个 throw 方法可用于触发异常;如果生成器内部有处理异常的代码,则执行;如果没有,则冒泡;
生成器还有一个 close 方法可用于抛出 exit 异常
yield from yield from 带来了双向通讯机制,貌似可用来实现异步编程;先定义生成器,然后激活它;之后向它发送数据;当数据处理完成后,会触发异常,获得处理结果;
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 31 32 33 34 35 36 37 from collections import namedtuple Result = namedtuple('Result' , 'count average' )def averager (): total = 0.0 count = 0 average = None while True : term = yield if term is None : break total += term count += 1 average = total/count return Result(count, average) def grouper (results, key ): while True : results[key] = yield from averager() def main (data ): results = {} for key, values in data.items(): group = grouper(results, key) next (group) for value in values: group.send(value) group.send(None ) print (results)
yield from 跟 await 有一个很大的不同,即 yield from 在将工作做到一半后,将控制权还给调用者,由调用者做剩下的工作;
据说 python 后来引入了 await
第19章 并发模型 协程: coroutine,一个可以暂停并重新运行自己的函数;
协程的特点在于可以通过关键字,标识出异步的位置,然后交出控制权,让主程序的其他部分获得控制权;自己则进入队列,等待异步的代码执行完毕;改变自己状态,等待被唤醒,继续运行自己余下部分的代码;
没想到 Python 有一个全局锁(GIL),每次只允许一个线程占有,那这就意味着 python 无法同时利用多核的优势好像,除非起多个进程,就像 js 的 cluster 一样;
Python 解释器每隔一定的时间(貌似是 5ms),会释放 GIL,以便其他线程能够获取锁;另外,任意一个函数在调用 syscall 时,它都会释放 GIL;
书里面的 spinner 案例,看起来很奇怪,因为协程的结束,竟然是由调用者的代码发起的,跟 js 好像不太一样;但是 await 貌似是一样的;
后来发现,协程也可以自己结束,不需要外部让它结束;书上的案例只是示范说可以主动干预。但其实正常使用场景是不干预,让它自行运行结束,返回结果;
问:好奇有无可能用装饰器,将非协程的代码,包装成协程代码?答:想了一下,虽然可能,但是由于非协程代码里面,在遇到 I/O 任务时,没有使用 await 交出控制权,该协程貌似可能会卡在那里等待;
协程能够起作用,貌似重点在于每次遇到 I/O 任务时,要主动交出控制权;在 js 里,很多库都是默认异步编写的,因此不容易忘记这个事情。但是在 python 里面,很多库并非天生异步,例如常用的 requests,此时很有必要提高警惕;
当协程获得控制权,处于运行中的状态时,它是无法被取消的。因为只有一个线程,当它在运行时,意味着想要取消它的代码并没有在运行;仅当协程位于队列中,处于等待状态时,才有可能被取消;此时取消它的代码有可能获得了控制权;
asyncio.run() 函数,做为所有协程运行代码的入口;
asyncio.create_task(),在当前协程中,创建一个新的协程;可基于返回的 task 对象,对新建的协程进行控制;
await coro(),调用 coro,并同步等待它返回结果;
调用 coro() 时,并不意味着 coro 的代码会马上执行,而只是表示将它加入了队列,实际的执行时间取决于事件循环的高度器;
跟 js 一样,await 关键字必须用在 async 定义的函数中;当函数用 async 定义时,它是一个协程;每次对该函数的调用,都是都它加入事件循环的队列中;而 await 表示交给当前协程对 CPU 的使用,即停止运行,让调度器去运行其他协程;等 await 的事件结束时,调度器会重新安排它运行;
GIL 的真实影响 各种处理网络请示的库,如 requests,在发起请示后,会释放当前线程的 GIL,以便其他线程可以抢占;但如果只有一个线程,那么抢占并没有意义。仅在多线程时,抢占才有意义;而且即使抢占成功,后续 requests 仍然会再次被分配 GIL,但此时它有可能仍然还没有取得响应,因此会浪费掉一些性能;但总的来说,多线程有助于提高 I/O 的并发处理能力;但不适用于 CPU 密集型的任务,性能反而变差,因为 CPU 不断在多个线程之间切换,但每次只运行一个线程,最终的性能还不如顺序执行来得快;
另外还有一些库的设计是异步的,但如果当前的代码不是 async 的话,貌似也无法使用 await 来交出控制权?
当处理计算密集型任务,因为 GIL 的存在,多线程是没有意义的,因为每次只有一个线程在工作;反而不如使用单线程来得简单和高效;如果有多核,则可以考虑使用多进程模式来提高效率;
第20章 Concurrent Executors concurrent.futures 库里面,有两个类,分别是 ThreadPoolExecutor 和 ProcessPoolExecutor,它们可以很方便的使用线程或者进程来实现并发;
对使用者来说,背后的线程或进程是透明的,它会自动开启多线程或进程,同时创建任务队列,收集各线程的处理结果;
Python 里面的 futures 有点像 js 里面的 promise;但 futures 一般不直接创建,而是交由框架来创建;开发者可以在更高的抽象维度来使用它,这样可以避免错误使用;
future.done() 方法可用来查询是否计算完成了,但更常见的做法是不查询,而是等待通知,即完成后,调用回调函数即可;
future 有个 add_done_callback 方法,它接受一个回调函数做为参数;注意:该回调函数,将在运行该 future 的线程或进程中直接运行;
future.result() 方法可用于获取计算结果;但 concurrency 和 asyncio 两个库对方法的实现有所不同;concurrency 调用 result 方法时,会造成堵塞,等待结果的返回;同时支持 timeout 参数,超时未返回时,会抛出异常;asyncio 则不支持 timeout 方法,但支持 await 关键字,这样不会造成堵塞;
concurrency 还有一个 as_completed 方法,专门用来读取 result,以避免堵塞,
executor.map() 主要用于一个函数,并发处理多个不同的参数
executor.submit() 则更灵活一些,多个不同的函数,并处理各自不同的参数;最后通过 as_completed 方法收集计算结果;
第21章 异步编程
虽然可以通过 async def 来定义异步函数,但是如果函数体中包含一些非异步的操作,比如将文件写入本地,貌似该同步操作有可能会造成堵塞,占用整个线程,直到写入成功?经查证,发现确实如此,在异步函数中,只要有任意一个函数是非异步阻塞的,将导致整个调用链都是阻塞的;
其他 海象符 :=,为了省写一行代码,先检查,确认有值后,再赋值;没值的话,就不赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 name = abc.get("name" )if name: else : if name := abc.get("name" ): else :