Python列表、元组、集合 实验实训
一、实验目的
1. 深入掌握列表、元组、集合底层特性:可变/不可变、可哈希判定规则,区分“引用不可变”和“元素内容不可变”;
2. 精通切片语法、步长正负、del切片删除的底层执行逻辑,理解切片删除时索引的计算陷阱;
3. 熟练运用集合交、并、差、对称差运算,掌握集合推导式、列表推导式;
4. 能够处理嵌套列表、嵌套元组,准确判断嵌套对象是否可哈希;
5. 理解集合去重丢失顺序问题,实现保留原始顺序的去重算法;
6. 具备代码排错能力,独立分析TypeError: unhashable type、切片删除预期不符等疑难问题;
7. 根据需求自主选择容器,完成小型综合数据处理任务。
二、实验环境
1. Python 3.X及以上;
2. 开发工具:PyCharm或VSCode;
3. 无第三方库依赖。
三、预习任务(课前必须完成)
(一)回顾基础
回顾list有序可变、tuple有序不可变、set无序可变、元素必须可哈希等基本特性。
(二)重点预习难点
1. del ls[::-1]、del ls[1::2]、del ls[::3]的区别,理解“切片删除是按照原始列表索引一次性计算,不是动态实时索引”;
2. 嵌套容器哈希判定:(1,2)可哈希;(1,[2,3])不可哈希;
3. 对称差集^的含义;
(三)预习思考题
1. 执行ls = [1,2,3,4,5,6]; del ls[::2],剩下哪些元素?如果一边循环一边删除列表元素会出现什么问题?
2. t = ([1,2],[3,4])能否加入集合?为什么?
3. 集合是可变对象,集合本身能否作为另一个集合的元素?如果想要嵌套集合怎么办?(提示:frozenset)
答:
1.循环删除问题:for 遍历依据列表索引迭代,删除元素后列表长度改变,后续元素向前移位,会造成元素跳过漏删,无法达到预期删除效果。
2.不能加入集合。元组内部存放了列表(可变对象),导致整个元组不可哈希,集合元素要求必须是可哈希对象,会触发TypeError: unhashable type: 'list'。
3.普通 set 是可变对象,不可哈希,不能直接作为集合的元素。 需要嵌套集合时,使用frozenset(冻结集合),它是不可变、可哈希类型,可以放入集合当中
四、实验原理
1. 列表list:动态数组,存储对象引用;切片操作生成新序列,del 列表[切片]原地批量删除;遍历过程中删除元素极易产生漏删;
2. 元组tuple:不可变指元组内部存储的引用地址不可修改,不限制引用指向的可变对象内部修改;只要元组内任意一个元素是不可哈希对象,整个元组不可哈希;
3. 集合set:哈希表实现;支持&、|、-、^四种运算;普通set可变,不能嵌套;固定不变集合使用frozenset(可哈希,可以放进集合);
4. 哈希判定:只有生命周期内hash值固定的对象,才是可哈希对象;
5. 去重两种方案:list(set(ls))简洁但丢失原有顺序;保序去重利用字典key自动去重且有序,或循环判断实现。
五、实验内容与任务(由易到难,共5个任务,任务5为综合设计必做)
任务1切片删除深度探究(验证+分析,重点考点)
要求:先手写预判结果,再运行代码核对,分析为什么和直觉不一样。
# 1
ls1 = [0,1,2,3,4,5,6,7,8,9]
del ls1[::3]
print("ls1:", ls1)
# 2
ls2 = [0,1,2,3,4,5,6,7,8,9]
del ls2[1::3]
print("ls2:", ls2)
# 3负步长切片删除
ls3 = [0,1,2,3,4,5]
del ls3[::-2]
print("ls3:", ls3)
#拓展坑:不要在for循环里边遍历边删
ls4 = [1,2,3,4,5,6]
for i in ls4:
if i % 2 == 0:
ls4.remove(i)
print("ls4循环删除偶数结果:", ls4)
要求:写出预判值、实际运行值,解释循环删除出错的原因。
del ls1[::3]预判:[1, 2, 4, 5, 7, 8] 实际:[1, 2, 4, 5, 7, 8]del ls2[1::3]预判:[0, 2, 3, 5, 6, 8, 9] 实际:[0, 2, 3, 5, 6, 8, 9]del ls3[::-2]预判:[0, 2, 4] 实际:[0, 2, 4]ls4 预判:[1, 3, 5] 实际:[1, 3, 5, 6]
解释循环删除出错的原因:for i in ls4是根据列表迭代器按下标顺序遍历;当remove()删除一个元素,列表长度变短,后面元素整体向前移动一位,迭代器下标继续 + 1,会跳过下一个元素。
任务2嵌套容器与可哈希深度辨析(探究题)
依次取消注释测试,记录每一条报错或成功,写出理由:
s = set()
# s.add( (1,2,3) )
# s.add( (1,[2,3]) )
# s.add( [1,2] )
# s.add( frozenset({1,2}) )
# s.add( {1,2} )
print(s)
1:s.add( (1,2,3) ) 成功 ,集合:{(1, 2, 3)}原因分析:元组(1,2,3)全部元素都是不可哈希(不可变),整体元组可哈希,可以作为 set 的元素。
2:s.add( (1,[2,3]) ) 报错TypeError: unhashable type: 'list' 原因分析:元组本身引用不可变,但内部包含列表[2,3](列表可变、不可哈希);只要元组有任意一个元素不可哈希,整个元组就不可哈希,不能放入集合。
3:s.add( [1,2] ) 报错TypeError: unhashable type: 'list'原因分析:list 是可变容器,不可哈希,不能作为集合的元素。
4:s.add( frozenset({1,2}) ) 成功,集合:{frozenset({1, 2})}原因分析:frozenset是不可变固定集合,可哈希,可以加入普通 set。
5:s.add( {1,2} ) 报错,TypeError: unhashable type: 'set'原因分析:普通set是可变对象,不可哈希,不能作为另一个集合的元素。
思考:如果我需要“集合里面存放集合”,应该用什么类型?
答:普通 set 可变不可哈希,不能嵌套;需要使用 frozenset(冻结集合)。frozenset 不可变、可哈希,可以作为 set 的元素,实现集合嵌套
frozenset作用:frozenset 为不可变集合,主要解决普通 set 不可哈希带来的限制;当需要把集合作为另一个集合的元素或者字典的键时,使用 frozenset。一旦创建,内容不能修改。
任务3集合全部运算+集合推导式
给定两组数据:A = {2,4,6,8,10,12},B = {3,6,9,12,15}。
要求代码实现:交集、并集、A-B、B-A、对称差集。
额外:使用集合推导式生成1~30中所有能被4整除的数字集合。
代码:
任务4去重算法对比(保序vs无序去重)
原始列表:data = [5,2,7,2,5,9,7,3,9,1]。
方案1:list(set(data)),观察输出顺序;
方案2:自己编写代码,保留第一次出现顺序完成去重(不允许直接使用第三方库)。
提示:新建空列表,遍历原始数据,元素不在新列表才append。
拓展:使用dict.fromkeys(data)一行实现保序去重。
对比:
- list(set(data)) 优点:代码简短,快速完成去重。 缺点:集合是无序的,会丢失原始元素出现顺序,输出顺序不确定。
- 手写循环保序去重 原理:新建空列表,遍历原列表,如果元素不在新列表,就追加进去;保留元素第一次出现的先后顺序。 优点:保留原始顺序,容易理解,兼容所有 Python 版本。 缺点:
num not in res2是列表查找,数据量大的时候效率偏低。
任务5综合设计题(必做,分值最高)
需求:有两份学生选课编号列表:
class1 = [101,103,105,107,109,103,101]
class2 = [102,104,106,108,109,107,102]
① 分别获取两个班不重复的选课学生编号(保序);
② 求出同时选两门课的学生编号;
③ 求出只选其中一门课的学生编号(对称差);
④ 求出选了1班但是没有选2班的学生编号;
⑤ 将最终的交集结果转为元组(为什么交集结果可以转元组,还可以存入集合?)
输出所有结果,并配上文字说明。
拓展
给定嵌套列表 nest = [[1,2],[3,4],[1,2],[5,6]],要求对嵌套列表去重。
提示:内层list不可哈希,需要先转成元组,去重之后按需转回列表。
必答思考题
1. del ls[::3]是按删除前索引计算还是动态实时索引?举例说明;
2. t=(1,[2,3]),为什么t[1].append(4)合法,但是t不能放入集合?
3. 为什么普通set不能嵌套,frozenset可以作为集合元素?
4. 使用for遍历列表同时remove元素为什么会出现删不干净?给出一种安全的修改方案;
5. list(set(数据))去重的缺陷是什么?保序去重有哪些实现方式?
答:
1.dells[::3]是按照删除之前原始列表的索引一次性计算所有待删除下标,不是删除一个元素就动态更新索引。 举例:ls = [0,1,2,3,4,5,6,7,8,9] del ls[::3] 原始下标 0、3、6、9 被删除;不是删完 0 之后列表变短再重新算下一个下标。如果是动态索引,得到的结果会和实际运行结果不一致。
2.元组的不可变指存储元素的引用地址不可改变,不是元素里面的数据不能修改。t[1]存的是列表的引用,这个引用不能替换,但是列表本身是可变对象,可以调用 append 修改列表内部内容,所以t[1].append(4)合法。 集合要求元素必须可哈希;元组中包含列表(不可哈希对象),整个元组就变成不可哈希,因此不能加入集合,会报TypeError: unhashable type: 'list'。
3.普通set是可变对象,可以 add、remove 修改内容,不可哈希,不能作为集合的元素。frozenset是冻结集合,创建后内容不能修改,属于可哈希对象,满足集合对元素的哈希要求,因此可以作为另一个集合的元素,实现集合嵌套。
4.for迭代器依靠下标遍历列表;调用remove()删除元素,列表长度缩短,后面的元素向前移动;迭代器下标继续向后增加,会跳过部分元素,造成漏删。即使个别测试样例碰巧输出正确,写法依旧不可靠
安全方案二选一: 方案 1:遍历列表的副本,for i in ls.copy():,在原列表执行 remove。 方案 2:使用列表推导式生成新列表,ls = [x for x in ls if 条件]
5.缺陷:集合是无序容器,list(set(数据))去重之后丢失原始数据的先后顺序,输出顺序不确定。
保序去重实现方式:
- 手写循环:新建空列表,遍历原始数据,元素不在新列表才 append;
list(dict.fromkeys(数据源)),利用字典 key 不可重复并且保存插入顺序的特性实现保序去重
实验小结
本次实验掌握了列表、元组、集合三种容器的核心特性:列表有序可变,支持切片原地删除,但遍历中直接删除会发生漏删问题;元组有序不可变,仅引用地址固定,内部若包含可变元素则整体不可哈希;集合基于哈希表实现,元素必须可哈希,可快速完成交、并、差、对称差运算,但原生无序,普通集合不能嵌套,需使用frozenset实现嵌套。
集合直接去重简单高效,但会丢失原始顺序;可通过循环判断或dict.fromkeys()实现保序去重。编写代码时需要注意可哈希判定、切片删除按原始索引计算等陷阱,根据业务需求合理选择容器类型处理数据。