本章我们将学习如何使用Python的组合数据类型将数据项集合在一起,以便在程序设计时有更多的选项。
组合数据类型
Python提供了5中内置的序列类型:bytearray、bytes、list、str与tuple,序列类型支持成员关系操作符(in)、大小计算函数(len())、分片([]),并且是可可迭代的。
元组是个有序序列,包含0个或多个对象引用,使用小括号包裹。元组是固定的,不能替换或删除其中包含的任意数据项。
使用()创建一个元组:
也可以使用tuple()创建一个元组:
语法 | 描述 |
---|---|
tup[1] | 读取第二个元素 |
tup[-2] | 反向读取;读取倒数第二个元素 |
tup[1:] | 截取元素 |
tup = ('first', 5, 'white', 'dog')
print(tup[1])
print(tup[-2])
print(tup[1:])
[out]
5
white
(5, 'white', 'dog')
元组只提供两种方法:
语法 | 描述 |
---|---|
t.count(x) | 返回对象x在元祖t中出现的次数 |
t.index(x) | 返回对象x在元组t中出现的最左边位置 |
tup = ('1', 'first', '1', '1', '2')
print('count of "1":',tup.count('1'))
print('index of "2":',tup.index('2'))
[out]
count of "1": 3
index of "2": 4
与字符串一样,元组之间可以使用 + 号和 * 号进行运算。这就意味着他们可以组合和复制,运算后会生成一个新的元组。
语法 | 描述 |
---|---|
len(t) | 返回元组t中元素个数 |
+ | 连接 |
* | 复制 |
in | 元素是否存 |
for … in …: | 迭代 |
比较运算符<、<=、>、>=、==、!= | 逐项进行比较 |
元组中的元素值是不允许删除的,但我们可以使用del删除整个元组:
tup = ('python', 'hello', 1997, 2000);
print(tup)
del tup
print("After deleting tup : ")
print(tup)
[out]
('python', 'hello', 1997, 2000)
After deleting tup :
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-1-a12bbf13863f> in <module>()
4 del tup
5 print("After deleting tup : ")
----> 6 print(tup)
NameError: name 'tup' is not defined
当元组出现在二进制操作符的左边或出现在unary语句的右边时,可以不使用圆括号。
a,b = (1,2) # left of binary operator
for x,y in ((1,2),(3,4),(5,6)): # left of binary operator
print(x,y)
del a,b # right of unary statement
def f(x):
return x,x**2 # right of unary statement
命名的元组(namedtuple)与普通元组一样,有相同的表现特征,其添加的功能就是可以根据名称引用元组中的项。
collections模块提供了namedtuple()函数,用于创建自定义的元组数据类型。该函数的第一个参数是想要创建的自定义元组数据类型的名称,第二个参数是一个字符串,其中包含使用空格分隔的名称,每个名称代表该元祖数据类型中的一项。该函数返回一个自定义的类,可用于创建命名的元组。
import collections
Sale = collections.namedtuple('Sale', 'productid customerid data quantity price')
sales = list()
sales.append(Sale(432,921,"2018-04-01",3,8.2))
sales.append(Sale(543,879,"2018-03-31",6,8.1))
print(sales)
[out]
[Sale(productid=432, customerid=921, data='2018-04-01', quantity=3, price=8.2), Sale(productid=543, customerid=879, data='2018-03-31', quantity=6, price=8.1)]
这里我们创建了包含两个Sale项的列表,我们可以使用索引位置来引用元组中的项,也可以使用名称进行引用,后者正式命名的元组的特点:
total = 0
for sale in sales:
total += sale.quantity * sale.price
print('Total ¥{0:.2F}'.format(total))
[out]
Total ¥73.20
列表是包含0个或多个对象引用的有序序列,支持与字符串以及元组一样的分片与步距语法,列表是可变的,因此我们可以对列表中的项进行删除或替换,插入、替换或删除列表中的分片也是可能的。
使用[]创建一个元组:
也可以使用list()创建一个列表:
语法 | 描述 |
---|---|
lst[1] | 读取第二个元素 |
lst[-2] | 反向读取;读取倒数第二个元素 |
lst[1:] | 截取元素 |
lst = ['first', 5, 'white', 'dog']
print(lst[1])
print(lst[-2])
print(lst[1:])
[out]
5
white
[5, 'white', 'dog']
下表中,L为列表。
语法 | 描述 |
---|---|
L.append(x) | 将数据项x追加到L的末尾 |
L.count(x) | 统计元素x在L中出现的次数 |
L.extend(m)L += m | 将iterable m的项追加到L的末尾 |
L.index(x, start, end) | 返回数据项x在L中(或L的start: end分片中)最左边出现的索引位置,如果没找到x,则产生ValueError异常 |
L.insert(i, x) | 在索引位置i处插入元素x |
L.pop() | 移除L最右边的数据项,并返回该元素的值 |
L.pop(i) | 移除L索引位置i处的数据项,并返回该元素的值 |
L.remove(x) | 从L中移除最左边的数据项x,如果没找到x产生ValueError异常 |
L.reverse() | 对L进行反转 |
L.sort(…) | 对L进行排序,与内置的sorted()函数一样,可以接受可选的key与reverse参数 |
L = [5, 'python', (1,2), 5, 'today']
L.append(9)
print('列表追加项:', L)
print('列表中5出现的次数:', L.count(5))
L.extend('hello')
print('追加迭代器中的项:',L)
print('"python"最左边索引值:', L.index('python'))
L.insert(1, 'insert')
print('在索引位置1处插入:', L)
pop_item = L.pop()
print('L末尾数据项:', pop_item)
print('移除末尾数据项后的结果:', L)
L.remove((1,2))
print('移除(1,2)后的列表:', L)
L.reverse()
print('反转后的列表:', L)
[out]
列表追加项: [5, 'python', (1, 2), 5, 'today', 9]
列表中5出现的次数: 2
追加迭代器中的项: [5, 'python', (1, 2), 5, 'today', 9, 'h', 'e', 'l', 'l', 'o']
"python"最左边索引值: 1
在索引位置1处插入: [5, 'insert', 'python', (1, 2), 5, 'today', 9, 'h', 'e', 'l', 'l', 'o']
L末尾数据项: o
移除末尾数据项后的结果: [5, 'insert', 'python', (1, 2), 5, 'today', 9, 'h', 'e', 'l', 'l']
移除(1,2)后的列表: [5, 'insert', 'python', 5, 'today', 9, 'h', 'e', 'l', 'l']
反转后的列表: ['l', 'l', 'e', 'h', 9, 'today', 5, 'python', 'insert', 5]
任意可迭代的(列表、元组等)数据类型都可以使用序列拆分操作符进行拆分,即*。用于赋值操作符左边的两个或多个变量时,其中一个使用*进行引导,数据项将赋值给该变量,而所有剩下的数据项将给带星号的变量。
first, *rest, last = [1, 2, 3, 4, 5]
print(first, rest, last)
[out]
1 [2, 3, 4] 5
由于列表是可变的,我们可以对其数据项进行删除。
列表内涵是一个表达式,也是一个循环,该循环有一个可选的、包含在方括号中的条件,作用是为列表生成数据项,并且可以使用条件过滤掉不需要的数据项,可以使用表达式,也可以使用附加条件。常见语法:
在没有列表内涵时,我们找出1900~1940年之间所有的闰年,可能会这么写:
# 普通方法找1900~1940年之间的闰年
leaps = list()
for year in range(1900, 1940):
if (year%4 == 0 and year%100 != 0) or (year%400 == 0):
leaps.append(year)
print(leaps)
[out]
[1904, 1908, 1912, 1916, 1920, 1924, 1928, 1932, 1936]
学习了列表内涵之后我们可以简化程序:
# 列表内涵找1900~1940年之间的闰年
leaps = [year for year in range(1900, 1940) if (year%4 == 0 and year%100 != 0) or (year%400 == 0)]
print(leaps)
[out]
[1904, 1908, 1912, 1916, 1920, 1924, 1928, 1932, 1936]
两种方法等效,得到同样的结果。
set也是一种组合数据类型,支持成员关系操作符(in)、对象大小计算操作符(len()),并且也是iterable。Python提供了两种内置的集合类型:可变的set类型,固定的frozenset类型。进行迭代时,集合类型以任意顺序提供其数据项。
只有可哈希运算的对象可以添加到集合中。所有的内置固定数据类型(比如float、frozenset、int、str、tuple)都是可哈希运算的,可以添加到集合中。内置的可变数据类型(比如dict、list、set)都不是可哈希运算的,不能添加到集合中。
集合是0个或多个对象引用的无序组合。集合是可变的,因此可以很容易的添加和移除数据项,但是由于其中的项是无序的,因此没有索引位置的概念,也不能分片或按步距分片。
使用set()创建一个集合:
集合中包含的每个数据项都是独一无二的——添加重复的数据项固然不会引发问题,但是也毫无意义。比如,下面产生的三个集合是一样的:set('apple')、set('aple')、{‘e', 'p', 'l', 'a'}。鉴于此,集合常用于删除重复的数据项。比如,x是一个字符串列表,在执行x=list(set(x))
之后,x中的每个字符串都是独一无二的,存放顺序是任意的。
s、t为集合,x为数据项。
语法 | 描述 |
---|---|
s.add(x) | 将x添加到s中——如果s中尚未包含x |
s.clear() | 清空s |
s.copy() | 返回s的浅拷贝 |
s.difference(t)s-t | 返回一个新集合,其中包含在s中但不在t中的所有数据项 |
s.difference_update(t)s-=t | 移除每一个在t中但不在s中的项 |
s.discard(x) | 如果x在s中,则移除x |
s.intersection(t)s&t | 返回一个新集合,其中包含所有同时包含在s和t中的数据项 |
s.intersection_update(t)s&=t | 使得s包含自身与t的交集 |
s.isdisjoint(t) | 如果s与t没有相同的项,返回True |
s.issubset(t)s<=t | 如果s与t相同,或s是t的子集,返回True;使用s<t可以测试s是否是t的真子集 |
s.issupset(t)s>=t | 如果s与t相同,或s是t的超集,返回True |
s.pop() | 返回并移除s中的一个随机项,如果s为空,就产生一个KeyError |
s.remove(x) | 从s中移除x,如果s中不包含x,就产生KeyError |
s.symmetric_difference(t)s^t | 返回一个新集合,其中包含s与t中的每个数据项,但不包含同时在这两个集合中的数据项 |
s.symmetric_difference_update(t)s^=t | 使得s只包含其自身与t的对称差 |
s.union(t)s|t | 返回一个新集合,其中包含集合s中的所有数据项以及在t中而不在s中的数据项 |
s.update(t)s|=t | 将t中每个s中不包含的数据项添加到集合s中 |
除了调用set()创建集合,或使用集合字面值创建集合外,我们可以使用集合内涵创建集合。集合内涵是一个表达式,也是一个带有可选条件的循环,支持的语法:
固定集合是指那种一旦创建就不能修改的集合,只能使用frozenset数据类型函数创建,不带参数调用时,frozenset()返回一个空的固定集合,带一个frozenset参数时,将返回改参数的 浅拷贝,对于任何其他类型的参数,都尝试将给定的对象转换为一个forzenset。
映射是键-值数据项的组合,并提供了存取数据项及其键、值的方法。Python3.0支持两种无序的映射类型——内置的dict类型以及标准库中的collections.defaultdict类型。Python3.1引入了一种新的、有序的映射类型collections.OrderedDict,该类型是一个字典,与内置的dict有相同的方法和属性,但在存储数据项时以插入顺序进行。
dict是一种无序的组合数据类型,其中包含0个或多个键-值对。
可以使用{}创建:
也可以使用dict()函数创建:
字典的键值是独一无二的,因此,如果向字典中添加一个已存在的键值项,实际效果是新值替换旧值。
d为字典
语法 | 描述 |
---|---|
d.clear() | 移除d中所有项 |
d.copy() | 返回d的浅拷贝 |
d.fromkeys(s, v) | 返回一个dict,该字典的键为序列s中的项,值为None或V |
d.get(k) | 返回键k关联的值,如果d中不存在k则返回None |
d.get(k, v) | 返回键k关联的值,如果d中不存在k则返回v |
d.items() | 返回d中所有(key, value)对的视图 |
d.keys() | 返回d中所有键的视图 |
d.pop(k) | 返回键k的关联值,并移除键为k的项,如果k不包含在d中就产生KeyError |
d.pop(k, v) | 返回键k的关联值,并移除键为k的项,如果k不包含在d中就返回v |
d.popitem() | 返回并移除d中任意一个(key, value)对,如果d为空就产生KeyError |
d.setdefault(k, v) | 与d.get()方法一样,不同之处在于,如果k没有包含在d中就插入一个键为k的新项,其值为None或v |
d.update(a) | 将a中每一个尚未包含在d中的(key, value)对添加到d中,对同时包含在d与a中的每个键,使用a中对应的值替换d中对应的值——a可以是字典,也可以是(key, value)对的一个iterable或关键字参数 |
d.values() | 返回d的所有值的视图 |
上面提到了“视图”概念,其相对于通常的iterables有两个不同点:
注:两种通过键取值方式的比较
我们可以通过d[k] 和 d.get()两种形式来取值,比如我们进行词频统计时,使用words[word]+=1或words[word] = words.get(word, 0) + 1 都可以进行加1操作,但是如果单词第一次出现,第一种形式会产生KeyValue错误,第二种则会正确运行。
字典内涵是一个表达式,也是一个循环,该循环带有一个可选条件。语法:
例:
# 使用字典内涵创建字典,其中每个键是当前目录中文件的文件名,值则为以字节计数的文件夹大小
import os
file_sizes = {name: os.path.getsize(name) for name in os.listdir('.')}
print(file_sizes)
[out]
{'.ipynb_checkpoints': 0, '第三章组合数据类型.ipynb': 12387}
默认字典也是一种字典——这种字典包含普通字典所提供的所有操作符与方法,与其不同的是可以对遗失的键进行处理。
创建默认字典时,我们可以传入一个工厂函数,这样就会为遗失的键创建默认值。看下面例子
import collections
words = collections.defaultdict(int)
x = words['a']
print(x)
[out]
0
上面我们创建的默认字典words永远不会产生KeyError异常,如果遇到没有的键,其值通过工厂函数(int())设置为0。
有序字典collections.OrderedDict是以数据项的插入顺序进行存储。
import collections
d = collections.OrderedDict([('first', 1), ('second', 2), ('third', 3)])
print(d.keys())
[out]
odict_keys(['first', 'second', 'third'])
可以看出我们通过二元组列表创建有序字典后,获取去键视图也为有序的。
有序字典另一种稍专业一些的用途是生成排序字典。给定一个字典d,可以按如下方式转换为排序字典:d=collections.OrderedDict(sorted(d.items()))
。
iterable数据类型每次返回其中的一个数据项。任意包含__iter__() 方法的对象或任意序列(也即包含__getitem__()方法的对象)都是一个iterable,并可以提供一个迭代子。迭代子是一个对象,该对象可以提供__next__()方法,该方法依次返回每个相继的数据项,并在没有数据项时产生StopIteration异常。
常见的迭代操作符与函数(s与t为序列):
语法 | 描述 |
---|---|
s+t | 返回一个序列,该序列是s与t的连接 |
s*n | 返回一个序列,该序列是s的n个副本的连接 |
x in i | 如果x出现在iterable i中,返回True |
all(i) | 如果iterable i中的每一项都评估为True,就返回True |
any(i) | 如果iterable i中的任意项评估为True,就返回True |
emumerate(i, start) | 通常用于for… in 循环中,提供一个(index, item)元组序列,其中索引其实值为0或start |
len(x) | 返回x的“长度” |
max(i, key) | 返回iterable i中的最大的项,如果给定的是key函数,就返回key(item)值的最大项 |
min(i, key) | 返回iterable i中的最小的项,如果给定的是key函数,就返回key(item)值的最小项 |
range(start, stop, step) | 返回一个整数迭代子,使用一个参数(stop)时,迭代子的取值范围从0到stop-1;使用两个参数(start与stop)时,迭代子取值范围从start到stop-1;使用三个参数时,迭代子取值范围从start到stop-1,每两个值之间间隔step |
reversed(i) | 返回一个迭代子,该迭代子以反序从迭代子i中的返回项 |
sorted(i, key, reverse) | 以排序后顺序从迭代子i返回项,key用于提供DSU(修饰、排序、反修饰)排序,如果reverse为True,则排序以反序进行 |
sum(i, start) | 返回iterable i中项的和,加上start(默认为0),i可以包含字符串 |
zip(i1, …, iN) | 返回元组的迭代子,使用迭代子i1到iN |
数据项返回的顺序依赖于底层的iterable。对列表和元组等情况,数据项的返回值通常从第一个数据项开始依次返回,而对于字典与集合,迭代子是任意顺序的返回项。
由于数据片总是曲子某个数据项的一个单独副本,所以获取一个列表的副本可以通过下面方式:
lst = ['apple', 'dog']
copy_of_lst = lst[:]
print(copy_of_lst)
[out]
['apple', 'dog']
对于字典和集合,可以使用dict.copy()和set.copy()来实现。此外,copy模块还提供了copy.copy() 函数,该函数返回给定对象的一个副本。
在以上各种组合数据类型创建的时候,提到可以使用工厂方法来创建一个组合数据类型的副本:
# 工厂方法创建副本
d = {'first':'hello', 'second':'world'}
L = ['hello', 'world']
s = {'hello', 'world'}
copy_of_dict = dict(d)
copy_of_list = list(L)
copy_of_set = set(s)
注意:以上的复制都是浅拷贝,也就是说,复制的只是对象引用而非对象本身。对于固定数据类型(数字、字符串等),这与复制的效果是相同的,但对于可变的数据类型,比如嵌套的组合类型,这意味着相关对象同时被原来的组合与复制得来的组合引用。请看下面代码:
# 浅拷贝实例
x = [12, 34, ['hello', 'world']]
y = x[:]
print(x, y)
y[0] = 56
y[2][1] = 'boy'
print(x, y)
[out]
[12, 34, ['hello', 'world']] [12, 34, ['hello', 'world']]
[12, 34, ['hello', 'boy']] [56, 34, ['hello', 'boy']]
从输出结果可以看出,前两项固定数据类型并没有同时改变,而列表中的列表同时变化,说明x与y的第三项都指向的同一列表的引用。我们可以使用深拷贝来避免此类问题:
import copy
# 浅拷贝实例
x = [12, 34, ['hello', 'world']]
y = copy.deepcopy(x)
print(x, y)
y[0] = 56
y[2][1] = 'boy'
print(x, y)
[out]
[12, 34, ['hello', 'world']] [12, 34, ['hello', 'world']]
[12, 34, ['hello', 'world']] [56, 34, ['hello', 'boy']]
从输出结果看,x与y已经完全独立了。