流畅的python笔记(二)数据结构

目录

一、内置序列类型概览

二、列表推导式和生成器表达式

列表推导式

生成器表达式

 三、元组

元组和记录

元组拆包

嵌套元组拆包

具名元组

元组作为不可变列表

四、切片

切片是左闭右开区间

多维切片和省略

修改切片就地改变原序列

五、对序列使用 + 和 *

六、序列的增量赋值 *= 与 +=

七、list.sort方法与内置函数sorted

八、用bisect模块管理已排序的序列

用bisect搜索

用bisect.insort插入新元素

九、替换列表的数据结构

数组

内存视图

NumPy和SciPy

双向队列和其它形式的队列

collections.deque

queue

multiprocessing

asyncio

heapq


一、内置序列类型概览

python的核心特点之一是对序列数据类型的支持。

按照序列能否存放不同类型对象分类

容器序列

        list、tuple和collections.deque,这些是容器序列类型,同一个序列对象中可以存放不同类型的数据。

扁平序列

        str、bytes、bytearray、memoryview和array.array,这些是扁平序列类型,这种一个序列对象中只能容纳一种类型。

        容器序列存放的是其所包含的任意类型对象的引用,而扁平序列存放的就是值。可以类比为容器序列中存放的是指针(引用跟指针类似),通过指针来指向不同的元素。而扁平序列类型就像C语言中的数组,其本质是一段连续的内存空间。扁平序列其实更加紧凑,但是只能存放字符、字节和数值这种基础类型。

按照序列能否被修改分类

可变序列

不可变序列

二、列表推导式和生成器表达式

一句话,列表推导式只用于构建列表,生成器表达式则可以用来创建任何类型的序列。

列表推导式

列表推导式只是构建列表的一种快捷方式

直接看例子,

l = [x for x in "ABC"]
print(l)

可以看到列表推导式是用中括号围起来的,用for循环来生成列表,里边有两个x,第二个x就是for循环遍历的可迭代对象中的单个元素,第一个x不一定是直接x,也可以对x做各种操作,如下例子,上边列表中元素是字符,本例将字符变成unicode码。

l = [ord(x) for x in "ABC"]
print(l)

需要注意的是,再python3中,列表推导式是有自己的局部作用域的,即x是一个局部变量,改变x不会改变外边的变量。

        上边两个例子中可以认为列表推导式中都是只有一个变量x,其实可以有很多个变量,下边是一个有两个变量的例子。

colors = ['black', 'white']
sizes = ['S', 'M', 'L']

tshirts = [(color, size) for color in colors
                        for size in sizes]
print(tshirts)

 注意一下一个有意思的点:

 列表推导式一般用来生成列表,如果要生成其它类型的序列,就需要生成器表达式出场了。

生成器表达式

用法和列表推导式几乎一模一样,只不过把中括号换成了小括号。

symbols = "acbdf"
t = tuple(ord(symbol) for symbol in symbols)
print(t)

如果生成器表达式是函数调用中的唯一参数,那么生成器表达式的小括号可以去掉,直接放在函数调用的小括号里边即可。

        也可以用列表推导式来生成元组等类型,其过程是先生成一个列表,然后再将列表作为参数传递给元组类的某个构造函数。而用生成器表达式则是逐个地产出元素,而不是先建立一个完整的列表,显然生成器表达式能够更节省内存。

        还是上边那个两个变量的例子,如果要打印出所有的组合,用列表推导式需要在内存中建立一个二维序列,当这个序列很大时这是不可接受的。而用生成器表达式则是一次只生成一个元素。

colors = ['black', 'white']
sizes = ['S', 'M', 'L']

for tshirt in ('%s %s' % (c, s) for c in colors
                                for s in sizes):
    print(tshirt)

 

 三、元组

city , year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
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)

元组和记录

