Python 内置数据结构(一)

从上一篇“Python入坑指北”开始,一系列的Python文章,是关于Python的学习笔记,参考书是《Python for Data Analysis, 2nd Edition》,github的仓库有详细的jupyter notebook(https://github.com/wesm/pydata-book)。

元组(tuple)

1.1 初始化

1
2
3
4
5
6
7
8
9
# 初始化
tup1 = 4, 5, 6
print(tup1)
tup2 = 'hello', [1, 3], (4, 6), 0.5, 1, 1==1, {'a':1, 'b':2}
print(tup2)
tup3 = tuple([4, 0, 2])
print(tup3)
print(tuple('string'))
print(tuple(['foo', [1, 2], True]))

以上代码输出结果:

1
2
3
4
5
(4, 5, 6)
('hello', [1, 3], (4, 6), 0.5, 1, True, {'a': 1, 'b': 2})
(4, 0, 2)
('s', 't', 'r', 'i', 'n', 'g')
('foo', [1, 2], True)

可以看出几个重要的点:

  1. 元素以逗号“,”分割,以括号( )包含一个元组的所有元素;
  2. 一个tuple里面可以同时容纳多种类型的元素,且不需要声明tuple类型
  3. 可以使用list进行tuple的初始化;
  4. 可以将一个string转化为tuple;

1.2 元素访问

1
2
3
4
5
6
tup1 = tuple('string')
print(tup1[0])
tup2 = tuple(['foo', [1, 2], True])
tup2[1].append(3)
print(tup2)
tup2[2] = False

以上代码输出结果:

1
2
3
4
5
6
s
('foo', [1, 2, 3], True)
Traceback (most recent call last):
File "ch3_1.py", line 6, in <module>
tup2[2] = False
TypeError: 'tuple' object does not support item assignment

我们总结下访问的几个注意事项:

  1. 使用中括号 [ ] 进行元组元素的访问;
  2. 元组中元素只可读,不可写;

== ,说好的元素只读,那上面代码的第4行是怎么回事,为什么还能添加一个元素?其实tup2[1]指向的是一个list,在这里是元素指向的list不可变,list本身是可以变更的。

1.3 元组的解包 (Unpacking tuple)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
tup = (4, 5, 6)
a, b, c = tup
print(b)

tup = (4, 5, (6, 7))
a, b, (c, d) = tup
print(d)

# 交换变量赋值
a, b = 1, 2
b, a = a, b
print(a, b)

# 用于元组和数组的迭代解包
seq = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
for a, b, c in seq:
print('a={0}, b={1}, c={2}'.format(a, b, c))

# 函数的多返回值
values = 1, 2, 3, 4, 5
a, b, *rest = values
print(a, b)
print(rest)
# 如果rest此时是不需要使用的变量,可以如下表示,使用‘_’当做占位符
a, b, *_ = values

以上代码输出结果:

1
2
3
4
5
6
7
8
5
7
2 1
a=1, b=2, c=3
a=4, b=5, c=6
a=7, b=8, c=9
1 2
[3, 4, 5]

元组的解包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接收这些元素的元组的空档数一致;也可以用*来表示忽略多余的元素(如上代码所述);

一般的迭代对象包括:元组,列表,字典,字符串。

1.4 常用类方法

如果在ipython中或者Jupyter notebook中,使用TAB键就能列出tuple的类方法,下面说几个常用方法;

1
2
3
tup = (1, 2, 3, 3)
tup.count(3) # 计算3在tuple中的个数
tup.index(2) # 返回第一个2在tuple中的位置

列表(list)

与tuple相比,list就是可变长的,可直接修改相应元素的对象,使用中括号 [] 进行初始化。

2.1 初始化

  • 自定义初始化list
1
2
3
4
5
6
a_list = [2, 3, 7, None]
tup = ('foo', 'bar', 'baz')
b_list = list(tup)
print(b_list)
b_list[1] = 'peekaboo'
print(b_list)

列表和元组在语法上有点类似,list可以直接从tuple初始化得到,两者之前最大的差异就是可修改与不可修改

以上代码执行的结果如下,可以看到第5行代码修改了list的第二个元素

1
2
['foo', 'bar', 'baz']
['foo', 'peekaboo', 'baz']
  • list常用来实现一个迭代器或者生成表达式:
1
2
3
4
gen = range(10)
print(gen)
g_list = list(gen)
print(g_list)

输出结果如下:

1
2
range(0, 10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

2.2 添加和删除元素

1
2
3
4
5
6
7
8
9
10
11
12
13
b_list = ['foo', 'bar', 'baz']
b_list.append('dwarf') # 末尾添加元素
print(b_list)
b_list.insert(1, 'red') # 指定index添加元素
print(b_list)
b_list.pop(2) # 指定index删除元素
print(b_list)
b_list.append('foo') # 末尾添加元素
print(b_list)
b_list.remove('foo') # 删除第一个foo
print(b_list)
print('dwarf' in b_list) # 判断dwarf是否在list中
print('dwarf' not in b_list)

可以看到,添加元素有append, insert方法,删除元素有pop, remove方法,其他有关list的方法,可以在ipython中使用TAB键进行查看;

如果不考虑性能,可以使用append和remove,不然可以使用Python的另外一个数据结构 ‘multiset’,以此来优化性能;

输出结果:

1
2
3
4
5
6
7
['foo', 'bar', 'baz', 'dwarf']
['foo', 'red', 'bar', 'baz', 'dwarf']
['foo', 'red', 'baz', 'dwarf']
['foo', 'red', 'baz', 'dwarf', 'foo']
['red', 'baz', 'dwarf', 'foo']
True
False

2.3 列表的拼接和组合

1
2
3
4
print([4, None, 'foo'] + [7, 8, (2, 3)])
x = [4, None, 'foo']
x.extend([7, 8, (2, 3)]) # 已定义的list,使用extend进行扩展组合
print(x)

以上代码输出结果:

1
2
[4, None, 'foo', 7, 8, (2, 3)]
[4, None, 'foo', 7, 8, (2, 3)]

列表的拼接 “+” 会创建出一个新的对象并进行拷贝,如果构造了一个数据量大的列表,再对其进行copy操作,将会非常影响性能,此时建议使用extend进行扩展组合操作;

1
2
3
4
5
6
7
everything = []
for chunk in list_of_lists:
everything.extend(chunk)
# 上面这个操作肯定比下面这个要更快
everything = []
for chunk in list_of_lists:
everything = everything + chunk

2.4 排序

  • 排序
1
2
3
4
5
6
a = [7, 2, 5, 1, 3]
a.sort()
print(a)
b = ['saw', 'small', 'He', 'foxes', 'six']
b.sort(key=len) # 按元素长度排序
print(b)

输出结果:

1
2
[1, 2, 3, 5, 7]
['He', 'saw', 'six', 'small', 'foxes']

注意:列表排序并不需要重新创建对象,直接在原有对象上进行操作(in-place);

  • 二分查找与有序列表维护

Python内置 bisect模块实现了有序列表的二分查找和插入,看下以下几个操作:

1
2
3
4
5
6
import bisect
c = [1, 2, 2, 2, 3, 4, 7]
print(bisect.bisect(c, 2)) # the same of bisect_right 获取'2'这个元素在列表中即将插入的index
print(bisect.bisect(c, 5))
bisect.insort(c, 6) # the same of insort_right
print(c)

注意:

  1. bisect 方法仅获取了index,并没有对元素进行插入操作;
  2. bisect模块并不会对list进行判断是否有序,所以,使用前需要进行排序,否则可能出现操作返回错误的结果;

以上代码执行结果:

1
2
3
4   # 优先靠右,所以获取到的index为4
6
[1, 2, 2, 2, 3, 4, 6, 7]

2.5 切片

1
2
3
4
5
6
7
8
9
10
seq = [7, 2, 3, 7, 5, 6, 0, 1]
print(seq[1:5]) # not included seq[5]
seq[3:4] = [6, 3] # 替换 index 为 3,4 的两个元素为 6,3
print(seq)
print(seq[:5]) # start不填,默认从0开始
print(seq[3:]) # start不填,默认到末尾
print(seq[-4:]) # 倒数第4个开始,到末尾
print(seq[-6:-2])
print(seq[::2]) # 正序,步长为2
print(seq[::-1]) # 倒序,步长为1,the same of seq.reverse()

[start:stop:step],左闭右开,包含start开始的元素,不包含stop的元素

1
2
3
4
5
6
7
8
[2, 3, 7, 5] 	
[7, 2, 3, 6, 3, 5, 6, 0, 1]
[7, 2, 3, 6, 3]
[6, 3, 5, 6, 0, 1]
[5, 6, 0, 1]
[6, 3, 5, 6]
[7, 3, 3, 6, 1]
[1, 0, 6, 5, 3, 6, 3, 2, 7]

内置序列函数(Built-in Sequence Functions)

Python包含三种主要的序列类型:list, tuple, range;还有其他的序列类型,包含二进制数据(binary data)文本字符串(text strings)

3.1 enumerate

按照以往循环一个列表可能的操作是下面这样:

1
2
3
4
i = 0
for value in collection:
do something with value
i += 1

Python内置了枚举操作,并返回对应的index和value:

1
2
3
4
5
some_list = ['foo', 'bar', 'baz']
mapping = {}
for i, v in enumerate(some_list):
mapping[v] = i
print(mapping) # {'bar': 1, 'baz': 2, 'foo': 0}

3.2 sorted

1
2
print(sorted([7, 1, 2, 6, 0, 3, 2]))
print(sorted('horse race'))

数字默认从大到小排序,字符串默认字母顺序排序

1
2
[0, 1, 2, 2, 3, 6, 7]
[' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's']

3.3 zip

zip将两组或者多组可迭代的对象打包成一对对的元组

1
2
3
4
5
6
7
seq1 = ['foo', 'bar', 'baz']
seq2 = ['one', 'two', 'three']
zipped = zip(seq1, seq2)
list(zipped) # [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]
seq3 = [False, True]
# 打包出来的大小取决于最小的序列大小,即为2
list(zip(seq1, seq2, seq3)) # [('foo', 'one', False), ('bar', 'two', True)]

两种常用的zip方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 与enumerate结合
for i, (a, b) in enumerate(zip(seq1, seq2)):
print('{0}: {1}, {2}'.format(i, a, b))
# 0: foo, one
# 1: bar, two
# 2: baz, three

# 解包(unzip)的另一种操作,也可进行数据的行列相互转换
pitchers = [('Nolan', 'Ryan'), ('Roger', 'Clemens'),
('Schilling', 'Curt')]
first_names, last_names = zip(*pitchers)
first_names #('Nolan', 'Roger', 'Schilling')
last_names #('Ryan', 'Clemens', 'Curt')

3.4 reversed

1
list(reversed(range(10)))  # 序列翻转

注意:reversed是个生成表达式(generator),需要搭配list或者for循环使用,reversed()本身返回的是一个迭代器对象。

下一篇文章会介绍一下其他的内置数据结构,包含字典(dict)、集合(set),以及他们和list的对比;才疏学浅,如有错误,还望联系并指出。