Python的序列类型非常丰富,包括了列表(list),元组(tuple),字符串(str), 字节数组(bytes),队列(deque),今天我们就着重了解下python中的这些内置序列类型。
内置序列类型介绍
python标准库中的序列类型使用C语言实现,大体上可分为下面几类。
容器序列
list、tuple、collections.deque等,能存放不同类型的数据
扁平序列
str、bytes、bytearray、memoryview(内存视图)、array.array等,存放的是相同类型的数据
容器序列和扁平序列有什么不同呢?
容器序列存放的实际上是对象的引用,因此可以存放不同类型的数据;扁平序列存放的是对象的值,是一段连续的内存空间,因此要求对象必须是相同类型的数据才行,如字符、数字、字节。
当然,序列类型也可以按照能否被修改来分类。
可变序列
list、bytearray、array.array、collections.deque、memoryview。
不可变序列
tuple、str、bytes。
所有的序列类型中最重要也最基础的就是列表了。列表可以存放各种不同类型的数据。作为已经对python有所了解的读者,对列表应该很熟悉了。所以,我们直接开始讨论列表推导吧。
列表推导,洋文名list comprehension,是一种构造列表的方法,异常强大而且非常常用,但是却很简单。掌握列表推导后,还能帮助我们理解生成器的概念。让我们直接开始吧!
列表推导和生成器表达式
列表推导和可读性
先来看一个例子:
1 symbols = '¥#%&£γ' 2 codes = [] 3 for symbol in symbols: 4 codes.append(ord(symbol)) 5 print(codes)
以上代码将字符串symbols中的每个字符的ascii码追加到codes中,下面我们使用列表生成式来达成的目的
1 symbols = '¥#%&£γ' 2 codes = [ord(symbol) for symbol in symbols] 3 print(codes)
可以看出,列表推导使用更少的代码完成了相同的事,并且具有更高的可读性,这也符合python的哲学理念。事实上,很多使用for循环完成的列表操作都可以使用列表推导,然而,要注意的是避免列表推导被滥用。如果列表推导的代码太多、太复杂的话,你就得重新考量是不是要用for循环重写了。
注意:python会忽略代码[],(),{}中的换行,因此位于[],(),{}中的代码可以省略掉\,直接换行。
列表推导同filter和map的比较
filter和map是python函数式编程中常用的两个方法,对于一些简单的创建表的操作,列表推导比filter和map使用起来更方便。
1 symbols = '¥#%&£γ' 2 codes1 = [ord(s) for s in symbols if ord(s) > 200] 3 codes2 = list(filter(lambda c:c>200, map(ord, symbols)))
扑克牌
扑克牌出去大小王分为2,3,4,5,6,7,8,9,10,J,Q,K,4种花色,黑桃(spades)、红桃(hearts)、梅花(clubs)、方块(diamonds)。下面我们使用列表推导的方法完成扑克牌的创建。
1 from collections import namedtuple 2 cards = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'] 3 kinds = ['spades', 'hearts', 'clubs', 'diamonds'] 4 PokerCard = namedtuple('PokerCard', ('card', 'kind')) 5 PokerCards = [PokerCard(card, kind) for card in cards for kind in kinds] 6 print(PokerCards)
namedtuple是collections模块提供的具名元组,可以以下面的方式调用
PokerCard[0], PockerCard[1], PockerCard.kind,PockerCard.card
最后总结一下,列表推导的作用只有一个就是生成列表,一定不要为了列表推导而列表推导。
生成器表达式
列表推导可以很方便的用来生成列表,但是假如让你一下子生成100万个数字呢?1000万个呢?这个时候列表就不合适了,该轮到我们的生成器表达式大显身手了。
生成器表达式遵循了迭代器协议(实现了__iter__和__next__方法),它并不会产生任何的元素,而是以定义了某种生成元素的规则,在你真正需要的时候,它才会逐个的产生元素,跟数学中集合描述法的定义很相似。所以无论你要用生成器表达式生成多少个元素,它占用的实际内存依然很小。
生成器表达式的语法规则和列表推导差不多,只是把方括号[]变成了圆括号()
1 symbols = '$¢£¥€¤' 2 codes = list(ord(s) for s in symbols) 3 print(codes) 4 import array 5 arr = array.array('I', (ord(s) for s in symbols)) 6 print(arr)
若果生成器表达式是函数的唯一参数,可以不用括号;array是array模块提供的数组,需要指定存储的数据的类型,I表示无符号整数。
元组
元组常常又被称为“不可变列表”,的确如此,但除此之后,元组还具有字段记录的功能。这一点常常被人忽略,没有引起足够的重视。
元组和记录
元组也是对数据的记录,元组中的每个元素都有相应的意义。
元组拆包
可以使用平行赋值的方式对元组进行拆包
#元组拆包 city, population, country = ('beijing', 20000000, 'China') print(city, population, country) #使用*处理不在意的元素 a, b, *rest = range(5) print(a) print(b) print(rest) a, *rest, b = range(5) print(a) print(b) print(rest)
具名元组
还记得上面的扑克牌的例子吗?有的时候,我们还想要对元组里的字段进行命名,而collections.namedtuple恰恰就是为此而生的。
那么如何使用具名元组呢?
1 City = namedtuple('City', ('name', 'country', 'population', 'location')) 2 beijing = City('beijing', 'China', 1300000000, (35, 120)) 3 print(beijing) 4 print(beijing.name, beijing.country, beijing[3])
namedtuple接收两个参数,一个是元组名,一个是各个字段的名称构成的集合,可以是元组也可以是由空格分开的字符串,具名元组使用下标或者成员访问符.的方式访问每个字段。
切片
列表、元组、字符串都支持切片操作。
切片为什么要忽略最后一个元素
python的绝大部分包含区间范围的方法中都是默认忽略最后一个元素的,例如range(10)会忽略最后的元素10,在其他编程语言中也如此。这样做的好处也是显而易见的。
1、切片只包含最后一个元素时,很方便的就可以知道元素的数量,如mylist[:3]有3个元素
2、切片包含start和end两个时,元素数量就是end-start
3、可以很方便的把序列分成互不重叠的两部分 list[:x], list[x:]
对对象进行切片操作
1 s = 'bicycle' 2 print(s[:3])#'bic' 3 print(s[::-1])#反向取值 elcycib 4 print(s[::-2])#反向取值,步长为2 eccb
list.sort方法和内置sorted
list.sort会把列表就地排序,不会产生新的列表,因此list.sort返回None,这是python的惯例,如果一个方法对对象进行了就地改动,那他就应该返回None。
sorted是内置的排序函数,他会返回一个新的列表。
两个函数都有两个关键字参数。
reverse
为True时,元素从大到小排序,默认False
key
一个接受一个参数的函数,返回的结果会被用来作为比较的关键字进行排序
key=str.lower可以实现忽略大小写, key=str.len以长度作为排序
1 fruits = ['grape', 'apple', 'banana', 'pear', 'cheery'] 2 print(sorted(fruits)) #默认以字典序排序 3 print(sorted(fruits, reverse=True)) #逆序输出 4 print(sorted(fruits, key=str.__len__)) #以字符串的长度为关键字进行排序 5 print(fruits.sort()) #None, fruits原地排序 6 print(fruits ) #已排序
其他序列类型
列表既灵活又简单,但在某些需求时,我们可能会有更好的选择。比如要存放1000万个浮点数的话,数组(array)的效率就要高得多,再比如如果需要对序列进行频繁的先进先出的操作,双端队列(deque)无疑是更好的选择。
数组
数组可以看成是只允许存放固定类型数据的高效化列表,数组支持所有list的方法,如pop, insert, extend, insert。此外数组还提供从文件读取和存入文件更快的方法,frombytes, tofile。
数组的创建array需要两个参数,一个是数组的类型类型码,另一个是个列表或者是一个可迭代对象。
类型码如下:
类型码 类型 字节
'b' 整型 1
'B' 无符号整型 1
'u' unicode字符 2
'h' 整型 2
'H' 无符号整型 2
'i' 整型 2
'I' 无符号整型 2
'l' 整型 4
'L' 无符号整型 4
'f' 浮点型 4
'd' 浮点型 8
1 from random import random 2 from array import array 3 4 #创建一个包含10000个8字节浮点数的数组 5 floats = array('d', (random() for _ in range(10**4))) 6 print(floats[0], floats[-1], len(floats)) 7 8 #必须要以二进制的方式读写文件 9 with open("floats.txt", "wb") as fp: 10 floats.tofile(fp) 11 12 #创建一个空数组 13 floats2 = array('d') 14 with open("floats.txt", "rb") as fp: 15 floats2.fromfile(fp, 10**4) #第二个参数可以知道读取多少个元素 16 #floats2.frombytes(fp.read()) 与上面是等价的 17 print(floats2[0], floats2[-1], len(floats2)) 18 print(floats==floats2)
除数组外,pickle是另一个能够序列化数字类型的方法,pickle几乎可以处理所有的内置类型甚至包括用户自定义的类。
内存视图
内存视图(memoryview)实际上跟C语言中的类型转换类似,memoryview.cast它引用一块内存并且可以以指定的方式打包成一个全新的对象返回给你。
1 #创建一个16位二进制数组 2 nums = array('h', [-2,-1,0,1,2]) 3 #该数组的内存视图 4 memv = memoryview(nums) 5 print(memv[0], len(memv)) 6 7 #该内存视图以无符号8位二进制方式呈现 8 memv_oct = memv.cast('B') 9 print(list(memv_oct)) 10 11 #等价于把nums[2]的高4位变成了4,所以nums[2]成了1024 12 memv_oct[5] = 4 13 print(nums)
numpy和scipy简单介绍
相信你即便没有使用过numpy和scipy这两个模块,也一定听说过它们的大名。目前流行的大数据分析、人工智能很大程度上都依赖于这两个模块,这也得益于numpy和scipy提供的性能优越的高阶数组和矩阵操作。如果要详细介绍它们,那不写几本书是不可能,所以这里只是简单介绍下它们对多维数组的基本操作,作为对python基本序列类型的扩展和补充。
1 import numpy 2 a = numpy.arange(12) 3 #print(a) 4 a.shape=(3,4) 5 print(a) 6 #print(a[2]) 7 #print(a[:, -1]) 8 #print(a[:,::2]) 9 print(a.transpose())
双端队列和其他形式的队列
使用列表的append和pop方法我们也可以模拟出队列“先进先出”的特性,但是列表的删除第一个元素或者添加第一个元素是很耗时的操作,因为牵扯到后面数据的移动。
collections.deque双端队列可以快速的在两头添加、删除元素,append和popleft、appendleft和pop都是原子操作,意味着collections.deque是线程安全的,无需考虑锁的问题。
1 from collections import deque 2 dq = deque(range(10), maxlen=10) 3 print(dq) 4 5 #旋转操作 6 dq.rotate(3) 7 print(dq) 8 9 #队尾追加元素 10 dq.append(11) 11 print(dq) 12 13 #移除队头的元素 14 print(dq.popleft()) 15 print(dq)
值得注意的是,deque有一个maxlen参数表明队列可以容纳的元素数量,在队列已满的情况下在向队列里添加元素的时候,则队头或者队尾的元素会被移除。
此外python还有其他模块提供了队列。
queue
提供了同步(线程安全)类Queue、LifoQueue、PriorityQueue,构造方法中都有一个maxsize可选参数表示队列的容量,与collections.deque不同的是,当队列满时,队列会被锁住,直到有其他线程取出了队列中的元素为止。
multiprocessing
实现了自己的Queue,与queue.Queue类似,是用来设计给进程间通信使用。
asyncio
异步编程提供的Queue、LifoQueue、PriorityQueue和JoinableQueue。
heapq
heapq没有队列类,但是提供了heappush和heappop方法。