不应当只把元组当作不可变的列表,元组可以当作记录使用。这个意思是说元组中每个元素都可以看成是一个匿名的字段的值。比如上边例子中列表traveler_ids,其中每个元素都是一个元组,这个时候每个元组对象就是一条记录,记录的第一个字段是国家,第二个字段是护照号码,只不过没有明确的字段名。

元组拆包

如例子中,下边两种用法都是元组拆包,其本质就是用相同数量的变量来接收元组中的元素。

city , year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
'%s/%s' % passport

本质上,可迭代对象都是可以做拆包的,比如列表:

x = ["hello", "world"]
y, z = x
print(y, z)

但是用%字符串格式化进行拆包貌似是元组专用的,至少列表是不行的。即平行赋值的拆包方式适用于任何可迭代对象,而字符串格式化只能用于元组。

        一个经典用法是不借助中间变量交换两个变量的值,其本质也是元组拆包,只不过由于运算符右边只有一个元组参数,因此把小括号给省略了。

可以用 * 来将一个可迭代对象拆包作为函数的参数。divmod函数是计算商和余数,其有两个参数,分别是被除数和除数。这里用 *t 将元组t拆包用作函数的参数。

还可以将元组作为一个函数的返回值,这样就可以方便地用几个变量来接收返回值。比如os.path.split()函数就会返回以路径和最后一个文件名字符串组成的元组(path, last_part):

拆包的时候,可能有些元素我们不感兴趣,因此可以用 _ 来作为占位符。

        python中一个重要的用法是, 在函数参数中用 *args 来让args列表接收不确定数量的参数。这个概念也被扩展到了平行赋值中。

平行赋值时,* 前缀只能用在一个变量名前边,但是可以出现在赋值表达式的任意位置。

嵌套元组拆包

没啥意思,看看例子。

 

具名元组

即collections.namedtuple,collections是python的一个标准库模块,里边存放的都是一些工厂函数,类似C++ STL中的容器或模板类。

        先来个例子观察具名元组的使用:

创建一个具名元组需要两个参数,第一个是类名,第二个是一个类的各个字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是空格分隔开的字段名组成的字符串。可以通过字段名或者索引来获取一个字段的信息。

        有意思的一点是,用namedtuple构建的类的实例所消耗的内存跟元组是一样的。流畅的python里的解释是字段名都被存在对应的类里边,那么问题来了,对象是怎么通过字段名来得到某个属性的值的呢?

from collections import namedtuple
import sys

Card = namedtuple("Card", "x y z")
t1 = Card(1, 2, 3)
t2 = (1, 2, 3)
print(sys.getsizeof(t1), sys.getsizeof(t2))

 具名元组还有一些自己专有的属性,比如_fields类属性,_make(iterable)类方法,和实例方法_asdict()。_fields类属性是一个包含这个类所有字段名的元组。类方法_make可以接受一个可迭代对象来生成一个实例,如果不用_make的话,就只能传入很多个参数来构造,而不能直接传入一个可迭代对象。当然也可以用 * 前缀来拆包一个元组再传入构造函数。_asdict()是一个实例方法,用于将具名元组对象的信息以collections.OrderedDict的形式返回,即一个键值对列表的形式。

元组作为不可变列表

除了与增删元素有关的方法以外,元组几乎支持列表其它的所有方法。

 reversed(my_tuple)并不会改变my_tuple,而是将my_tuple逆序然后返回一个新的元组。

四、切片

在python中,像列表list,元组tuple和字符串str这类序列类型都支持切片操作。

切片是左闭右开区间

 切片的时候还可以选择步长,即不连续切片。

需要注意的是如果步长是负的,那么起始索引应该大于结束索引。如下图所示。

 在python中对序列型对象切片的时候是使用了一个切片类的,即slice(start, stop, step)。当我们对一个序列型对象seq进行切片seq[start: stop: step],实际上 start: stop: step 会返回一个切片对象 slice(start, stop, step),这个时候对于切片操作,python解释器实际上会调用特殊方法__getitem__,即seq.__getitem__(slice(start, stop, step))。如下图所示,如果我们经常用某种切片,可以直接定义一个切片对象,就不用每次都输入起始点结束点和步长了。

