不一样的列表

时间:2023-01-16 20:18:47

列表作为最常见最常用的数据结构之一,也叫做可变容器。

一、不同分类

(1)容器序列:tuple, list, collection.deque这些可以存储不同类型数据的容器。

(2)扁平序列:str, bytes,array.array这些只能存容纳一种类型数据。

list容器序列可以存储不同类型的数据,但是这样做其实并没有什么好处,当一个列表中既有字符串,又有整形,那么对这个列表对象的内置函数或者特殊方法的调用就收到了限制

lst = list(range(3))
lst.appen('a')
lst
#输出
[1, 2, 3, 'a']

lst.sort() # error

容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。

序列类型还能按照能否被修改来分类

(1)可变序列:list、bytearray、array.array、collections.deque 和 memoryview

(2)不可变序列: tuple,str, bytes等

二、列表推导式

一种快速高效的写法,当然这是以部分可读性为代价的。(list comprehension)

来看下面例子

string_list = [chr(i) for i in range(97, 122)]
string_list
#输出
['a', 'b', 'c',......'y', 'z']
string_list = []
for i in range(97, 122):
    sting_list,append(chr(i))
sting_list
#输出 ['a', 'b', 'c',......'y', 'z']

python2版本的可能情况

a = 'a'
s = [a for a in range('asdf')]
print(a)

#输出
f
结果可能不是预期的a    

而在python3中不会出现这种情况,而python2也即将在2020年被抛弃,在python3中无论是列表推导,还是生成器表达式,或者是set,dict推到,都有自己局部的作用域,也就是说,表达式内部的变量和赋值不会只作用在局部,不会影响到全局。

2.1、用列表推导完成笛卡尔积:

string_int = [(chr(s), d) for s in range(97, 122) for d in range(10) ]
print(string_int)

输出:

不一样的列表不一样的列表
[('a', 0), ('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8), ('a', 9), ('b', 0), ('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), ('b', 6), ('b', 7), ('b', 8), ('b', 9), ('c', 0), ('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8), ('c', 9), ('d', 0), ('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), ('d', 6), ('d', 7), ('d', 8), ('d', 9), ('e', 0), ('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8), ('e', 9), ('f', 0), ('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 7), ('f', 8), ('f', 9), ('g', 0), ('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8), ('g', 9), ('h', 0), ('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8), ('h', 9), ('i', 0), ('i', 1), ('i', 2), ('i', 3), ('i', 4), ('i', 5), ('i', 6), ('i', 7), ('i', 8), ('i', 9), ('j', 0), ('j', 1), ('j', 2), ('j', 3), ('j', 4), ('j', 5), ('j', 6), ('j', 7), ('j', 8), ('j', 9), ('k', 0), ('k', 1), ('k', 2), ('k', 3), ('k', 4), ('k', 5), ('k', 6), ('k', 7), ('k', 8), ('k', 9), ('l', 0), ('l', 1), ('l', 2), ('l', 3), ('l', 4), ('l', 5), ('l', 6), ('l', 7), ('l', 8), ('l', 9), ('m', 0), ('m', 1), ('m', 2), ('m', 3), ('m', 4), ('m', 5), ('m', 6), ('m', 7), ('m', 8), ('m', 9), ('n', 0), ('n', 1), ('n', 2), ('n', 3), ('n', 4), ('n', 5), ('n', 6), ('n', 7), ('n', 8), ('n', 9), ('o', 0), ('o', 1), ('o', 2), ('o', 3), ('o', 4), ('o', 5), ('o', 6), ('o', 7), ('o', 8), ('o', 9), ('p', 0), ('p', 1), ('p', 2), ('p', 3), ('p', 4), ('p', 5), ('p', 6), ('p', 7), ('p', 8), ('p', 9), ('q', 0), ('q', 1), ('q', 2), ('q', 3), ('q', 4), ('q', 5), ('q', 6), ('q', 7), ('q', 8), ('q', 9), ('r', 0), ('r', 1), ('r', 2), ('r', 3), ('r', 4), ('r', 5), ('r', 6), ('r', 7), ('r', 8), ('r', 9), ('s', 0), ('s', 1), ('s', 2), ('s', 3), ('s', 4), ('s', 5), ('s', 6), ('s', 7), ('s', 8), ('s', 9), ('t', 0), ('t', 1), ('t', 2), ('t', 3), ('t', 4), ('t', 5), ('t', 6), ('t', 7), ('t', 8), ('t', 9), ('u', 0), ('u', 1), ('u', 2), ('u', 3), ('u', 4), ('u', 5), ('u', 6), ('u', 7), ('u', 8), ('u', 9), ('v', 0), ('v', 1), ('v', 2), ('v', 3), ('v', 4), ('v', 5), ('v', 6), ('v', 7), ('v', 8), ('v', 9), ('w', 0), ('w', 1), ('w', 2), ('w', 3), ('w', 4), ('w', 5), ('w', 6), ('w', 7), ('w', 8), ('w', 9), ('x', 0), ('x', 1), ('x', 2), ('x', 3), ('x', 4), ('x', 5), ('x', 6), ('x', 7), ('x', 8), ('x', 9), ('y', 0), ('y', 1), ('y', 2), ('y', 3), ('y', 4), ('y', 5), ('y', 6), ('y', 7), ('y', 8), ('y', 9)]
View Code

