Python列表、元组、集合 实验实训

发布时间:2026/11/6 1:57:56
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)

要求:写出预判值、实际运行值,解释循环删除出错的原因。

  1. del ls1[::3]预判:[1, 2, 4, 5, 7, 8] 实际:[1, 2, 4, 5, 7, 8]
  2. del ls2[1::3]预判:[0, 2, 3, 5, 6, 8, 9] 实际:[0, 2, 3, 5, 6, 8, 9]
  3. del ls3[::-2]预判:[0, 2, 4] 实际:[0, 2, 4]
  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)一行实现保序去重。

对比:

  1. list(set(data)) 优点:代码简短,快速完成去重。 缺点:集合是无序的,会丢失原始元素出现顺序,输出顺序不确定。
  2. 手写循环保序去重 原理:新建空列表,遍历原列表,如果元素不在新列表,就追加进去;保留元素第一次出现的先后顺序。 优点:保留原始顺序,容易理解,兼容所有 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(数据))去重之后丢失原始数据的先后顺序,输出顺序不确定。

保序去重实现方式:

  1. 手写循环:新建空列表,遍历原始数据,元素不在新列表才 append;
  2. list(dict.fromkeys(数据源)),利用字典 key 不可重复并且保存插入顺序的特性实现保序去重

实验小结

本次实验掌握了列表、元组、集合三种容器的核心特性:列表有序可变,支持切片原地删除,但遍历中直接删除会发生漏删问题;元组有序不可变,仅引用地址固定,内部若包含可变元素则整体不可哈希;集合基于哈希表实现,元素必须可哈希,可快速完成交、并、差、对称差运算,但原生无序,普通集合不能嵌套,需使用frozenset实现嵌套。

集合直接去重简单高效,但会丢失原始顺序;可通过循环判断或dict.fromkeys()实现保序去重。编写代码时需要注意可哈希判定、切片删除按原始索引计算等陷阱,根据业务需求合理选择容器类型处理数据。