多维切片和省略

多维切片针对的是多维数据,比如二维切片得到的就是二维数据,自然也就需要两个切片对象才行。在 [ ] 运算符里用逗号隔开两个切片对象就可以得到二维切片了,还可以用逗号隔开两个索引来得到二维索引。比如二维的numpy.ndarray可以用a[i, j]来获取某个元素,a[m : n, k : l]来得到二维切片。[ ] 运算符是与 __getitem__和__setitem__这两个特殊方法相关的。实际上我们在使用二维索引的时候,这两个特殊方法会以元组的方式来接受二维索引,即a.__getitem__((i, j))。

        需要注意的是python的内置序列类型都是一维的,所以是不能用二维索引的,所以对于二维列表来说,索引方式可以是a[i][j],但不能是a[i, j]。

        python还有一个对象是省略,即三个英文句号(...)。它可以用于函数参数列表中,用于表示不确定参数数目,比如 f(a, ..., z),也可以用于切片中,用于切片中时与具体的对象维度有关系,比如a[i, ...],如果a是四维数组,则等价于a[i, :, :, :],即相当于其它三个维度上都是全选的。

修改切片就地改变原序列

对于可修改序列对象来说,其切片与原序列是共享内存的,即修改切片也会修改原序列,但需要注意的是,如果要对切片赋值,那么赋值运算符右侧必须是一个可迭代对象,即使只有一个单独的值,也要转化成可迭代对象。

五、对序列使用 + 和 *

python程序员默认序列是支持 + 和 * 的操作的。 + 用来拼接两个序列,返回一个拼接后的序列。* 用来将一个序列复制几份再拼接起来。+ 和 * 都不修改原有的操作对象。

        * 有一个坑(容器序列,其元素是可变对象,不能用 *,否则复制的都是同一批引用)。如下图例子,my_list是一个列表,其元素也是列表,我们知道列表是容器序列,容器序列中保存的是元素的引用,扁平序列中保存的才是数据。即序列my_list中的元素是对其它可变对象的引用。如果序列中的元素不是对可变对象的引用就没事,比如 my_list=[1, 2, 3, 4, 5, 6]。

那么怎么构建一个包含多个列表的列表呢?用列表推导式。如下图所示,原来列表推导式中用来构建列表的每一项也可以与自变量i无关。注意str是不可变序列,因此 ['_'] * 3 是合法的。

下边这种用 * 的方式是错误的。外层列表看似有三个元素,但是这三个元素都是指向同一个列表的引用。

六、序列的增量赋值 *= 与 +=

 以+=为例讲解,*=类似。

        += 背后的特殊方法是 __iadd__,这个方法是用于就地加法的。但是如果一个类没有实现__iadd__方法,则python会退一步调用__add__。

a += b

对上边例子来说,如果实现了__iadd__方法,且a为可变序列,则会对a进行原地改动,即把b赋值一份接到a上,效果等价于a.extend(b)。但是如果a没有实现__iadd__,则a += b等价于 a = a + b,即计算a + b生成一个新的对象再赋值给a,这个过程b没有变,但是变量名a已经被关联到了新的对象了。

        可变序列都实现了__iadd__方法,用于实现就地加法。不可变序列根本不支持这个操作,因此不会实现此方法。所以不可变对象的 += 和 *= 操作都是相当于把变量名关联到了一个新的对象。对于 *= ,其对应的方法是__imul__。

        一般情况下,对不可变序列进行重复拼接操作效率很低,因为需要构造一个新对象,解释器把原来对象中的元素赋值到先得对象,然后再追加新的元素。但是str类型是个例外,因为字符串 +=操作太普遍了,因此CPython对其进行了优化,为str对象初始化内存的时候,会留出额外的可扩展空间,因此增量操作的时候,不会涉及复制原有字符串到新位置这类操作。

        接下来看一个额外例子,能否改变元组中的可变对象?

