1 列表的底层 —— 内存中的连续车厢
是什么?为什么学?
列表(list)是 Python 里最常用的容器:一个变量装多个值。之前学的变量一次只能存一个值,但现实中常常有一堆数据——购物清单有 10 样东西、一个班有 30 个成绩。列表就是用来「一次装一堆」的容器。为什么零基础必学?了解它的底层设计,你才能理解「下标为什么从 0 开始」「为什么 insert 比 append 慢」这些高频问题。
底层原理
Python 的列表在内存里是动态数组:它申请一块连续的内存,每个元素存一个「引用」(指向真实数据的地址,类似快递单号)。因为存的是引用而不是数据本身,所以同一个列表里可以混装字符串、数字、布尔值。按下标取值 fruits[2] 时,Python 直接按「起始地址 + 2 × 每个引用占的字节数」算出位置,一步到位——所以下标取值极快,这也是下标从 0 开始的原因:第 1 个元素的偏移量是 0。
术语小词典:引用(reference)就是「指向真实数据的地址编号」,像快递单号——凭单号去仓库取真东西。
生活类比
列表像一列火车:车厢一节挨一节(内存连续),每节车厢里放的是一张「快递单号」(引用),凭单号去仓库取真东西(数据)。想找第 3 节车厢的东西,从车头数过去就行——这就是按下标取值的原理。
注意事项总结
1 一个变量装多个值 + 按下标取:
fruits = ["苹果", "香蕉", "橘子", "葡萄"]
print(fruits[0]) # 第一个元素
print(fruits[1]) # 第二个元素
print(fruits[3]) # 第四个元素
运行结果:
苹果
香蕉
葡萄
下标从 0 开始数,所以下标 0、1、2、3 对应第一、二、三、四个元素。写 fruits[4] 就会报 IndexError(越界)。
2 负数下标——从尾部数:
fruits = ["苹果", "香蕉", "橘子"]
print(fruits[-1]) # 倒数第一个
print(fruits[-2]) # 倒数第二个
运行结果:
橘子
香蕉
负数下标本质是「长度 – 正数」。列表长了之后,用 -1 取最后一个元素是最高频的写法。
3 len() 数长度 + 字符串也能按下标取:
fruits = ["苹果", "香蕉", "橘子"]
print(len(fruits)) # 列表长度
word = "Python"
print(word[0]) # 字符串也能按下标取字符
print(len(word))
运行结果:
3
P
6
字符串本质上也是「字符的序列」,同样支持下标。这也解释了为什么 Day 5 里 for ch in "abc" 能遍历字符串——凡是「能按下标取」的东西都能被 for 遍历。
易错点
- 错误写法:
fruits[3](列表只有 3 个元素) → 问题:报IndexError: list index out of range,下标越界 → 正确写法:最大下标是len(列表) - 1,或用负数下标[-1]取最后一个。 - 错误写法:以为下标从 1 开始,
fruits[1]想取第一个 → 问题:实际取到的是第二个元素 → 正确写法:下标从 0 开始,第 n 个元素的下标是 n – 1。 - 错误写法:
list = [1, 2]用 list 当变量名 → 问题:覆盖了内置函数list(),后面再调用list()会报错 → 正确写法:别用list、str、int等内置名字当变量名。
记忆口诀:下标从零数,-1 取末尾;内存连续存,取值快如飞。
2 增删改查 —— 动态数组的四大操作
是什么?为什么学?
列表能随时增、删、改,这是它比元组、字符串强的地方。以后写程序处理数据(加一条记录、删一条记录、改一个值)全靠这几个操作,是列表使用频率最高的部分。
底层原理
因为列表是动态数组:append 在末尾追加时,容量够就直接写;容量不够就「搬家」——重新申请一块更大的内存(通常是原容量的 1.125 倍或更多),把旧元素复制过去再追加,所以 append 偶尔慢一下,但平均很快。insert 插到中间则要把插入位置之后的元素全部往后挪一位,元素越多越慢。remove 删除同理:先找到值的位置,再把后面的元素往前挪。
生活类比
列表像一列可改造的火车:append 是在车尾挂一节新车厢(最省事);insert 是硬在中间插一节,后面的车厢都得往后挪;remove / pop 是卸掉一节车厢,后面的再往前补位。挪动越多越慢——所以车尾操作(append / pop 末尾)总是最快的。
注意事项总结
1 增——append 和 insert:
shopping = ["牛奶"]
shopping.append("鸡蛋") # 末尾追加
shopping.append("面包")
shopping.insert(1, "火腿") # 插到下标 1
print(shopping)
运行结果:
['牛奶', '火腿', '鸡蛋', '面包']
append(值) 永远把新元素放到最后;insert(位置, 值) 把值插到指定下标,原本该位置及之后的元素自动往后挪。
2 删——remove 按值删,pop 按下标删:
nums = [1, 2, 3, 4]
nums.remove(2) # 按「值」删除第一个 2
last = nums.pop() # 弹出最后一个
first = nums.pop(0) # 弹出下标 0 的元素
print(nums)
运行结果:
[3]
remove(值) 按值删除(只删第一个匹配的,值不存在会报 ValueError);pop(位置) 删除并返回被删的值,不写位置默认弹最后一个。
3 改——按下标直接赋值:
scores = [85, 92, 78]
scores[2] = 99 # 把第三个成绩改成 99
scores[0] = scores[0] + 10 # 第一个成绩加 10 再存回去
print(scores)
运行结果:
[95, 92, 99]
列表[下标] = 新值 是「改」的唯一写法。scores[0] = scores[0] + 10 先读旧值、加 10、再写回——这是「原地更新」的经典写法。
易错点
- 错误写法:
nums.remove(99)(99 不在列表里) → 问题:报ValueError: x is not in list→ 正确写法:先if 99 in nums:判断,再 remove。 - 错误写法:
shopping[5] = "可乐"(想加新元素) → 问题:报IndexError,下标赋值不能越界新增 → 正确写法:追加用append,插入用insert。 - 错误写法:
nums.remove(2)想删掉所有 2 → 问题:只删了第一个 2 → 正确写法:循环 remove 直到2 not in nums,或用列表推导(Day 8 后学)。 - 错误写法:
fruits = fruits.append("梨")→ 问题:把 append 的返回值赋给变量,fruits 变成 None! → 正确写法:append 是「原地操作」,返回 None,直接fruits.append("梨")即可。
记忆口诀:末尾追加 append,指定位置 insert;按值删除 remove,弹出用 pop 有回礼。
3 切片 —— 一次取出一段数据
是什么?为什么学?
切片(slicing)用 列表[开始:结束:步长] 一次取出一段,是 Python 最优雅的语法之一。反转、取前三个、隔一个取一个……一行搞定。学会切片,很多看似复杂的操作会变成一行代码。
底层原理
切片操作会创建一个新列表(拷贝):Python 按你给的开始、结束、步长,把对应的元素复制到新列表里。所以切出来的列表和原列表是「两份独立的数据」——修改切片结果不会影响原列表。步长为负时,Python 从尾部反向取,[::-1] 因此成了「反转」的经典写法。
生活类比
切片像复印机:你从一摞资料里选出第 3~5 张,复印出一份新的小册子——原资料一动不动,复印件随便改。[:] 就是「整摞全部复印一遍」,[::-1] 是「倒着复印一遍」。
注意事项总结
1 基本切片——含头不含尾:
nums = [10, 20, 30, 40, 50]
print(nums[1:4]) # 下标 1 到 3
print(nums[:3]) # 从头到下标 2
print(nums[2:]) # 从下标 2 到末尾
print(nums[:]) # 整个拷贝
运行结果:
[20, 30, 40]
[10, 20, 30]
[30, 40, 50]
[10, 20, 30, 40, 50]
[1:4] 取下标 1、2、3(含 1 不含 4),和 range 一样「含头不含尾」;省略开始 = 从头开始;省略结束 = 到末尾;[:] 都省略 = 整个列表的拷贝(不是同一个列表!)。
2 步长——隔一个取一个、反转:
nums = [10, 20, 30, 40, 50]
print(nums[::2]) # 隔一个取一个
print(nums[::-1]) # 反转
运行结果:
[10, 30, 50]
[50, 40, 30, 20, 10]
[::2] 步长为 2,取下标 0、2、4;[::-1] 步长为 -1,从尾部开始每次减 1,结果就是反转。
3 切片产生的是新列表(修改切片不影响原列表):
nums = [1, 2, 3, 4, 5]
part = nums[1:3]
part[0] = 99
print(part) # 修改切片结果
print(nums) # 原列表不受影响
运行结果:
[99, 3]
[1, 2, 3, 4, 5]
改 part 只是改那份拷贝,nums 还是原样。理解这一点,你就不会犯「改切片以为改了原列表」的错误。
易错点
- 错误写法:
nums[1:4]以为取 4 个 → 问题:只取 3 个(下标 1、2、3) → 正确写法:记住含头不含尾:个数 = 结束 – 开始。 - 错误写法:想反转却写
nums[-1]→ 问题:nums[-1]只是最后一个元素,不是反转 → 正确写法:反转写nums[::-1],nums[-1]是取末尾单个元素。 - 错误写法:切片的开始比结束大(正步长,如
nums[4:1]) → 问题:返回空列表[],不报错但结果不是想要的 → 正确写法:想从后往前取要用负步长。 - 错误写法:以为切片会修改原列表 → 问题:切片永远返回新列表,原列表不变 → 正确写法:想原地改原列表用赋值:
nums[:2] = [0, 0]。
记忆口诀:切片三参数,含头不含尾;负号一出现,方向就反转。
4 元组 —— 为什么需要「不可变」?
是什么?为什么学?
元组(tuple)用圆括号,长得像列表,但创建后不能增删改。听起来很没用?恰恰相反:正因为不可变,元组才能当字典的键、才能安全地到处传递不被意外修改,运行速度还更快。它是 Python 里「固定数据」的专用容器。
底层原理
元组在内存里是固定大小的数组:创建时一次性分配好所有元素的空间,之后结构不再变化,所以元组没有 append/remove 这些方法。也正因为结构固定,Python 能做更多优化(更少内存、更快的访问),还能算出固定的「哈希值」——这是它以后能当字典键的根本原因(Day 7 会学)。列表可变,算不出稳定的哈希,所以列表不能当字典键。
术语小词典:哈希值(hash value)就是给每个键算一个类似「索引编号」的数字,按编号直接定位,查起来飞快——就像图书馆按书号找书,而不是一本本翻。
生活类比
元组像塑封的证件照:照片定好后就不能再改(不可变),正因为不会变,才能放心地到处提交(当字典键、安全传递);列表像普通照片,随时能涂改,反而不适合当「身份证明」。
注意事项总结
1 创建和读取元组 + 拆包:
point = (3, 5) # 一个坐标点
print(point[0]) # 按下标取
x, y = point # 拆包:一次性取出所有值
print("x =", x, "y =", y)
运行结果:
3
x = 3 y = 5
x, y = point 是元组专属的「拆包」语法——左边写几个变量,右边元组有几个元素,自动一一对应赋值。Day 7 遍历字典时还会见到。
2 元组不能修改(错误示范,运行会报错):
point = (3, 5)
# point[0] = 100 # 运行会报错:TypeError: 'tuple' object does not support item assignment
print("元组一旦创建,内容就固定了")
「不可变」指的是结构不可变:元组里的位置和个数固定了,但元素本身如果是可变类型(如列表)仍然可以改。
3 元组的两种创建姿势(一个小陷阱):
t1 = (1, 2, 3) # 正常元组
t2 = (1) # 没有逗号!这是整数不是元组
t3 = (1,) # 加逗号才是元组
print(type(t1))
print(type(t2))
print(type(t3))
运行结果:
<class 'tuple'>
<class 'int'>
<class 'tuple'>
(1) 里没有逗号,Python 把它当成数学里的括号,结果是整数 1;创建单元素元组必须写 (1,)——这个逗号是元组的「身份标识」。
易错点
- 错误写法:
point[0] = 100→ 问题:报TypeError,元组不能改 → 正确写法:用列表;或重新创建元组point = (100, 5)。 - 错误写法:
t = (1)想创建单元素元组 → 问题:t 其实是整数 1 → 正确写法:写t = (1,),逗号不能省。 - 错误写法:试图
tuple.append(1)→ 问题:报AttributeError,元组没有 append → 正确写法:元组创建后不能加元素,需要变长就用列表。 - 错误写法:忘记元组能拆包,写三行分别取 → 问题:代码啰嗦 → 正确写法:
x, y, z = (1, 2, 3)一行搞定。
记忆口诀:元组圆括号,出生定终身;想取全出来,拆包一行成。
5 遍历列表与常用方法
是什么?为什么学?
把列表里的元素「一个一个」拿出来处理,就是遍历。结合 Day 5 的 for 循环,就能处理成百上千条数据(求和、统计、筛选)。这是「容器 + 循环」的组合拳。
注意事项总结
1 遍历 + enumerate 同时拿下标 + 求和:
scores = [85, 92, 78]
for score in scores: # 直接遍历元素
print("成绩:", score)
for index, score in enumerate(scores): # 还需要位置信息时用 enumerate
print("第", index + 1, "个成绩是", score)
total = 0
for score in scores: # 累加器(Day 5 的升级版)
total = total + score
print("总分:", total, "平均分:", total / len(scores))
运行结果:
成绩: 85
成绩: 92
成绩: 78
第 1 个成绩是 85
第 2 个成绩是 92
第 3 个成绩是 78
总分: 255 平均分: 85.0
只关心「每个值」用 for x in 列表;还需要「第几个」的位置信息时用 enumerate。
2 常用方法速查表:
| 方法 | 作用 |
|---|---|
列表.sort() | 从小到大排序(会改变原列表) |
列表.reverse() | 反转顺序(会改变原列表) |
列表.index(值) | 查找值第一次出现的下标 |
列表.count(值) | 统计值出现次数 |
值 in 列表 | 判断值在不在列表里(返回 True/False) |
nums = [5, 2, 8, 1]
nums.sort() # 从小到大
print(nums) # [1, 2, 5, 8]
nums.sort(reverse=True) # 从大到小
print(nums) # [8, 5, 2, 1]
nums2 = [3, 7, 3, 9, 3]
print(nums2.index(7)) # 7 的下标是 1
print(nums2.count(3)) # 3 出现了 3 次
print(9 in nums2) # True
print(5 in nums2) # False
易错点
- 错误写法:遍历时想改列表元素却直接
for x in 列表: x = 新值→ 问题:x 只是元素的拷贝,改了不影响原列表 → 正确写法:用下标for i in range(len(列表)): 列表[i] = 新值。 - 错误写法:
sort()和sorted()分不清 → 问题:sort()原地改并返回 None,sorted()返回新列表 → 正确写法:想保留原列表用sorted(列表),想原地排用列表.sort()。
记忆口诀:遍历用 for,位置用 enumerate;排序原地 sort,统计 count 和 in。
6 动手实践:购物清单管理
做一个购物清单管理程序,支持 4 个功能:查看清单、添加物品、删除物品、退出。
# 购物清单管理
shopping = ["牛奶", "鸡蛋", "面包"]
while True:
print("\n当前清单:", shopping)
print("1. 查看清单 2. 添加物品 3. 删除物品 4. 退出")
choice = input("请选择操作:")
if choice == "1":
if len(shopping) == 0:
print("清单是空的")
else:
for i, item in enumerate(shopping):
print(i + 1, ". ", item)
elif choice == "2":
item = input("要添加什么:")
shopping.append(item)
print("已添加:", item)
elif choice == "3":
item = input("要删除什么(输入物品名):")
if item in shopping:
shopping.remove(item)
print("已删除:", item)
else:
print("清单里没有这个东西")
elif choice == "4":
print("再见!")
break
else:
print("无效选项,请输入 1~4")
试着运行:添加「可乐」、删除「鸡蛋」、输入无效选项,观察程序的表现。这个程序把 Day 4 的 if/elif、Day 5 的 while/break、Day 6 的列表增删改查全部串起来了。
今日总结
列表是什么:内存里的「动态数组」,存的是引用所以能混装类型,按下标取值极快
下标从 0 开始,-1 是最后一个,最大下标是 len(列表) - 1
增:append(末尾)/ insert(指定位置);删:remove(按值)/ pop(按位置并返回)
改:列表[下标] = 新值;append / sort / reverse 都是原地操作,返回 None
切片 列表[开始:结束:步长] 含头不含尾、返回新列表;[::-1] 反转
遍历:for x in 列表 取元素,enumerate 同时拿下标,配合累加器做统计
元组用 (),不可变、可拆包、更快;单元素元组必须带逗号 (1,)
完成购物清单管理并测试添加、删除、无效输入三种场景
| 报错 | 原因 | 修复 |
|---|---|---|
IndexError: list index out of range | 下标超出了列表长度 | 检查下标范围,最大下标是 len(列表) - 1 |
ValueError: x is not in list | remove / index 的值在列表里不存在 | 先判断 if 值 in 列表 再删除或查找 |
TypeError: 'tuple' object does not support item assignment | 试图修改元组 | 元组不可变,改用列表或重新创建元组 |
AttributeError: 'list' object has no attribute 'append' | 把变量名拼错,或对非列表调用了 append | 确认变量真的是列表,检查拼写 |
TypeError: 'int' object is not subscriptable | 对数字用了下标(5[0]) | 下标只能用在列表、元组、字符串上 |