《流畅的Python》Data Structures--第2章序列array
第二部分 Data Structure
- Chapter2 An Array of Sequences
- Chapter3 Dictionaries and Sets
- Chapter4 Text versus Bytes
An Array of Sequences
本章讨所有的序列包括list,也讨论Python3特有的str和bytes。
也涉及,list, tuples, arrays, queues。
概览内建的序列
分类
Container swquences: 容器类型数据
- list, tuple
- collections.deque: 双向queue。
Flat sequences: 只存放单一类型数据
- str,
- bytes, bytearray, memoryview : 二进制序列类型
- array.array: array模块中的array类。一种数值数组。即只储存字符,整数,浮点数。
分类2:
Mutable sequences:
- list, bytearray, array.array
- collections.deque
- memoryview
Immutable sequences:tuple, str, bytes

⚠️,内置的序列类型,并非直接从Sequence和MutableSequence这两个抽象基类(Abstract Base Class ,ABC)继承的。
了解这些基类,有助于我们总结出那些完整的序列类型包括哪些功能。
List Comprehensions and Generator Expressions
可以简写表示:listcomps, genexps。
例子:使用list推导式。
# >>> symbols = '$¢£¥€¤' >>> codes = [ord(symbol) for symbol in symbols] >>> codes [36, 162, 163, 165, 8364, 164]
ord(c)是把字符转化为Uicode对应的数值.
列表推导式的好处:
- 比直接用for语句,更方便。也同样好理解。
- 类似函数, 会产生局部作用域,不会再有变量泄露的问题。
map()和filter组合
symbols = '$¢£¥€¤' beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols))) print(beyond_ascii)
map(func, iterable) -> iterator
速度上list comprehensions更快。
Generator Expressions
Listcomps只能产生一个list,而genexps可以产生其他类型的序列。
它遵守迭代器协议,逐个产生元素。
例子,利用genexps产生tuple和array.array
symbols = '$¢£¥€¤' a = tuple(ord(symbol) for symbol in symbols) print(a) import array arr = array.array("I", (ord(symbol) for symbol in symbols)) print(arr) print(arr[0])
colors = ['black', 'white'] sizes = ['S', 'M', 'L'] a = ('%s %s' % (c, s) for c in colors for s in sizes) print(a) #产生一个生成器表达式<generator object <genexpr> at 0x10351b3c0>
⚠️函数生成器,要加yield关键字。
Tuples are not just Immutable lists
tuple除了是不可变数组/列表。
另有一个功能体现: 储存从数据库提取的一条记录:record, 但这个record没有field name,只有value。
例如:
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')] for passport in sorted(traveler_ids): print('%s/%s' % passport) for country, _ in traveler_ids: print(country)
#输出 BRA/CE342567 ESP/XDA205856 USA/31195855 USA BRA ESP
⚠️:_是一个占位符。
tuple unpacking
一个习惯用法产生的概念:
>>> lax_coordinates = (33.9425, -118.408056) >>> latitude, longitude = lax_coordinates # tuple unpacking >>> latitude 33.9425 >>> longitude -118.408056
下面的赋值代码省略了中间变量 :
>>>b,a=a,b
#等同于
x = (a, b)
b, a = x
这个概念也可以用到其他的类型上,如list。range(), dict。
>>> a, b, *rest = range(3) >>> a, b, rest (0, 1, [2])
⚠️使用*号抓起过多的item
有拆包,就有打包:
>>> o = 1
>>> n = 2
>>> o, n
(1, 2)
Nested Tuple Unpacking 嵌套tuple的解包
只要表达式左边的结构,符合嵌套元祖的结构,嵌套tuple也可以解包
>>> x = ('Tokyo','JP',36.933,(35.689722,139.691667)) >>> name, cc, pop, (latitude, longitude) = x >>> name 'Tokyo' >>> longitude 139.691667
Named Tuples --一个tuple的子类
具有tuple的方法,同时也有自己的方法和属性。
因为tuple具有拆包封包的特性,用起来很方便。
⚠️list拆包后,再封包,得到的是tuple类型。
>>> a = list(range(2)) >>> a [0, 1] >>> x,y = a >>> x 0 >>> y 1 >>> x, y (0, 1) >>> z = x ,y >>> z (0, 1)
但是,如果把tuple类型用在一条数据库记录上:因为缺少fields字段。就很不方便,因此出现了Tuples类的子类named tuples。
使用工厂函数:collections.namedtuple(typename, field_names, *)
- field_names可以是['x', 'y']也可以是"x, y, z"或"x y z"
下例子:创建一个子类City, 它的父类是tuple。即Ciyt.__bases__返回(<class 'tuple'>,)
from collections import namedtuple Card = namedtuple("Card", ['rank', 'suit']) City = namedtuple('City', 'name country population coordinates') tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667)) print(tokyo) #City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))
⚠️内存一样
namedtuple和tuple的实例,占用的内存是一样的。因为namedtuple实例把字段名储存在了对应的类中。
因为结构固定,所以可以像dict一样显示key=value;
Tu = ('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
import sys print(sys.getsizeof(tokyo)) print(sys.getsizeof(Tu)) #88 #88
类方法
_fields: 返回类的所有的field的名字。
>>> City._fields ('name', 'country', 'population', 'coordinates')
_asdict:返回一个对应field字段的dict:
print(tokyo._asdict()) #返回一个dict {'name': 'Tokyo', 'country': 'JP', 'population': 36.933, 'coordinates': (35.689722, 139.691667)}
_make(): 接受一个可迭代对象来生成这个类的一个实例。等同于City(*tokyo)。
小结:
因为namedtuple的功能扩展,这个类就支持从数据库读取一条记录。但是因为是tuple的子类,不能对记录进行修改。
Slicing --切片的高级用法
list, tuple, str这些sequences类都支持slicing。
Why Slices and Range Exclude the last Item?
就是习惯。真要找原因:
-
my_list[:x] and my_list[x:]合起来就是my_list
- a[:3], 直接清楚的表示这个切片包括3个item。
- a[2:6], 6-2等于4。表示这个切片包括四个item。
Slice Objects
切片对象的用途:
一个有固定格式纯文本文件,需要对这个文件的每一行都进行相同的切片操作,那么使用Slice对象保存这种切片的起始位置和step。
之后无需反复重写切片了。
例子:
#invoice.txt 0.....6.................................40........52...55........ 1909 Pimoroni PiBrella $17.50 3 $17.50 1489 6mm Tactile Switch x20 $4.95 2 $9.90 1510 Panavise Jr. - PV-201 $28.00 1 $28.00
#首先,读取文本文件的内容 with open('invoice.txt', 'r') as invoice: line_items = invoice.read() #然后,按照文本内的格式,创建3个切片对象, sku = slice(0, 6) description = slice(6, 40) unit_price = slice(40, 52) #最后,逐行打印切片的结果 for item in line_items.split('\n')[1:]: print(item[unit_price], item[description])
直接使用切片对象的好处,方便代码的管理和维护。用自然语言描述要切片的字符串。
class slice(start=None, stop[, step=None])
返回一个slice对象。start,step默认为None。
slice对象有三个只读的数据属性(就是instance variable)start, stop, step
>>> a slice(0, 6, None) >>> type(a).__dict__.keys() dict_keys(['__repr__', '__hash__', '__getattribute__', '__lt__', '__le__',
'__eq__', '__ne__', '__gt__', '__ge__', '__new__', 'indices', '__reduce__', 'start', 'stop', 'step', '__doc__']) >>> a.start 0 >>> a.stop 6
给切片赋值
-
可以给切片赋值,值必须是可迭代对象。替换原list中的元素。
-
但如果切片start等于stop,则相当于插入了。
⚠️可参考源码(c写的)或这篇文章https://www.the5fire.com/python-slice-assignment-analyse-souce-code.html
Pythong, Ruby都支持对序列类型进行+和*操作
需要注意*操作对嵌套list:
>>> d = [["1"]*3]*3 >>> d [['1', '1', '1'], ['1', '1', '1'], ['1', '1', '1']] >>> d[1][1] = 's' >>> d [['1', 's', '1'], ['1', 's', '1'], ['1', 's', '1']]
相当于:
>>> a = ['1']*3 >>> a ['1', '1', '1'] >>> b = [a]*3 >>> b [['1', '1', '1'], ['1', '1', '1'], ['1', '1', '1']] >>> b[1][1] = "x" >>> b [['1', 'x', '1'], ['1', 'x', '1'], ['1', 'x', '1']]
⬆️例子,列表b,使用了3次a。b相当于[a, a, a]。
Augmented Assignment with Sequences 序列的增量赋值操作符 *=, +=
+=的背后是__iadd__方法,即(in-place addition,翻译过来就是,在当场进行加法运算,简单称为就地运算。)
这是因为a.+= b中的a 的内存地址不发生变化,所以称为就地运算。
⚠️但是,是否执行就地运算,要看type(a)是否包括__iadd__这个方法了。如果没有,则只相当于a = a + b, 新的a是一个新的对象。
- 可变序列支持__iadd__
- 不可变序列当然不支持了。比如tuple就不支持。
- ⚠️str作为不可变序列,支持__iadd__,这是因为在循环内对str做+=太普遍了。因此对它做了优化,str实例初始化内存时预留了足够的空间,因此不会复制原有的字符串到新内存位置,
>>> l = list(range(3)) >>> l [0, 1, 2] >>> id(l) 4421232256 >>> l *= 2 >>> id(l) 4421232256
同样 *=及其他增量赋值操作符 都是这样。
关于+=的 Puzzler
>>> 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])
这个例子,完成了操作之后报告了❌。原因分析:
需要前置知识点:字节码,dis模块。 code object等知识。
模块dis:https://docs.python.org/zh-cn/3/library/dis.html 字节码反汇编器
什么是字节码?
字节码就是把可读的源码通过编译转化为bytecode,字节码还需要解释器才能转换为机器代码,所以字节码是一种中间代码。
操作系统都支持字节码转化,因此字节码可以用到不同系统中,通过转化为机器码直接在硬件上运行。
而且字节码也可以逐条执行。有了解释型语言的特点。
Python源代码会被编译成bytecode, 即Cpython解释器中表示Python程序的内部代码。它会缓存在.pyc文件(在文件夹__pycache__)内,这样第二次执行同一文件时速度更快。
具体见博文:https://www.cnblogs.com/chentianwei/p/12002967.html
再说上面的例子: dis.dis(x):反汇编x对象。
>>> dis.dis('s[a] += b') 1 0 LOAD_NAME 0 (s) #把s推入计算栈 2 LOAD_NAME 1 (a) #把a推入计算栈 4 DUP_TOP_TWO # 复制顶部的两个引用,无需从栈取出item 6 BINARY_SUBSCR #执行计算: TOS = TOS1[TOS], 即把s[a]的值推入计算栈(TOS代表栈的顶部), 现在栈里有3个item. 8 LOAD_NAME 2 (b) #把b推入栈 10 INPLACE_ADD #Tos = tos1+ tos, 即先从栈取出前2个item, 然后s[a] + b的结果推入栈。 12 ROT_THREE # 将第2个,第三个栈项向上提升一个位置,栈顶部的项移动到第3个位置。 14 STORE_SUBSCR # tos1[tos] = tos3 16 LOAD_CONST 0 (None) 18 RETURN_VALUE
t[2] += [50,60]
当进行到上面👆粉色行时,要改变元组t的第3个元素,所以会报告❌。
由上面的例子想到:
- 不要把可变对象放到tuple里
- 增量赋值不atomic operation, 它抛出了❌,但完成了操作。
- 查看Python的字节码并不难。
附加:
上一章提到的slicing,通过dis.dis()分析,发现a[slice(1:2)]等同于a[1:2]
list.sort方法和内建函数sorted()
list.sort()和sorted()的区别:
>>> b = [3,2,1] >>> b.sort <built-in method sort of list object at 0x10c99c180> >>> b.sort() >>> b [1, 2, 3]
>>> c = [3,2,1] >>> sorted(c) [1, 2, 3] >>> c = (3,2,1) >>> sorted(c) [1, 2, 3]
list.sort方法是对原list对象进行操作,不是复制,返回None,即操作原对象,不创造新list。
sorted()会创建一个新的list, 并返回这个list。它的参数可以是任何的iterable object。
sorted(iterable,*, key=None, reverse=False)
具体参数讲解见文档: key接受一个函数明,算法根据key来排序。
>>> fruits = ['grape', 'raspberry', 'apple', 'banana'] >>> sorted(fruits) ['apple', 'banana', 'grape', 'raspberry'] >>> sorted(fruits, reverse=True) ['raspberry', 'grape', 'banana', 'apple'] >>> sorted(fruits, key=len) ['grape', 'apple', 'banana', 'raspberry']
使用bisect模块来管理已经排序的序列
这个模块提供了2个函数bisect, insort。使用了binary search algorithm来进行搜索或插入元素。这个算法广泛应用于二叉搜索数,B树系列。
bisect.bisect_left(a, x)
a是一个有序序列,x是要插入a的值。为了保持插入x后,a仍然是一个有序序列,使用二分搜索算法,返回一个数值,这个数值是x应该插入的索引位置。
如果a是list,那么返回值可以作为list.insert()的第一个参数。例子:
>>> NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31] >>> import bisect #用bisect(a, x)得到要插入的位置,然后使用insert()方法插入 >>> NEEDLES.insert(bisect.bisect(NEEDLES, 7),7) >>> NEEDLES [0, 1, 2, 5, 7, 8, 10, 22, 23, 29, 30, 31]
除了bisect_left还有bisect及bisect_right函数,它们的区别就是如果要插入的x元素已经存在于a中,则把x插入到已经存在的x元素的左边或右边
上面的代码仍然可以进一步封装,因此封装成bisect.insort(a, x)
>>> NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31] >>> import bisect >>> bisect.insort(NEEDLES, 7) >>> NEEDLES [0, 1, 2, 5, 7, 8, 10, 22, 23, 29, 30, 31]
一个很好玩的例子,格式输出:
import bisect import sys HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30] NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31] ROW_FMT = '{0:2d} @ {1:2d} {2}{0:<2d}' #<符号代表靠右 def demo(bisect_fn): for needle in reversed(NEEDLES): positon = bisect_fn(HAYSTACK, needle) offset = positon * ' |' #偏移 print(ROW_FMT.format(needle, positon, offset)) if __name__ == '__main__': if sys.argv[-1] == 'left': bisect_fn = bisect.bisect_left else: bisect_fn = bisect.bisect print('DEMO', bisect_fn.__name__) print('haystack ->', " ".join('%2d' % n for n in HAYSTACK)) demo(bisect_fn)
可以试一试看看结果。
~/自我练习/python练习 ⮀ python3 linshi.py #如果带参数left, 则插入位置发生变化。 ~/自我练习/python练习 ⮀ python3 linshi.py left
利用这个模块可以带来很多便利,具体见文档的例子。
需要⚠️,二分搜索算法的时间复制度O(lgn), 插入list的时间复杂度是O(n)。
在大数据下运行,寻求更快的运算速度。
list类型用起来很灵活,并且容易使用。但在一些特殊需求下,有更好的选择。
比如存放1000万个浮点数字,在这么大的大数据下,代码运算的快慢就体现出来了。
如果使用Python提供的array模块中的array,就可以带来更快的速度。因为一个array中存的不是float对象,而是打包的bytes类型,相当于机器值。这点和C语言中的数组类似。
还有,频繁对序列的第一个和最后一个元素进行添加和移除操作,那么deque或双deque类型数据运算的速度更快。
Python中提供了大量标准库包,这些都是可以挖掘的知识点,能为不同需求提供支持。所以如果寻求高效,那么就充分掌握对标准库的序列类型。
备注:
本章节从array之后未做阅读和学习,仅仅大略的了解了一下。
浙公网安备 33010602011771号