2.2、列表推导生成一摞纸牌

ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
[(rank, suit) for suit in suits for rank in ranks]

三、切片的强大:

>>> l = [10, 20, 30, 40, 50, 60]
>>> l[:2] # 在下标2的地方分割
[10, 20]
>>> l[2:]
[30, 40, 50, 60]
>>> l[:3] # 在下标3的地方分割
[10, 20, 30]
>>> l[3:]
[40, 50, 60]

以0为下标开始这样做的好处

(1)、当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3) 和 my_list[:3] 都返回 3 个元素。

(2)、当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。

(3)、这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x] 和 my_list[x:] 就可以了

3.1、给切片赋值:可迭代对象的重要

>>> 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切片是一个可迭代对象,而100却不是,当我们把100转化成[100]这个可迭代序列

>>> l[2:5] = [100]
>>> l
[0, 1, 100, 22, 9]

四、对列表序列使用+和*

如果你熟悉字符串拼接的话,*,+一定经常使用,对于列表也是可以使用+和*的,对一个对象复制几份在拼接起来,这是个不错的选择

>>> l = [1, 2, 3]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'

但这并不是说可以为所欲为,来看下面例子

一个包含 3 个列表的列表,嵌套的 3 个列表各自有 3 个
元素来代表井字游戏的一行方块

>>> weird_board = [['_'] * 3] * 3 
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O' 
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]


就像这样

row=['_'] * 3
board = []
for i in range(3):
  board.append(row)

每次都会向列表board中追加一个row,但其实追加的都只是对同一个row的引用

上面例子其实说明了,如果你想使用mylist = [ [] ] * 3 初始化得到的一个包含三个列表的列表对象,就像这样mylist [[], [], []],中确实包含三个对象,但是其实你的列表中的三个元素都是对同一个列表对象的引用,并且都指向同一个列表。

正确的做法应该是这样

>>> board = [['_'] * 3 for i in range(3)] 
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X' 
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

>>> board = []
>>> for i in range(3):
  row=['_'] * 3
... board.append(row)

# 每次都会新建一个row列表对象

五、列表序列的增量赋值

也许在python中不支持++或者—这种自加自减的操作,那么如果能利用好*= 和+=也不错。

增量赋值运算符 += 和 *= 的表现取决于它们的第一个操作对象。

# 像这样

  ls = [1]
  ls += [2]
  ls

  [1, 2]

 # 或者

  ls = [1]

  ls *= 2

  ls

  [1, 1]

+= 背后的特殊方法是 __iadd__ (用于“就地加法”),就像ls.extend([1]), 但是如果一个类没有实现这个方法的话,Python 会退一步调用 __add__ ,就会变成ls = ls + ls, 也就是说两个ls的和然后再赋值给一个新的ls列表,这当然要比__iadd__方法实现要麻烦

和占用时间和内存的多。所以__iadd__方法是否存在也决定了表达式+=有没有关联到一个新的对象。

六、list.sort()与sorted(list)

list.sort 方法会就地排序列表,也就是说不会把原列表复制一份。这也是这个方法的返回值是 None 的原因,提醒你本方法不会新建一个列表。在这种情况下返回 None 其实是 Python 的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返

