目录
1 元组
元组是一种固定长度、不可变的Python对象序列。创建元组最简单的办法就是用逗号分隔序列值。
>>> tup = 4, 5, 6
>>> tup
(4, 5, 6)
当你通过更复杂的表达式来定义元组时,通常需要用括号将值包起来
>>> nested_tup = (4, 5, 6), (7, 8)
>>> nested_tup
((4, 5, 6), (7, 8))
使用tuple函数将任意序列或迭代器转换为元组
>>> tuple([4, 0, 2])
(4, 0, 2)
>>> tup = tuple('string')
>>> tup
('s', 't', 'r', 'i', 'n', 'g')
元组的元素可以通过中括号[]来获取,Python中的序列索引是从0开始的:
>>> tup[0]
's'
虽然对象元组中存储的对象其自身是可变的,但是元组一旦创建,各个位置上的对象是无法被修改的
>>> tup = tuple(['foo', [1, 2], True])
>>> tup[2] = False
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-10-c7308343b841> in <module>()
----> 1 tup[2] = False
TypeError: 'tuple' object does not support item assignment
如果元组中的一个对象是可变的,例如列表,你可以在它内部进行修改:
>>> tup[1].append(3)
>>> tup
('foo', [1, 2, 3], True)
可以使用+号连接元组来生成更长的元组
>>> (4, None, 'foo') + (6, 0) + ('bar',)
(4, None, 'foo', 6, 0, 'bar')
将元组乘以整数,则会和列表一样,生成含有多份拷贝的元组:
>>> ('foo', 'bar') * 4
('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar')
对象自身并没有复制,只是指向它们的引用进行了复制。
1.1 元组拆包
如果你想要将元组型的表达式赋值给变量,Python会对等号右边的值进行拆包:
>>> tup = (4, 5, 6)
>>> a, b, c = tup
>>> b
5
嵌套元组也可以拆包
>>> tup = 4, 5, (6, 7)
>>> a, b, (c, d) = tup
>>> d
7
交换变量名
>>> a, b = 1, 2
>>> a
1
>>> b
2
>>> b, a = a, b
>>> a
2
>>> b
1
拆包的一个常用场景就是遍历元组或列表组成的序列
>>> 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))
a=1, b=2, c=3
a=4, b=5, c=6
a=7, b=8, c=9
Python语言新增了一些更为高级的元组拆包功能,用于帮助你从元组的起始位置“采集“一些元素。这个功能使用特殊的语法*rest,用于在函数调用时获取任意长度的位置参数列表:
>>> values = 1, 2, 3, 4, 5
>>> a, b, *rest = values
>>> a, b
(1, 2)
>>> rest
[3, 4, 5]
rest部分有时是你想要丢弃的数据,rest这个变量名并没有什么特殊之处,为了方便,很多Python编程者会使用下划线(_)来表示不想要的变量:
>>> a, b, *_ = values
1.2 元组方法
由于元组的内容和长度是无法改变的,它的实例方法很少。一个常见的有用方法是count(列表中也可用),用于计量某个数值在元组中出现的次数:
>>> a = (1, 2, 2, 2, 3, 4, 2)
>>> a.count(2)
4
2 列表
列表的长度是可变的,它所包含的内容也是可以修改的。你可以使用中括号[]或者list类型函数来定义列表:
>>> a_list = [2, 3, 7, None]
>>> tup = ('foo', 'bar', 'baz')
>>> b_list = list(tup)
>>> b_list
['foo', 'bar', 'baz']
>>> b_list[1] = 'peekaboo'
>>> b_list
['foo', 'peekaboo', 'baz']
list函数在数据处理中常用于将迭代器或者生成器转化为列表
>>> gen = range(10)
>>> gen
range(0, 10)
>>> list(gen)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2.1 增加和移除元素
使用append方法可以将元素添加到列表的尾部
>>> b_list.append('dwarf')
>>> b_list
['foo', 'peekaboo', 'baz', 'dwarf']
使用insert方法可以将元素插入到指定的列表位置
>>> b_list.insert(1, 'red')
>>> b_list
['foo', 'red', 'peekaboo', 'baz', 'dwarf']
插入位置的范围在0到列表长度之间。
insert与append相比,计算代价更高。因为子序列元素不得不在内部移动为新元素提供空间。
insert的反操作是pop,该操作会将特定位置的元素移除并返回
>>> b_list.pop(2)
'peekaboo'
>>> b_list
['foo', 'red', 'baz', 'dwarf']
元素可以通过remove方法移除,该方法会定位第一个符合要求的值并移除它:
>>> b_list.append('foo')
>>> b_list
['foo', 'red', 'baz', 'dwarf', 'foo']
>>> b_list.remove('foo')
>>> b_list
['red', 'baz', 'dwarf', 'foo']
使用in关键字可以检查一个值是否在列表中
>>> 'dwarf' in b_list
True
not关键字可以用作in的反义词,表示“不在”
>>> 'dwarf' not in b_list
False
与字典、集合(后面会介绍)相比,检查列表中是否包含一个值是非常缓慢的。这是因为Python在列表中进行了线性逐个扫描,而在字典和集合中Python是同时检查所有元素的(基于哈希表)。
2.2 连接和联合列表
与元组类似,两个列表可以使用+号连接
>>> [4, None, 'foo'] + [7, 8, (2, 3)]
[4, None, 'foo', 7, 8, (2, 3)]
如果你有一个已经定义的列表,你可以用extend方法向该列表添加多个元素:
>>> x = [4, None, 'foo']
>>> x.extend([7, 8, (2, 3)])
>>> x
[4, None, 'foo', 7, 8, (2, 3)]
请注意通过添加内容来连接列表是一种相对高代价的操作,这是因为连接过程中创建了新列表,并且还要复制对象。使用extend将元素添加到已经存在的列表是更好的方式,尤其是在你需要构建一个大型列表时:
>>> everything = []
>>> for chunk in list_of_lists:
>>> everything.extend(chunk)
# 实现速度更快
>>> everything = []
>>> for chunk in list_of_lists:
>>> everything.extend(chunk)
2.3 排序
可以调用列表的sort方法对列表进行内部排序(无须新建一个对象)
>>> a = [7, 2, 5, 1, 3]
>>> a.sort()
>>> a
[1, 2, 3, 5, 7]
sort有一些选项偶尔会派上用场。其中一项是传递一个二级排序key——一个用于生成排序值的函数。例如,我们可以通过字符串的长度进行排序:
>>> b = ['saw', 'small', 'He', 'foxes', 'six']
>>> b.sort(key=len)
>>> b
['He', 'saw', 'six', 'small', 'foxes']
2.4 二分搜索和已排序列表的维护
内建的bisect模块实现了二分搜索和已排序列表的插值。bisect.bisect会找到元素应当被插入的位置,并保持序列排序,而bisect.insort将元素插入到相应位置:
>>> import bisect
>>> c = [1, 2, 2, 2, 3, 4, 7]
>>> bisect.bisect(c, 2)
4
>>> bisect.bisect(c, 5)
6
>>> bisect.insort(c, 6)
>>> c
[1, 2, 2, 2, 3, 4, 6, 7]
bisect模块的函数并不会检查列表是否已经排序,因为这么做代价太大。因此,对未排序列表使用bisect的函数虽然不会报错,但可能会导致不正确的结果。
2.5 切片
使用切片符号可以对大多数序列类型选取其子集,它的基本形式是将start:stop传入到索引符号[]中:
>>> seq = [7, 2, 3, 7, 5, 6, 0, 1]
>>> seq[1:5]
[2, 3, 7, 5]
切片还可以将序列赋值给变量
>>> seq[3:4] = [6, 3]
>>> seq
[7, 2, 3, 6, 3, 5, 6, 0, 1]
由于起始位置start的索引是包含的,而结束位置stop的索引并不包含,因此元素的数量是stop-start。
start和stop是可以省略的,如果省略的话会默认传入序列的起始位置或结束位置:
>>> seq[:5]
[7, 2, 3, 6, 3]
>>> seq[3:]
[6, 3, 5, 6, 0, 1]
负索引可以从序列的尾部进行索引:
>>> seq[-4:]
[5, 6, 0, 1]
>>> seq[-6:-2]
[6, 3, 5, 6]
步进值step可以在第二个冒号后面使用,意思是每隔多少个数取一个值
>>> seq[::2]
[7, 3, 3, 6, 1]
当需要对列表或元组进行翻转时,一种很聪明的用法就是向步进传值-1
>>> seq[::-1]
[1, 0, 6, 5, 3, 6, 3, 2, 7]
3 内建序列函数
3.1 enumerate
在遍历一个序列的同时追踪当前元素的索引
i = 0
for value in collection:
# 使用值做点事
i += 1
由于这种场景很常见,所以Python内建了enumerate函数,返回了(i,value)元组的序列,其中value是元素的值,i是元素的索引:
for i, value in enumerate(collection):
# 使用值做点事
当你需要对数据建立索引时,一种有效的模式就是使用enumerate构造一个字典,将序列值(假设是唯一的)映射到索引位置上:
>>> some_list = ['foo', 'bar', 'baz']
>>> mapping = {}
>>> for i, v in enumerate(some_list):
>>> mapping[v] = i
>>> mapping
{'bar': 1, 'baz': 2, 'foo': 0}
3.2 sorted
sorted函数返回一个根据任意序列中的元素新建的已排序列表
>>> sorted([7, 1, 2, 6, 0, 3, 2])
[0, 1, 2, 2, 3, 6, 7]
>>> sorted('horse race')
[' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's']
sorted函数接受的参数与列表的sort方法一致。
3.3 zip
zip将列表、元组或其他序列的元素配对,新建一个元组构成的列表
>>> seq1 = ['foo', 'bar', 'baz']
>>> seq2 = ['one', 'two', 'three']
>>> zipped = zip(seq1, seq2)
>>> list(zipped)
[('foo', 'one'), ('bar', 'two'), ('baz', 'three')]
zip可以处理任意长度的序列,它生成列表长度由最短的序列决定:
>>> seq3 = [False, True]
>>> list(zip(seq1, seq2, seq3))
[('foo', 'one', False), ('bar', 'two', True)]
zip的常用场景为同时遍历多个序列,有时候会和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
给定一个已“配对”的序列时,zip函数有一种机智的方式去“拆分”序列。这种方式的另一种思路就是将行的列表转换为列的列表。语法看上去略显魔幻:
>>> 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
reversed函数将序列的元素倒序排列:
>>> list(reversed(range(10)))
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
reversed是一个生成器,因此如果没有实例化(例如使用list函数或进行for循环)的时候,它并不会产生一个倒序的列表。
4 字典
dict(字典)可能是Python内建数据结构中最重要的。它更为常用的名字是哈希表或者是关联数组。字典是拥有灵活尺寸的键值对集合,其中键和值都是Python对象。用大括号{}是创建字典的一种方式,在字典中用逗号将键值对分隔:
>>> empty_dict = {}
>>> d1 = {'a' : 'some value', 'b' : [1, 2, 3, 4]}
>>> d1
O{'a': 'some value', 'b': [1, 2, 3, 4]}
你可以访问、插入或设置字典中的元素,就像访问列表和元组中的元素一样:
>>> d1[7] = 'an integer'
>>> d1
{'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}
>>> d1['b']
[1, 2, 3, 4]
你可以用检查列表或元组中是否含有一个元素的相同语法来检查字典是否含有一个键:
>>> 'b' in d1
True
你可以使用del关键字或pop方法删除值,pop方法会在删除的同时返回被删的值,并删除键:
>>> d1[5] = 'some value'
>>> d1
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
5: 'some value'}
>>> d1['dummy'] = 'another value'
>>> d1
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
5: 'some value',
'dummy': 'another value'}
>>> del d1[5]
>>> d1
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
'dummy': 'another value'}
>>> ret = d1.pop('dummy')
>>> ret
'another value'
>>> d1
{'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}
keys方法和values方法会分别为你提供字典键、值的迭代器。然而键值对并没有特定的顺序,这些函数输出的键、值都是按照相同的顺序:
>>> list(d1.keys())
['a', 'b', 7]
>>> list(d1.values())
['some value', [1, 2, 3, 4], 'an integer']
使用update方法将两个字典合并
>>> d1.update({'b' : 'foo', 'c' : 12})
>>> d1
{'a': 'some value', 'b': 'foo', 7: 'an integer', 'c': 12}
update方法改变了字典中元素位置,因此对于任何原字典中已经存在的键,如果传给update方法的数据也含有相同的键,则它的值将会被覆盖。
4.1 从序列生成字典
mapping = {}
for key, value in zip(key_list, value_list):
mapping[key] = value
由于字典本质上是2-元组(含有2个元素的元组)的集合,字典是可以接受一个2-元组的列表作为参数的:
>>> mapping = dict(zip(range(5), reversed(range(5))))
>>> mapping
{0: 4, 1: 3, 2: 2, 3: 1, 4: 0}
4.2 默认值
代码逻辑
if key in some_dict:
value = some_dict[key]
else:
value = default_value
字典的get方法和pop方法可以返回一个默认值
value = some_dict.get(key, default_value)
带有默认值的get方法会在key参数不是字典的键时返回None,而pop会抛出异常。
# 将字词组成的列表根据首字母分类为包含列表的字典
>>> words = ['apple', 'bat', 'bar', 'atom', 'book']
>>> by_letter = {}
>>> for word in words:
>>> letter = word[0]
>>> if letter not in by_letter:
>>> by_letter[letter] = [word]
>>> else:
>>> by_letter[letter].append(word)
>>>
>>> by_letter
{'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}
字典的setdefault方法就是为了这个目的而产生的。
>>> for word in words:
>>> letter = word[0]
>>> by_letter.setdefault(letter, []).append(word)
这个类使得上述目的实现更为简单。想要生成符合要求的字典,你可以向字典中传入类型或能在各位置生成默认值的函数:
from collections import defaultdict
by_letter = defaultdict(list)
for word in words:
by_letter[word[0]].append(word)
4.3 有效的字典键类型
尽管字典的值可以是任何Python对象,但键必须是不可变的对象,比如标量类型(整数、浮点数、字符串)或元组(且元组内对象也必须是不可变对象)。这里要使用到一个术语叫作哈希化,通过hash函数可以检查一个对象是否可以哈希化(即是否可以用作字典的键):
>>> hash('string')
5023931463650008331
>>> hash((1, 2, (2, 3)))
O1097636502276347782
>>> hash((1, 2, [2, 3])) # 会因为列表是可变的失败
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-129-800cd14ba8be> in <module>()
----> 1 hash((1, 2, [2, 3])) # 会因为列表是可变的失败
TypeError: unhashable type: 'list'
为了将列表作为键,一种方式就是将其转换为元组,而元组只要它内部元素都可以哈希化,则它自己也可哈希化:
>>> d = {}
>>> d[tuple([1, 2, 3])] = 5
>>> d
{(1, 2, 3): 5}
5 集合
集合是一种无序且元素唯一的容器。你可以认为集合也像字典,但是只有键没有值。集合可以有两种创建方式:通过set函数或者是用字面值集与大括号的语法:
>>> set([2, 2, 2, 1, 3, 3])
{1, 2, 3}
>>> {2, 2, 2, 1, 3, 3}
{1, 2, 3}
集合支持数学上的集合操作,例如联合、交集、差集、对称差集。
>>> a = {1, 2, 3, 4, 5}
>>> b = {3, 4, 5, 6, 7, 8}
两个集合的联合就是两个集合中不同元素的并集。可以通过union方法或|二元操作符完成:
>>> a.union(b)
{1, 2, 3, 4, 5, 6, 7, 8}
>>> a | b
{1, 2, 3, 4, 5, 6, 7, 8}
交集包含了两个集合中同时包含的元素。可以使用&操作符或intersection方法获得交集:
>>> a.intersection(b)
{3, 4, 5}
>>> a & b
{3, 4, 5}
Python集合操作
所有的逻辑集合运算都有对应操作,允许你用操作的结果替代操作左边的集合内容。对于大型集合,下面的代码效率更高:
>>> c = a.copy()
>>> c |= b
>>> c
{1, 2, 3, 4, 5, 6, 7, 8}
>>> d = a.copy()
>>> d &= b
>>> d
{3, 4, 5}
和字典类似,集合的元素必须是不可变的。如果想要包含列表型的元素,必须先转换为元组:
>>> my_data = [1, 2, 3, 4]
>>> my_set = {tuple(my_data)}
>>> my_set
{(1, 2, 3, 4)}
你还可以检查一个集合是否是另一个结合的子集(包含于)或超集(包含):
>>> a_set = {1, 2, 3, 4, 5}
>>> {1, 2, 3}.issubset(a_set)
True
>>> a_set.issuperset({1, 2, 3})
True
当且仅当两个集合的内容一模一样时,两个集合才相等
>>> {1, 2, 3} == {3, 2, 1}
True
6 列表、集合和字典的推导式
列表推导式是最受欢迎的Python语言特性之一。它允许你过滤一个容器的元素,用一种简明的表达式转换传递给过滤器的元素,从而生成一个新的列表。列表推导式的基本形式为:
[expr for val in collection if condition]
这与下面的for循环是等价的:
result = []
for val in collection:
if condition:
result.append(expr)
过滤条件是可以忽略的,只保留表达式。
例如,给定一个字符串列表,我们可以过滤出长度大于2的,并且将字母改为大写:
>>> strings = ['a', 'as', 'bat', 'car', 'dove', 'python']
>>> [x.upper() for x in strings if len(x) > 2]
['BAT', 'CAR', 'DOVE', 'PYTHON']
集合与字典的推导式是列表推导式的自然拓展,用相似的方式生成集合与字典。字典推导式如下所示:
dict_comp = {key-expr : value-expr for value in collection
if condition}
集合推导式看起来很像列表推导式,只是中括号变成了大括号:
set_comp = {expr for value in collection if condition}
和列表推导式类似,集合、字典的推导式非常方便,它们使代码更易读易写。如果有一个字符串的列表,假设我们想要一个集合,集合里包含列表中字符串的长度,我可以通过集合推导式很方便地实现:
>>> unique_lengths = {len(x) for x in strings}
>>> unique_lengths
{1, 2, 3, 4, 6}
我们也可以使用map函数更函数化、更简洁地表达:
>>> set(map(len, strings))
{1, 2, 3, 4, 6}
# 创建一个将字符串与其位置相匹配的字典作为字典推导式
>>> loc_mapping = {val : index for index, val in enumerate(strings)}
>>> loc_mapping
{'a': 0, 'as': 1, 'bat': 2, 'car': 3, 'dove': 4, 'python': 5}
6.1 嵌套列表推导式
# 假设我们有一个包含列表的列表,内容是英语姓名和西班牙语姓名:
all_data = [['John', 'Emily', 'Michael', 'Mary', 'Steven'],
['Maria', 'Juan', 'Javier', 'Natalia', 'Pilar']]
# 假设我们想要获得一个列表包含所有含有2个以上字母e的名字,我们当然可以简单地使用for循环:
names_of_interest = []
for names in all_data:
enough_es = [name for name in names if name.count('e') >= 2]
names_of_interest.extend(enough_es)
可以将整个操作用一个嵌套列表推导式来完成
>>> result = [name for names in all_data for name in names
>>> if name.count('e') >= 2]
>>> result
['Steven']
嵌套列表推导式可能会让你有点晕头转向。列表推导式的for循环部分是根据嵌套的顺序排列的,所有的过滤条件像之前一样被放在尾部。下面的例子是将含有整数元组的列表扁平化为一个简单的整数列表:
>>> some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
>>> flattened = [x for tup in some_tuples for x in tup]
>>> flattened
[1, 2, 3, 4, 5, 6, 7, 8, 9]
for表达式的顺序应当和你写嵌套for循环来替代列表推导式的顺序一致:
flattened = []
for tup in some_tuples:
for x in tup:
flattened.append(x)
你当然可以嵌套多层的列表推导式,但超过两到三层之后你很可能开始疑惑这种做法是否会有利于代码可读性。嵌套推导式的语法要和列表推导式中的列表推导式区分开,列表推导式中的列表推导式也是非常有效的:
# 创建了一个包含列表的列表,而不是所有内部元素扁平排列
>>> [[x for x in tup] for tup in some_tuples]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]