如上例子所示,可以看到,元组中有可变对象列表,对列表的改动成功了,但是也抛出了异常。这给了我们一个教训,即不要在元组中放置可变对象。

七、list.sort方法与内置函数sorted

对于序列对象而言,排序是很重要的内容。

        list.sort会就地排序列表,返回值是None,这也是python的一个惯例,对于就地操作的方法返回None,比如random.shuffle也是这样。

        内置函数sorted则会返回一个列表,其接受任何形式的可迭代对象作为参数,但不管接受的是什么参数,其返回值都是列表。

        list.sort和sorted都有两个可选的参数。一个是reverse,如果设置为True,则会降序排列。还有一个可选参数key,这个参数是一个只接受一个参数的函数,其会对序列中的每一个元素进行操作(不改变原始序列中的值),产生结果是排序算法依赖的对比关键字。比如说 key=str.lower,将序列中每个字符都小写化,可以在排序的时候忽略大小写,或者key=len,则会基于字符串长度排序。key的默认值是恒等函数,即默认用元素自己的值来排序。

八、用bisect模块管理已排序的序列

bisect模块主要包含两个函数,bisect和insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。

用bisect搜索

用法为:position = bisect.bisect(haystack, needle)

其中haystack是一个排序好的序列,needle是待查找的值,返回值position是一个索引值,在索引position之前的索引处数字都是小于needle处的值的,比如下边例子,needle = 3,则position = 1,若needle = 4,则position = 2。

import bisect
haystack = [1, 4, 5, 6, 8, 12, 15, 20]
needle = 4

position = bisect.bisect(haystack, needle)
print(position)

        bisect有两个可选参数用于选定搜索的范围。lo和hi,lo默认是0,hi默认是序列的长度。

        bisect函数其实是bisect_right的别名,还有一个姊妹函数bisect_left。bisect_right返回的位置处是第一个大于needle的索引,bisect_left返回的是第一个小于等于needle的值所在的索引。比如下边例子,bisect_right返回3,bisect_left则返回1。

haystack = [1, 4, 4, 5, 6, 8, 12, 15, 20]
needle = 4

用bisect.insort插入新元素

用法:insort(seq, item)

        seq是排好序的有序序列,item是我们要插入的元素,用insort可以把变量item插入到序列seq中,并保持seq有序。insort相当于先查找再插入,即等价于先用bisect再用insert之类的插入。

        insort跟bisect一样,有lo和hi两个可选参数控制查找范围,也有个变体insort_left,此变体背后使用bisect_left,即在左边插入,insort则是在右边插入。

九、替换列表的数据结构

有时候列表不是最优选。比如要存放1000万个浮点数,数组(array)的效率要高很多,因为数组背后存放的不是float对象,而是数字的机器翻译,即字节表述。再比如,若要对序列频繁进行先入先出,deque(双端队列)的速度应该更快。

数组

这里指的是python标准库中的数组,即array.array。而不是numpy数组。

        如果我们想要一个只包含数字的列表,那么数组更合适。数组支持所有跟可变序列相关的操作,包括 .pop、.insert、.extend。数组还提供从文件读取和存入文件的更快的方法,如 .frombytes和 .tofile。

        创建数组需要一个类型码,类型码用来表示再底层的C语言应该存放怎样的数据类型。比如array('b')创建出的数组就只能存放一个字节大小的整数,‘b’代表有符号的字符,大小范围为 -128 到 127。这样指向以后,数组中就不能存放其它类型的数据。

        例题:创建一个1000万个浮点数的数组,把数组保存文件里,再从文件读取此数组。

from array import array
from random import random