回 None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象.

而sorted(list)恰好相反,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器(见第 14 章)。而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表。

不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数。

sorted(list, reverse=false, key=可选参数)
list.sort( reverse=false, key=可选参数)
ls = ['b', 'asdf', 'cd']
ls.sort(reverse=True)
print(ls)
ls.sort(key=len)
print(ls)
ls.sort(reverse=True, key=len)
print(ls)
# 输出
['cd', 'b', 'asdf']
['b', 'cd', 'asdf']
['asdf', 'cd', 'b']

七、bisect

让我们来尝试一下用Python标准库中的模块来实现二分法来在列表中查找或者插入元素

# 插入元素
import
bisect ls = list(range(10)) print(ls) bisect.insort(ls, 7.0) print(ls) bisect.insort_left(ls, 9.0) print(ls) #output [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 7.0, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 7.0, 8, 9.0 9]
# 查找元素
ls = list(range(10)) print(ls) #output [1,2,3,4,5,6,7,8,9] bisect.bisect(ls, 3.0) #右边 #output 4 bisect.bisect_left(ls, 4.0) #查找并返回元素左边 #output 4

7.1、根据一个分数找到相应的成绩

def grade(socre, breakpoints=[60, 70, 80, 90], grades="FDCBA"):
    i = bisect.bisect(breakpoints, socre)
    return grades[i]

[grade(score) for score in [60, 60, 54, 77, 90, 91, 85] ]

# output
['D', 'D', 'F', 'C', 'A', 'A', 'B']

 八、比列表更好的选择

也许列表很好用,但是在面对不同的需要时,总有一些其他的更好的选择

8.1、数组 array.array

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

Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。比如 b 类型码代表的是有符号的字符(signed char),因此 array('b') 创建出的数组就只能存放一个字节大小的整数,范围从 -128

到 127,这样在序列很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存放除指定类型之外的数据。

8.2、内存视图 memoryview

memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。内存视图其实是泛化和去数学化的 NumPy 数组。它让你在不需要复制内容的前提下,在数据结构之间共享内存。

8.3、numpy

利用numpy,可以很好的处理数组

>>> 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]])
>>> a[2] 
array([ 8, 9, 10, 11])
>>> a[2, 1] 
9
>>> a[:, 1] 
array([1, 5, 9])
>>> a.transpose() 
array([[ 0, 4, 8],
    [ 1, 5, 9],
    [ 2, 6, 10],
    [ 3, 7, 11]])

 安装 NumPy 之后,导入它(NumPy 并不是 Python 标准库的一部分)。

 新建一个 0~11 的整数的 numpy.ndarry,然后把它打印出来。

 看看数组的维度,它是一个一维的、有 12 个元素的数组。

 把数组变成二维的,然后把它打印出来看看。

 打印出第 2 行。

 打印第 2 行第 1 列的元素。

 把第 1 列打印出来。

 把行和列交换,就得到了一个新数组。

8.4、双端队列

使用list时经常会牵涉到一些操作,例如删除第一个元素,添加一个元素到列表的第一个位置,也就是内置函数insert(), pop(),append(),利用这些方法可以模拟栈或者队列的特性,先进先出,或者先进后出,但是这对于列表来说都是很耗费操作的,因为往往一个元素的变动会影响到其他元素的位置变动。

面对这种情况,collections.deque(双向队列)可以帮助我们

>>> 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)

 maxlen 是一个可选参数,代表这个队列可以容纳的元素的数量,而且一旦设定,这个属性就不能修改了。

队列的旋转操作接受一个参数 n,当 n > 0 时,队列的最右边的 n个元素会被移动到队列的左边。当 n < 0 时,最左边的 n 个元素会被移动到右边。

 当试图对一个已满(len(d) == d.maxlen)的队列做尾部添加操作的时候,它头部的元素会被删除掉。注意在下一行里,元素 0 被删除了。

 在尾部添加 3 个元素的操作会挤掉 -1、1 和 2。

extendleft(iter) 方法会把迭代器里的元素逐个添加到双向队列的左边,因此迭代器里的元素会逆序出现在队列里。

很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存
放除指定类型之外的数据。