目录
一、浅谈序列类型
提到python的序列类型,我们第一能想到的可能就是列表跟元组了。
我们再深入的想一下,可能又能想到。列表可变,元组不可变。但是其实他们的意义却不仅如此?
我们知道任何一门语言的序列类型都会分为可变和不可变。但是其实还有第二种分类,就是是否可以存放不同类型的数据。
也就是我们所说的扁平序列和容器序列。这两种序列那种更好呢?当然是看具体使用了。
python中的序列类型中标准库提供的有
list、tuple、collections.deque、str、bytes、bytearray、array.array
其他类包中比较出色的如 numpy.ndarry等
他们又分为容易序列和标准序列。
二、LIST
为什么我们称list为优秀的数据结构
可能我们在python中使用最多的就是列表,为什么我们会经常使用他呢。
- 他是可变的,我们在对他进行crud时十分方便。
- 他是有序的,我们可以通过索引轻松的对其操作。
- 他是友好的,我们可以在其内部存放字符串、数字、枚举、对象、函数以及我们想存放的一切。
也就是说list在设计之初,就为广大开发者考虑过了一个友好的数据结构。在python中就是list,他是一种可变的容器序列。
这里也简对常用的序列结构进行一个分类
序列 | 是否可变 | 类型 |
---|---|---|
list | 可变 | 容器 |
tuple | 不可变 | 容器 |
str | 不可变 | 扁平 |
.deque | 可变 | 容器 |
bytes | 不可变 | 扁平 |
array | 可变 | 扁平 |
列表推导式的运用与扩展
列表推导式
先来看一组案例。
income_list = [98000, 78600, 260000, 470000, 580000]
上述数据是某一部门的员工的年收入,现在我们想看一下该部门的年收入大于十万的员工的日收入。
如何求出呢,最直接的方法就是遍历这个列表,然后判断是否满足大于10万,再进行计算,放入新的列表返回。
但是这样会显得我们很呆,并且执行效率不高。稍微好一点的方法就是用filter
和map
函数。
def get_date_income(income):
return int(income//365)
date_income_list = list(map(get_date_income ,filter(lambda x: x >100000, income_list)))
[712, 1287, 1589]
乍看好像还不错,但在阅读时看起来。又是map又是filter难免有些不美,这时候我们可以试试列表推导式。
date_income_list = [get_date_income(_income) for _income in income_list if _income >100000]
这样在代码的阅读时可能更简单明要一些
拓展
列表推导式可能大家平时中运用很广,我们可以很容易的生成一个序列结构。
比如上述的员工收入中不包含年终奖,我们假设该部门每位员工的年终奖都是两万。我们想生成一个新的薪资列表。
income_list = [_income + 20000 for _income in income_list]
[118000, 98600, 280000, 490000, 600000]
但是在平时开发过程中,我们见得更多的是这种。
income_list = (_income + 20000 for _income in income_list)
和方括号不同这次用的圆括号,根据以往的经验方括号是列表推导式那么圆括号就一定是元组推导式了。其实往往经验的推断都是错的,他是生成器表达式。他生成的是一个可迭代对象。
生成器表达式跟列表推导式的区别就是。
年底发奖金,公司把钱发到每一个人的手里,这时候事件已经完成了。这种就是列表推导式。年底发奖金公司给每个员工生成一个奖金条,特定的条件触发。如员工主动去要或者把这个条交给财务,财务根据条进行发放,这是生成器表达式。
这种做的好处就是在效率更高了,比如我一段程序里需要算年收入大于十万的员工的日收入。但是不一定能用到,用到再说。如果说用到没用到都事先给其生成了效率是不高的。
第二个好处就是。这个对象是可迭代并且只能迭代一次,
for income in income_list:
print(income)
138000
118600
300000
510000
620000
当我们第二次迭代时则什么都不会打印,这无疑是更省内存的。
生成器表达式序列方法也很简单。
list(income_list)
[118000, 98600, 280000, 490000, 600000]
tuple(income_list)
(118000, 98600, 280000, 490000, 600000)
除了他俩之外,我们还可以使用字典推导式
好像官方并无此说法,比如我们需要对年收入进行排序,并且把排名写上生成字典
income_list.sort(reverse=True)
income_dict = {income[0]+1:income[1] for income in enumerate(income_list)}
{1: 580000, 2: 470000, 3: 260000, 4: 98000, 5: 78600}
三、tuple
元组仅仅是不可变的列表吗
元组是不可变的列表,相信很多人都有这样的认为。甚至很多老程序员也这样认为,这并不稀奇。但如果细加思考,为什么会让他不可变呢。
我们知道列表是可变的,可变给这个数据结构变得非常便捷。就像一个不羁的浪子,拥有各种炫技的技能。有不羁的就有稳重的,俗话说老成谋国。在编程中我们需要不羁的浪子。
但我们也需要老成稳重的。比如我们在进行信息记录时则需要信息记录的数据结构是不能变的。
city_info = ('北京', '一线城市', '北方')
city, level, location = city_info
这样我们就取到了对应的记录,这个时候如果我们用列表来记录这些信息。显然是不合适的。
所以说元组在python中是天生的记录结构。当然这只是一种思想,你当然也可以自己构造一个数据结构来存放记录数据。
元组拆包
大家在学习其他编程语言时如果让两个变量交换值,可能的伪代码大概是这样的。
c = a
a = b
b = c
往往需要一个中间的变量,但是在python中却不必如此。
a, b = b, a
这样就可以了,你很难在其他编程语言中找到如此优雅的写法。这与python的特性相关,而实际上这种实现就依赖于元组的特性,我们且来看一下这是如何实现的。
a=1
b=2
a,b
(1, 2)
其实a, b即为一个元组。
而 b, a = a, b 就相当于 b, a = (1, 2)
自然而然变量b, a 则得到各自的值,这个其实就是一种元组拆包的写法。但是我们在学习python中往往一开始就接触到这种写法,所以会觉得就应该如此。
其他元组拆包的用法,在比如上述的元组中。
city_info = ('北京', '一线城市', '北方')
如果我们在获取城市信息的时候,只想获取城市的名称而不想获取其他信息。
再使用
city, level, location = city_info
难免臃肿,尤其当元组中有很多元素时,难道也有写众多变量吗。有些可以通过索引值进行获取,但当需要取特定几个值的信息时则又为不便。我们可以用拆包的方法进行书写。
city, *other = city_info
city
北京
我们在来看一下other是什么
['一线城市', '北方']
我们再来看一下其他的用法,定义一个求平方和的函数
def sum_of_squaresdef(a ,b):
print((a+b)**2)
sum_of_squaresdef(1 ,2)
9
tuple1 = (2 ,3)
sum_of_squaresdef(*tuple1)
25
四、数组
列表虽然非常强大,但是因为其本身的类型是容器类型。其效率难以保证,这个时候我们就可以使用数组。
在python中使用较多的数组有两种,一种是标准库中的数组。
array.array
from array import array
from random import random
number_list = array('I', (i for i in range(100)))
number_list[1:8]
array('I', [1, 2, 3, 4, 5, 6, 7])
其用法则与列表类似,但是由于其只保存一种数据类型。其效率更高。而同时数组的创立时则需要先指定其类型。
numpy.ndarray
numpy.ndarray也是只存放一种数据类型但是比原生的array更智能一些。
numpy.array([1, 2, 3, 4, 5])
numpy.array([1, 2, 3, 4, '5'])
array([1, 2, 3, 4, 5])
array(['1', '2', '3', '4', '5'], dtype='<U11')
他也是只存放一种数据类型,但是他会根据输入的数据,向下兼容。
这里我们再来讨论一下无论是python自带的数组也好,还是numpy提供的数组,为什么都要求只能存放一种数据类型呢。是因为数据结构中对数组的要求有两点。1.线性表 2.连续的内存空间和相同的数据
至于为什么会做此要求,感兴趣的同学可以去搜索一下。
五、被遗忘的序列管理器bisect
bisect的使用场景
bisect 属于python标准库的内置序列管理器,他需要管理的则是已经排序好的序列类型。
data = [4, 2, 9, 7]
data.sort()
[2, 4, 7, 9]
import bisect
bisect.bisect(data, 8)
3
他会返回被插入值将会出现在列表的位置,而并没有真的插入
data
[2, 4, 7, 9, 100]
如果我们想真正插入,则可以
index = bisect.bisect(data, 8)
data.insert(index, 8)
data
[2, 4, 7, 8, 9]
这样既实现了插入,又能在插入前做一个判断。并且bisect底层用的是二分法,这种查询在有序列表中的速度会非常快。
当然如果我们想直接插入,并保持序列的顺序而并不想先查询要插入值的位置,当然也有更简便的用法。
bisect.insort
bisect.insort(data, 6)
data
[2, 4, 6, 7, 8, 9]
这里再来说一下无论是bisect.bisect 还是 bisect.insort都是使用二分法,效率同样很快。所以在进行选用的时候完全可以根据自己的使用场景进行选择。
六、双向队列
当我们在使用栈的数据结构时,可以通过list来实现很多数据结构。如通过append 与 pop(0) 就可以实现队列。append 和pop()来实现栈。
但是事实上这种实现方式并不合理,因为这种实现其实是列表中每个元素都进行移动。
如果数据量比较少的时候尚可。一旦数据量级一上去,虽然也能用,但难免显得不那么优雅。
这时候我们就可以使用队列
from collections import deque
dq = deque(range(3), maxlen=5)
这样我们就新建了一个队列
deque([0, 1, 2])
队列的使用与列表大致相同。
我们可以使用append方法。
dq.append(5)
dq.append(6)
deque([0, 1, 2, 5, 6])
dq.append(7)
deque([1, 2, 5, 6, 7])
这个时候有个奇怪的现象出现,虽然7也被加入到队列中。但是队列本身头部的0却不见了,为什么会如此呢。因为我们在创建队列时指定了长度为5所以队列中只会有5个元素。这样我们在append的时候头部的0自然而然被挤了出去。
由于这种特性我们不仅可以从尾部加入数据也可以从头部加入数据。
dq.appendleft(8)
deque([8, 1, 2, 5, 6])
这次是从头部加入队列,那么尾部的元素自然也被挤了出去。除此之外还有rotate方法。
dq.rotate(1)
deque([6, 8, 1, 2, 5])
rotate就是移动队列中的元素当然这种元素一定是有序的。
rotate是移动的值如果是正数则会向右移动,最右边的值则会移动到最左边。这是个环形的队列就像北京地铁10号线一样。
当然如果为负数则会向左移动。
dq.rotate(-2)
当然也支持pop等方法,这种都与列表的使用大体一致了这里就不过多解释了。
deque([1, 2, 5, 6, 8])
并且append 和 popleft 都是原子操作,也就说是 deque 可以在多线程程序,而使用者不需要担心资源锁的问题。
所以这种双向队列在我们设计数据结构时同样也是一个非常不错的选择。
其他
另外其实在真正的开发中,线程及进程中的通信则用的是另一种数据结构。queue.Queue, 这个数据结构同样是安全的。
因为他是对deque做了一层新的封装,本篇主要是对python的基础数据序列类型进行介绍。故此对于Queue不再做过多的解释,如有感兴趣的同学可搜索其他博客进行学习。