floats = array('d', (random() for i in range(10**2))) # 生成器表达式可以用于生成任意序列类型,'d'表示是双精度浮点数对象,跟python内置类型不同,array.array是需要指定数值精度的
print(floats[-1]) # 查看数组中最后一个元素
fp = open('floats.bin', 'wb') # 以二进制可写的方式打开一个文件,如果文件不存在,则创建该文件
floats.tofile(fp) # 用.tofile方法把数组中的内容写入到二进制文件floats.bin中
fp.close() # 关闭文件
floats2 = array('d') # 再创建一个存储双精度浮点数的数组对象
fp = open('floats.bin', 'rb') # 打开上边关闭的文件
floats2.fromfile(fp, 10**2) # 用.fromfile方法从打开的文件中读取10**2个元素到新建的数组
fp.close() # 关闭文件
print(floats2[-1]) # 查看第二个数组的最后一个元素
print(floats2 == floats) # 判断两个数组是否相等

从以上例子可以看出,用python数组array.array是能够很方便地与文件进行交互的,通过tofile方法将数组内容存储到文件,再通过fromfile方法将文件中内容读取到数组。上边我们把浮点数写入读出二进制文件都要比文本文件快很多倍。而且二进制文件的大小也要更小一点。除了用array.array的tofile和fromfile方法外,还可以用pickle模块的dump方法将对象存储到文件,或者load方法从文件中加载信息到某个对象。pickle是python专用的,其文件后缀一般是 .pkl,可以选择是保存成文本文件或二进制文件。

 

python中的array.array非常类似C中的数组,也是要提供数据类型。需要注意的是array数组不支持list.sort这种原地排序方法,要给array数组排序的话只能先用sorted生成一个排序好的新数组,再让原数组名关联此新数组。对于查找和插入则仍然可以使用bisect模块来完成。

内存视图

memoryview是一个内置类,能让用户在不复制内容的情况下操作同一个数组的不同切片,本质上其实跟torch中的view方法思想有类似之处,就是从不同的角度看待同一块内存区域的数据。

from array import array
from random import random

numbers = array('h', [i for i in range(-2, 3)]) # 列表推导式生成数组,数组元素是十六进制整数
memv = memoryview(numbers) # 从内存角度看待该数组
print(len(memv), memv.tolist()) # 打印出数组长度于具体值,可以看出跟number一样
print(memv[0]) # 数组的第一个元素就是现在内存视图的第一个元素
memv_oct = memv.cast('B') # memv.cast类型强制转换,改成以无符号字符的角度来看待numbers数组的那一块内存
print(len(memv_oct), memv_oct.tolist()) # 因为一个无符号字符占八个字节,原先十六进制中的一个数字被拆分成了两个,因此内存视图长度加倍
memv_oct[5] = 4 # 改变内存视图的某个索引的值,可以影响到原数组,因为是共享内存的。
print(numbers)

NumPy和SciPy

numpy实现了多维同质数组和矩阵,scipy是基于numpy的库。numpy也可以轻易实现数组对象与文件的交互。numpy.save()用于将一个多维数组保存到一个二进制文件中。

双向队列和其它形式的队列

python中的列表可以当作栈或者队列使用。

        列表模拟队列:.append()插入元素,.pop(0)删除元素,即模拟了先进先出。

        列表模拟栈:.append()插入元素,.pop(l.size() - 1)删除元素,即模拟了后进先出。

        缺点是删除列表第一个元素之类的操作会移动列表中的所有元素,这些操作是非常耗时的。

collections.deque

        collections.deque类是一个线程安全、可以快速从两端添加或删除元素的数据类型,deque即双端队列。

需要注意的是在构造一个双端队列对象时有一个可选参数maxlen,代表这个队列可以容纳的元素的数量,这个属性一旦设定就不可修改。设定了这个属性以后,如果添加元素超出了容量,则会自动删除一部分元素,从左边插入就从右边删除,从右边插入就从左边删除。

        append和popleft都是原子操作,也就是说deque可以在多线程程序中当作先进先出的队列使用,即线程安全。

        还有一些其它的python库提供了队列的实现。

queue

multiprocessing

 asyncio

heapq

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值