第5 章 Python 容器操作 容器(container)是指用以容纳物料并以壳体为主的基本装置。这是一个宽泛的概念。 但是在Python领域中,许多人将其窄化了,认为只有list、tuple、dict和set才是容器。实际 上,并非如此,上述4个类型的对象仅仅是Python内置的对象容器。如图5.1所示,本章所 讨论的Python容器包括内存容器和外存容器。它们都是组织数据对象的容器对象,但存 放的物理容器不同:一个在内存,一个在外存。 图5.1 Python数据对象组织体系 内存容器由Python内置的容器类型和Python内置的字符串类型数据对象两部分组 成。如表5.1所列,str与list、tuple、dict和set都有壳体,都可以存放数据对象,并且在对存 放元素的操作上还有许多共同之处。唯一的不同是字符串太单纯,只存放字符,不像其他容 器那样,只要是数据对象都可以存放。此外,str与list和tuple在可解析性等方面也极为类 似,都属于Python的序列容器。 表5.1 Python内置内存容器的基本特征 名称标识符边 界 符元素类型 元素 可变 元素 有序 元素 分隔 元素 互异 字符串 元组 列表 字典 集合 str tupie list dict set forzenset '…'、"…"、'''…'''、"""…""" (…) […] {…} 字符串 任何类型 任何类型 键有限制,值可为任何对象 任何类型 否 是 否 位置 顺序 否 无 , 否 值否 是 5.1 Python内存容器对象的共性操作 5.1.1 内存容器对象的创建与类型转换 内存容器(简称容器)对象都可以用如下3种方式构建对象:用字面量直接书写、用构 造方法构造和用推导式创建。 ·141· 1.用字面量直接书写容器实例对象 不管是元组、列表、字符串,还是字典、集合,只要用相应的边界符将合法的元素括起来, 就成为某个容器的字面量,这个字面量是某种类型容器的实例对象。 代码5.1 用字面量创建容器实例对象的同时,用引用变量指向该对象。 >>> a = 1;b = 2; c = 3 >>> #####列表对象创建##### >>> list1 = [a,b,c,a,3] >>> list1; type(list1) [1, 2, 3, 1, 3] >>> #####元组对象创建##### >>> tuple1 = a,b,c,a,3 >>> tuple1; type(tuple1) (1, 2, 3, 1,3) >>> #####集合对象创建##### >>> set1 = {a,b,c,1,2,5} >>> set1; type(set1) {1, 2, 3, 5} >>> #####字典对象创建##### >>> dict1 = {'a':a,'b':b,'c':c,'a':5,'d':a} >>> dict1; type(dict1) {'a': 5, 'b': 2, 'c': 3, 'd': 1} >>> #####字符串对象创建##### >>> str1= 'abcde 123abc' >>> str1; type(str1) 'abcde123abc' .................................................................................... 说明: 从上述容器对象创建过程可以看出: (1)Python内置容器不一定都要求只存储相同类型的元素。 (2)列表和元组允许有重复的元素,而集合不能有重复元素,因为集合是按值分配存储 空间的。这符合数学中的集合概念。此外,字典不能有重复的键,若有重复的键,则只取最 后出现的键-值对。因为在字典中是按键存储值的,遇到先出现的键-值对,就先存储起来, 后面再出现相同的键,就用其对应的值覆盖原先的值。 (3)在用字面量创建容器对象的同时,还可以用变量引用这些容器实例对象。 (4)一般来说,元组就是一组以逗号分隔的数据对象,不一定要以圆括号为边界符。但 从易读的角度,还是加一对圆括号为好。 (5)上述对象的类型分别是,说 明这些对象都是相关类的实例,它们的类名分别为list、tuple、set和str。 .................................................................................... 2.用构造方法构造容器对象与容器对象类型转换 每一个类都有自己的构造方法,用于创建这个类的对象(包括空对象)。在面向对象的 ·142· 程序设计中,把作为类成员的函数称为方法。类的构造方法用于构造该类的对象。在创建 非空容器对象时,构造方法要求使用相容对象参数———可以将其转换为所需类型的数据对 象,如将字符串参数向列表转换等。对于列表、元组、字符串、集合和字典这些内置的容器, Python提供了内置的构造方法,分别为list()、tuple()、str()、set()和dict()。在前面的应 用中已经可以体会到,这些构造函数不仅可以创建数据对象,还可以进行类型转换。但是, 列表、元组、集合、字典不能转换为有意义的字符串对象,因为转换时将边界符也作为字符串 的一部分了。 代码5.2 列表、元组、集合、字典不能转换为有意义的字符串对象示例。 >>> s3 = str([5,3,1,'b','a','c',1]);s3,id(s3) #列表转换为字符串 ("[5, 3, 1, 'b', 'a', 'c', 1]", 1928842938480) >>> s4 = str((5,3,1,'b','a','c',1));s4,id(s4) #元组转换为字符串 ("(5, 3, 1, 'b', 'a', 'c',1)", 1928842938568) >>> s5 = str({5,3,1,'b','a','c',1});s5,id(s5) #集合转换为字符串 ("{5, 3, 1, 'b', 'a', 'c',1}", 1928842938656) >>> s6 = str({5:'a',7:'c',9:'s',3:'x'});s6,id(s6) #字典转换为字符串 ("{5: 'a', 7: 'c', 9: 's', 3: 'x'}", 1928842938744) 3.用推导式创建容器对象 为了动态地修改或创建容器对象,Python推出了推导式(comprehension)。 代码5.3 用推导式创建容器对象示例。 >>> [i * 2 for i in range(10)] #不带条件的列表推导式 [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] >>> [i * 2 for i in range(10) if i %2 != 0] #带有条件的列表推导式 [2, 6, 10, 14, 18] >>> {i * 2 for i in range(10)} #不带条件的集合推导式 {[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]} >>> [i * 2 for i in range(10) if i % 2 != 0] #带有条件的集合推导式 {2, 6, 10, 14, 18} >>> [x + y for x in 'ab' for y in '123'] #形成字符串列表 ['a1', 'a2', 'a3', 'b1', 'b2', 'b3'] >>> d5 = dict(zip(('a','b','c'),(1,2,3)));d5,id(d5) #基于zip 创建字典对象 ({'a': 1, 'b': 2, 'c': 3}, 1570113683176) .................................................................................... 注意:不要用推导式代替一切。若只需要执行一个循环,就应当尽量使用循环,更符合 Python提倡的直观性。 .................................................................................... 4.其他 字典除用dict()作为构造方法外,还提供了用fromkeys()构建字典的手段。fromkeys() 是dict类的成员,其语法如下: dict.fromkeys(seq[,value])) 这里的dict也可以用{}代替,语法如下: {}.fromkeys(seq[,value])) ·143· 它的参数是两个序列:seq(字典键值序列)和value(可选参数,设seq的值,返回值为一 个字典元素序列)。 代码5.4 用fromkeys()构建字典对象示例。 >>> seq = ('name', 'age', 'sex') >>> value = ('张', 28, 'male') >>> dict1 = dict.fromkeys(seq,value) >>> print (f"创建的字典为{str(dict1):s}。" ) 创建的字典为{'name': ('张', 28, 'male'), 'age': ('张', 28, 'male'), 'sex': ('张', 28, 'male')}。 >>> dict2 = dict.fromkeys(seq,'x') >>> print (f"创建的字典为{str(dict2):s}。") 创建的字典为{'name': 'x', 'age': 'x', 'sex': 'x'}。 >>> dict3 = dict.fromkeys(seq) >>> print (f"创建的字典为{str(dict3):s}。" ) 创建的字典为{'name': None, 'age': None, 'sex': None}。 >>> dict4 = {}.fromkeys(seq) >>> print (f"创建的字典为{str(dict4):s}。" ) 创建的字典为{'name': None, 'age': None, 'sex': None}。 5.1.2 容器对象的通用操作 所有的容器对象都可以进行下列操作。 1.type、ID 码和len 类型(type)与ID码是对象最重要的两个属性,前面已经使用过许多,这里不再赘述。 另一个重要属性是容器中元素的个数,它可以使用函数len()获取。 代码5.5 获取容器对象的长度。 >>> t1 = 'a','b','c','d','e','f',1,2,3,4,5,6 >>> len(t1) 12 >>> l1 = [t1] >>> len(l1) 1 >>> s1 = set(t1) >>> len(s1) 12 >>> l2 = list(t1) >>> len(l2) 12 >>> l1 [('a','b','c','d','e','f', 1, 2, 3, 4, 5)] .................................................................................... 说明:代码5.5中,先测试t1的长度,得到12;若用l1=[t1]将t1转换成列表,测试的 结果却是1;再将t1用s1=set(t1)转换为集合s1,测试结果为12;再用l2=list(t1)测试又 得到12。最后显示l1,得到[(a' ',b' ',c' ',d' ',e' ',f' ',1,2,3,4,5)]。这说明l1=[t1]是将t1作 为一个元素了。所以,进行容器类型转换,必须显式地使用构造方法。 .................................................................................... ·144· 2.获取容器中的最大元素、最小元素与数值元素和 下面3个Python内置函数可用于获取容器有关数据。 max(s):返回容器s的最大值(仅限字符串或数值序列)。 min(s):返回容器s的最小值(仅限字符串或数值序列)。 sum(s):返回容器s的元素之和(仅限数值序列)。 代码5.6 获取容器最大元素、最小元素与和示例。 >>> t2 = (2,3,4,5,9,2,1) >>> max(t2),min(t2) (9,1) >>> t3 = {'s','v','ab','wq'} >>> max(t3),min(t3) ('wq','ab') >>> sum(t3) Traceback (most recent call last): File "", line 1, in sum(t3) TypeError: unsupported operand type(s) for +: 'int' and 'str' >>> sum(t2) 26 >>> d1 = {'v':1,'y':8,'g':5,'p':9,'ab':6} >>> max(d1),min(d1) ('y','ab') .................................................................................... 说明:对于字典,元素的最大值与最小值,从可比较的键中选取。 .................................................................................... 3.用dir()获取对象的其他属性 dir()函数不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返 回参数的属性、方法列表。图5.2为用dir()显示几个容器对象属性的示例。 图5.2 用dir()获取容器对象属性示例 ·145· 4.容器及其成员的判定运算 容器对象的判定运算包括如下5类,它们均得到bool值:True或False。 (1)对象值比较运算符:>、>=、<、<=、==和!=。 (2)对象身份判定运算符:is和isnot。 (3)成员属于判定运算符:in和notin。 (4)布尔运算符:not、and和or。 (5)判定容器对象的元素是否全部或部分为True的内置函数:all()和any()。 代码5.7 对序列进行判定运算示例。 >>> list1 = ['ABCDE','Hello',"ok",'''Python''',123]; list2 = ['xyz',567] >>> list1 == list2, list1 != list2, list1 > list2, list1 < list2 (False, True, False, True) >>> 'ABCDE' in list1 True >>> ['xyz',567]is list2,list2 is ['xyz',567],list2 == ['xyz',567] (False, False, True) >>> >>> tup1 = (1,2,3); tup2 = (1,2,3); tup3 = ('a','b','c') >>> tup1 == tup2,tup1 is tup2 (True, False) >>> tup1 = tup2; tup1 is tup2 True >>> tup3 < tup2,tup3 > tup2 Traceback (most recent call last): File "", line 1, in tup3 < tup2,tup3 > tup2 TypeError: '<' not supported between instances of 'str' and 'int' >>> tup3 != tup2 True >>> >>> str1 = 'abcxy';str2 = 'abcdef' >>> str1 < str2, str1 > str2 (False, True) >>> >>> set1 = {1,2,3};set2 = {1,2,3,4,5} >>> set1 > set2,set1 < set2,set1 == set2,set1 != set2 (False, True, False, True) >>> >>> all(tup3),any(tup3) (True, True) .................................................................................... 说明: (1)相等比较(==)与是否比较(is)不同,相等比较的是值,是否比较的是ID码。 (2)只有相同元素类型的容器对象才可以进行大小比较。不同元素类型的容器对象只 可以进行相等或不等的比较。 (3)字符串之间的比较是按正向下标,从0开始以对应字符的码值(如ASCII码值)作 为依据进行的,直到对应字符不同,或所有字符都相同,才能决定大小或是否相等。 .................................................................................... ·146· 5.1.3 对象的浅复制与深复制 在Python中,“=”的本职操作是为对象添加引用,只有对修改后的不可变对象进行引 用时,才会形成新的对象;要复制数据对象,应当通过有关类或模块中的复制函数进行。这 些复制函数可以分为两类:浅复制(shallowcopy)和深复制(deepcopy)。 ● 可以进行浅复制的函数或方法包括:copy模块中的copy()函数、序列的切片操作、 对象的实例化等。 ● 可以进行深复制的函数或方法包括:copy模块中的deepcopy()函数等。 下面分两种情形举例说明浅复制和深复制的区别。 1.浅复制和深复制仅对可变数据对象有区别 代码5.8 对于可变数据对象和不可变数据对象来说,浅复制与深复制的作用区别 示例。 >>> import copy #导入copy 模块 >>> x = (1,2,3,('a','b')) # x 为 不可变对象 >>> y = copy.copy(x) #y 为x 的浅复制 >>> z = copy.deepcopy(x) #z 为x 的深复制 >>> id(x),id(y),id(z) #比较x、y、z 三者的ID 码 (2319542617016, 2319542617016, 2319542617016) >>> x1 = [1,2,3,['a','b']] #x1 为可变对象 >>> y1 = copy.copy(x1) #y1 为x1 的浅复制 >>> z1 = copy.deepcopy(x1) #z1 为x1 的深复制 >>> id(x1),id(y1),id(z1) #比较x1、y1、z1 三者的ID 码 (2319502470600, 2319542524936, 2319542496392) .................................................................................... 说明: (1)id(y)和id(z)都与id(x)相同,说明对于不可变对象,浅复制与深复制都不重新创建 新的对象。 (2)id(x1)、id(y1)与id(z1)三者都不相同,说明对于可变对象,浅复制与深复制都重 新创建了新的对象,但三者所创建的新对象是不相同的对象。 .................................................................................... 2.对层次性对象(如嵌套的对象容器以及派生类对象)进行浅复制,只复制最上一层 代码5.9 对于嵌套容器中的可变数据元素来说,浅复制与深复制复制深度的不同 示例。 >>> import copy >>> x = [1,2,3,['a','b','c'],4] >>> y = copy.copy(x) >>> z = copy.deepcopy(x) >>> id(x[2]),id(y[2]),id(z[2]) #获取同一整数对象的ID 码 (1584641152, 1584641152, 1584641152) >>> id(x[3][0]),id(y[3][0]),id(z[3][0]) #获取同一字符串的ID 码 (1907772550704, 1907772550704, 1907772550704) >>> id(x[3]),id(y[3]),id(z[3]) #获取同一可变对象成员的ID 码 (1907782020168, 1907782020168, 1907782020040) ·147· .................................................................................... 说明: (1)在容器嵌套的情况下,对于可变元素来说,浅复制与深复制的情形就不相同了。尽 管浅复制的数据对象的ID码与源对象的ID码不同,但其所有元素的ID码都与源对象对应 相同,即浅复制虽然会创建新对象,但其内容是原对象的引用,即它只复制了一层外壳。而 对于深复制来说,其内嵌的可变元素对象的ID码与源对象的对应元素的ID码不再相同,即 这些内嵌元素也被复制了。所以深复制是完全复制,包括了多层嵌套复制,复制的深度大于 浅复制。 (2)对于在第4章介绍的派生类对象复制,也会产生类似的情况。 .................................................................................... 习题5.1 一、判断题 1.元组与列表的不同仅在于一个是用圆括号作为边界符,另一个是用方括号作为边界 符。( ) 2.创建只包含一个元素的元组时,必须在元素后面加一个逗号,例如(3,)。( ) 3.列表是可变的,即使它作为元组的元素,也可以修改。( ) 4.表达式list('[1,2,3]')的值是[1,2,3]。( ) 5.表达式[]==None的值为True。( ) 6.生成器推导式比列表推导式具有更高的效率。( ) 7.代码 >>> aList = [] >>> for x in range(30):aList.append(x + x) 与代码 >>> aList = [x + x for x in range(30)] 等价。( ) 二、选择题 1.在后面的可选项中选择下列Python语句的执行结果。 print(type({}))的执行结果是( )。 print(type([]))的执行结果是( )。 print(type(()))的执行结果是( )。 A. B. C. D. 2.推导式[4(x,y)forxin[1,2,3]foryin[3,1,4]ifx!=y]的执行结果是( )。 A.[(1,3),(2,1),(3,4)] B.[(1,3),(1,4),(2,3),(2,1),(2,4),(3,1),(3,4)] C.[1,2,3,3,1,4] ·148· D.[(1,3),(1,1),(1,4),(2,3),(2,1),(2,4),(3,3),(3,1),(3,4)] 3.代码 >>> vec = [(1,2,3),(4,5,6),(7,8,9)] >>> [num for e in vec for num in e} 执行的结果是( )。 A.[1,2,3,4,5,6,7,8,9] B.[[1,2,3],[4,5,6],[7,8,9]] C.[[1,4,7],[2,5,8],[3,6,9]] D.(1,2,3,4,5,6,7,8,9) 4.下面不能创建一个集合的语句是( )。 A.s1=set() B.s2=set("abcd") C.s3=(1,2,3,4) D.s4=frozenset((3,2,1)) 5.下列代码执行时会报错的是( )。 A.v1={} B.v2={3:5} C.v3={[1,2,3]:5} D.v4={(1,2,3):5} 6.以下不能创建一个字典的语句是( )。 A.dict1={} B.dict2={3:5} C.dict3=dict([2,5],[3,4]) D.dict4=dict(([1,2],[3,4])) 7.代码 nums=set{1,1,2,3,3,3,4};print(len(nums) 执行后的输出结果为( )。 A.2 B.4 C.2 D.7 8.代码 a={1:/a/,2:/b/,3:/c/};print(len(a)) 执行后的输出结果为( )。 A.1 B.43 C.0 D.6 9.代码 a=[1,2,3,None,(),[],];print(len(a)) 执行后的输出结果为( )。 A.1 B.43 C.0 D.6 三、填空题 1.Python语句list1=[1,2,3,4];list2=[5,6,7];print(len(list1+list2)的执行结果 是。 2.Python语句print(tuple([1,3,]),list([1,3,]))的执行结果是。 3.Python语句print(tuple(range2),list(range2))的执行结果是。 4.Python列表生成式[iforiinrange7ifi%2!=0]和[i**2foriinrange5]的值 分别为。 ·149· 5.Python代码print(set([3,5,3,5,8]))的执行结果是。 6.使用列表推导式生成包含10个数字5的列表,语句可以写为。 7.Python代码score={l'anguage':80,'math':90,p' hysics':88,c' hemistry':82};score[' physics']=96;print(sum(score.value()/len(score)))的执行结果是。 8.Python代码d={1:'a',2:'b',3:'c',4:'d'};deld[1];deld[3];d[1]='A';print(len (d))的执行结果是。 9.Python代码score={l'anguage':80,'math':90,p' hysics':88,c' hemistry':82};score[' physics']=96;print(sum(score.value()/len(score)))的执行结果是。 10.Python代码print(sum(range(10)的执行结果是。 11.Python代码s1=[1,2,3,4];s2=[5,6,7]print(sum(range(10)的执行结果 是。 12.在Python代码中,first,*middles,last=range6执行后,middles的值为; first,second,third,*lasts=range6 执行后,lasts的值为;*firsts,last3,last2. Last=range6 执行后,firsts的值为;*middles,last=[88,85,99,95,66,77,96] 执行后,sum(middles)//len(middles)/的值为。 四、代码分析题 1.阅读下面的代码片段,给出各行的输出。 list = [[]]*5; list #output? 2.执行下面的代码,会出现什么情况? a = [] for i in range(10): a[i]= i * i 3.分析下面的代码,给出输出结果。 def multipliters(): return [lambda x:i * x for i in range(4)] print([m2 for m in multipliters()]) 4.分析下面的代码,给出输出结果。 L = ['Hello','World','IBM','Apple'] print([s.lower() for s in L]) 5.指出下面代码的输出是什么,并解释理由。 def multipliers(): return [lambda x : i * x for i in range(4)] print [m2 for m in multipliers()] 怎么修改multipliers的定义才能达到期望的结果? 五、实践题 1.编写代码,实现下列变换。 ·150·