从"单兵作战"到"集团军管理"
如果你已经掌握了变量(存储单个数据)以及 if 判断和 for 循环(控制程序逻辑),那么恭喜你,你已经具备了编写脚本的基础骨架。但此时你可能会遇到一个瓶颈:如果你的程序需要处理成百上千个学生的成绩,或者管理一个拥有几百名员工的通讯录,难道要定义几百个像 student_1、student_2 这样的变量吗?显然,这不仅不现实,而且会让代码变得无法维护。
这时候,我们需要引入 Python 中最强大的两个"数据容器":列表 (List)和字典(Dictionary)。
如果把变量比作一个个独立的"小盒子",那么列表就是一个"有序排队的长货架",而字典则是一个"凭钥匙快速查找的智能储物柜"。学会使用它们,标志着你的编程思维从处理"单个数据"跨越到了管理"批量数据",这也是从写几十行的玩具脚本,进化到能处理真实业务数据的关键一步。
列表:有序的万能收纳箱
列表是 Python 中最常用的数据结构之一。想象一下你去银行办事,所有人都在一个队列里按顺序排队,每个人都有一个号码牌(索引)。列表就是这样:它是一个有序的集合,里面的元素可以是任何类型(数字、字符串,甚至另一个列表),并且可以随时修改。
创建与访问:给数据排好队
在 Python 中,列表使用方括号 [] 来定义,元素之间用逗号分隔。
python
# 创建一个包含学生姓名的列表
students = ["Alice", "Bob", "Charlie", "David"]
# 访问元素:通过索引(Index)
# 注意:Python 的索引从 0 开始!
print(students[0]) # 输出:Alice (第一个人)
print(students[2]) # 输出:Charlie (第三个人)
# 负数索引:从后往前数
print(students[-1]) # 输出:David (最后一个人)
print(students[-2]) # 输出:Charlie (倒数第二个人)
很多新手容易犯的错误是访问 students[4],这会报错 IndexError,因为只有 4 个人,最大索引是 3。记住:长度是 4,最大索引是 3。
核心操作:增删改查
在实际开发中,我们很少静态地定义列表,更多时候是动态地往里面添加或移除数据。以下是最高频的四个操作:
-
增加(Append/Insert):
append():把新元素加到列表末尾(最常用)。insert():在指定位置插入元素。
-
修改(Update):直接通过索引赋值。
-
删除(Remove/Pop):
remove():根据值删除(如果有重复,只删第一个)。pop():根据索引删除,并返回被删掉的值(默认删最后一个)。
-
查询(In):判断某个元素是否在列表中。
python
fruits = ["苹果", "香蕉"]
# 1. 增加
fruits.append("橙子") # ['苹果', '香蕉', '橙子']
fruits.insert(1, "葡萄") # ['苹果', '葡萄', '香蕉', '橙子'] (插在索引 1 的位置)
# 2. 修改
fruits[0] = "红苹果" # ['红苹果', '葡萄', '香蕉', '橙子']
# 3. 删除
fruits.remove("葡萄") # 移除值为"葡萄"的元素
last_fruit = fruits.pop() # 移除最后一个 ("橙子"),并赋值给 last_fruit
# 4. 查询
if "香蕉" in fruits:
print("还有香蕉可以吃!")
结合循环:遍历列表的威力
高级操作:切片与排序
掌握了列表的基本增删改查后,让我们来看看两个更高级但非常实用的操作:切片 和排序。这两个功能能让你更灵活地处理列表数据。
列表切片:精准截取子集
切片(Slicing)就像切蛋糕一样,可以从列表中截取一部分。语法是 列表[开始索引:结束索引:步长],其中:
- 开始索引:包含(从0开始)
- 结束索引:不包含(到结束索引-1)
- 步长:每隔几个元素取一个(默认为1)
python
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 基础切片
print(numbers[2:5]) # [2, 3, 4] # 索引2到4(不包含5)
print(numbers[:3]) # [0, 1, 2] # 从头到索引2
print(numbers[5:]) # [5, 6, 7, 8, 9] # 从索引5到末尾
print(numbers[:]) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 复制整个列表
# 负数索引切片
print(numbers[-3:]) # [7, 8, 9] # 最后3个元素
print(numbers[:-2]) # [0, 1, 2, 3, 4, 5, 6, 7] # 除了最后2个
# 带步长的切片
print(numbers[::2]) # [0, 2, 4, 6, 8] # 每隔一个取一个
print(numbers[1::2]) # [1, 3, 5, 7, 9] # 从索引1开始,每隔一个取一个
print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] # 反转列表
# 更复杂的切片
print(numbers[1:8:2]) # [1, 3, 5, 7] # 索引1到7,每隔一个取一个
切片的应用场景:
- 获取前N个/后N个元素 :
fruits[:3]获取前3个水果 - 跳过首尾元素 :
data[1:-1]去掉第一个和最后一个 - 反转列表 :
reversed_list = original[::-1] - 提取偶数/奇数位置元素 :
even = numbers[::2],odd = numbers[1::2]
列表排序:让数据井然有序
Python 提供了两种主要的排序方式:sorted() 函数和 .sort() 方法。它们看起来很相似,但有重要区别:
python
# 原始数据
scores = [88, 92, 78, 95, 85, 90]
names = ["张三", "李四", "王五", "赵六"]
# 方法1:sorted() 函数 - 返回新列表,不修改原列表
sorted_scores = sorted(scores) # 升序排序
print("sorted() 返回的新列表:", sorted_scores) # [78, 85, 88, 90, 92, 95]
print("原列表未被修改:", scores) # [88, 92, 78, 95, 85, 90]
# 方法2:.sort() 方法 - 原地修改原列表
scores.sort() # 升序排序
print(".sort() 修改后的原列表:", scores) # [78, 85, 88, 90, 92, 95]
# 降序排序
scores = [88, 92, 78, 95, 85, 90]
sorted_desc = sorted(scores, reverse=True) # 降序
print("降序排序:", sorted_desc) # [95, 92, 90, 88, 85, 78]
scores.sort(reverse=True) # 原地降序排序
print("原地降序:", scores) # [95, 92, 90, 88, 85, 78]
sorted() 与 .sort() 的核心区别:
| 特性 | sorted() 函数 |
.sort() 方法 |
|---|---|---|
| 返回值 | 返回新列表,原列表不变 | 返回 None,原地修改原列表 |
| 原列表 | 保持不变 | 被直接修改 |
| 使用方式 | 新列表 = sorted(原列表) |
原列表.sort() |
| 适用场景 | 需要保留原列表,或对不可变序列排序 | 直接修改原列表,节省内存 |
高级排序技巧
1. 按字符串长度排序
python
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
# 按长度升序排序
sorted_by_length = sorted(fruits, key=len)
print("按长度排序:", sorted_by_length) # ['date', 'apple', 'banana', 'cherry', 'elderberry']
# 原地按长度降序排序
fruits.sort(key=len, reverse=True)
print("原地按长度降序:", fruits) # ['elderberry', 'banana', 'cherry', 'apple', 'date']
2. 按自定义规则排序
python
students = [
{"name": "张三", "score": 85, "age": 18},
{"name": "李四", "score": 92, "age": 19},
{"name": "王五", "score": 78, "age": 17},
{"name": "赵六", "score": 92, "age": 18}
]
# 按分数降序,分数相同时按年龄升序
sorted_students = sorted(students, key=lambda x: (-x["score"], x["age"]))
for student in sorted_students:
print(f"{student['name']}: 分数{student['score']}, 年龄{student['age']}")
# 李四: 分数92, 年龄19
# 赵六: 分数92, 年龄18
# 张三: 分数85, 年龄18
# 王五: 分数78, 年龄17
3. 切片与排序结合使用
python
# 获取分数最高的3名学生
top_3 = sorted(students, key=lambda x: x["score"], reverse=True)[:3]
print("前三名:", [s["name"] for s in top_3]) # ['李四', '赵六', '张三']
# 获取中间50%的数据(去掉前25%和后25%)
data = list(range(100)) # 0-99
n = len(data)
middle = data[n//4 : -n//4] # 索引25到74
print("中间50%的数据:", middle[:5], "...", middle[-5:]) # [25, 26, 27, 28, 29] ... [70, 71, 72, 73, 74]
实用小贴士
- 切片不会修改原列表:切片操作总是返回新列表,原列表保持不变。
- 负步长实现反转 :
[::-1]是最简洁的反转列表方法。 - 排序稳定性:Python 的排序是稳定的,相等元素的相对顺序保持不变。
- 性能考虑 :对于大数据集,
sorted()需要额外内存,.sort()更节省内存但会修改原数据。 - 复杂排序 :使用
key参数可以指定排序规则,lambda函数非常方便。
python
# 综合示例:处理学生成绩
grades = [85, 92, 78, 95, 88, 90, 76, 85]
# 1. 获取前3名成绩
top_3 = sorted(grades, reverse=True)[:3]
print(f"前三名成绩: {top_3}") # [95, 92, 90]
# 2. 获取去掉最高分和最低分后的平均分
trimmed = sorted(grades)[1:-1] # 去掉最低和最高
average = sum(trimmed) / len(trimmed)
print(f"去掉极端值后的平均分: {average:.1f}") # 86.7
# 3. 每隔一个学生取一个成绩(模拟分组)
every_other = grades[::2]
print(f"奇数位置学生成绩: {every_other}") # [85, 78, 88, 76]
掌握了切片和排序,你就能更高效地处理列表数据。切片让你能精准提取需要的部分,而排序则让数据变得井然有序。在实际项目中,这两个操作经常结合使用,比如先排序再取前N个,或者先切片再对子集排序。
常见错误与排查
新手在使用列表时,经常会遇到以下几个典型错误:
1. 索引越界(IndexError)
错误示例:
python
fruits = ["苹果", "香蕉", "橙子"]
print(fruits[3]) # 报错:IndexError: list index out of range
问题分析: 列表索引从 0 开始,长度为 3 的列表有效索引是 0、1、2。访问 fruits[3] 试图访问第 4 个元素,但列表只有 3 个元素。
修正方案:
python
fruits = ["苹果", "香蕉", "橙子"]
# 方法1:先检查长度
if len(fruits) > 3:
print(fruits[3])
else:
print(f"列表只有 {len(fruits)} 个元素,最大索引是 {len(fruits)-1}")
# 方法2:使用安全访问(Python 3.10+)
# print(fruits[3] if len(fruits) > 3 else "索引超出范围")
2. 遍历时修改列表
错误示例:
python
numbers = [1, 2, 3, 4, 5]
for num in numbers:
if num % 2 == 0: # 如果是偶数
numbers.remove(num) # 删除该元素
print(numbers) # 预期:[1, 3, 5],实际可能是:[1, 3, 4, 5]
问题分析: 在遍历列表的同时修改它(删除或插入元素),会导致迭代器混乱,可能跳过某些元素或产生意外结果。
修正方案:
python
numbers = [1, 2, 3, 4, 5]
# 方法1:创建新列表
filtered_numbers = []
for num in numbers:
if num % 2 != 0: # 只保留奇数
filtered_numbers.append(num)
print(filtered_numbers) # 输出:[1, 3, 5]
# 方法2:使用列表推导式(更简洁)
filtered_numbers = [num for num in numbers if num % 2 != 0]
print(filtered_numbers) # 输出:[1, 3, 5]
# 方法3:如果需要原地修改,可以遍历副本
numbers = [1, 2, 3, 4, 5]
for num in numbers[:]: # numbers[:] 创建副本
if num % 2 == 0:
numbers.remove(num)
print(numbers) # 输出:[1, 3, 5]
3. 混淆 append() 和 extend()
错误示例:
python
list1 = [1, 2, 3]
list2 = [4, 5, 6]
list1.append(list2)
print(list1) # 输出:[1, 2, 3, [4, 5, 6]],不是期望的 [1, 2, 3, 4, 5, 6]
问题分析: append() 将整个列表作为一个元素添加,而 extend() 将列表中的每个元素逐个添加。
修正方案:
python
list1 = [1, 2, 3]
list2 = [4, 5, 6]
# 正确使用 extend()
list1.extend(list2)
print(list1) # 输出:[1, 2, 3, 4, 5, 6]
# 或者使用 + 运算符
list1 = [1, 2, 3]
list1 = list1 + list2 # 或 list1 += list2
print(list1) # 输出:[1, 2, 3, 4, 5, 6]
排查技巧:
- 遇到
IndexError时,先用len()检查列表长度 - 需要修改列表时,考虑先创建新列表或遍历副本
- 不确定方法效果时,在交互式环境(如 Python REPL)中先小规模测试
之前我们学过 for 循环,它和列表简直是天生一对。你不需要关心索引是多少,只需要告诉 Python:"把列表里的每个东西都拿出来做一遍"。
python
scores = [85, 92, 78, 90, 65]
# 基础遍历
for score in scores:
print(f"当前分数:{score}")
# 结合条件判断:筛选出及格分数
print("--- 及格名单 ---")
for score in scores:
if score >= 60:
print(f"{score} 分 - 及格")
else:
print(f"{score} 分 - 不及格,需要补考")
这种写法不仅代码简洁,而且无论列表里有 5 个数据还是 5 万个数据,代码都不需要改动。这就是批量管理的魅力。
字典:智能的键值对储物柜
如果说列表是靠"排队序号"来找人,那么字典(Dictionary)就是靠"名字标签"来找人。在现实生活中,你想找张三的电话,不会去翻电话簿的第 108 页(索引),而是直接查"张三"这个名字。
字典由键 (Key)和值 (Value)组成,写成 key: value 的形式,整体用花括号 {} 包裹。
- 键(Key):必须是唯一的,且不可变(通常是字符串或数字),相当于储物柜的钥匙或标签。
- 值(Value):可以是任何数据类型,相当于柜子里存的东西。
创建与访问:一键直达
python
### 完整的字典遍历方法
除了 `.items()`,字典还提供了 `.keys()` 和 `.values()` 方法,分别用于遍历键和值。下面通过一个完整的示例来展示这三种方法的使用场景:
```python
# 创建一个学生信息字典
student_info = {
"2023001": {"name": "张三", "age": 18, "score": 85},
"2023002": {"name": "李四", "age": 19, "score": 92},
"2023003": {"name": "王五", "age": 17, "score": 78},
"2023004": {"name": "赵六", "age": 20, "score": 88}
}
print("=== 方法1:遍历所有键(.keys())===")
# 适用场景:只需要键,不需要值
for student_id in student_info.keys():
print(f"学号:{student_id}")
print("\n=== 方法2:遍历所有值(.values())===")
# 适用场景:只需要值,不需要键
for info in student_info.values():
print(f"姓名:{info['name']},年龄:{info['age']},分数:{info['score']}")
print("\n=== 方法3:同时遍历键和值(.items())===")
# 适用场景:需要同时使用键和值(最常用)
for student_id, info in student_info.items():
print(f"学号:{student_id},姓名:{info['name']},分数:{info['score']}")
print("\n=== 方法4:直接遍历字典(默认遍历键)===")
# 直接遍历字典等价于遍历 .keys()
for student_id in student_info:
print(f"学号:{student_id},姓名:{student_info[student_id]['name']}")
输出结果:
=== 方法1:遍历所有键(.keys())===
学号:2023001
学号:2023002
学号:2023003
学号:2023004
=== 方法2:遍历所有值(.values())===
姓名:张三,年龄:18,分数:85
姓名:李四,年龄:19,分数:92
姓名:王五,年龄:17,分数:78
姓名:赵六,年龄:20,分数:88
=== 方法3:同时遍历键和值(.items())===
学号:2023001,姓名:张三,分数:85
学号:2023002,姓名:李四,分数:92
学号:2023003,姓名:王五,分数:78
学号:2023004,姓名:赵六,分数:88
=== 方法4:直接遍历字典(默认遍历键)===
学号:2023001,姓名:张三
学号:2023002,姓名:李四
学号:2023003,姓名:王五
学号:2023004,姓名:赵六
三种遍历方法的适用场景
| 方法 | 返回值 | 适用场景 | 示例 |
|---|---|---|---|
.keys() |
所有键的视图 | 只需要键,不需要值 检查某个键是否存在 获取所有键的列表 | for key in my_dict.keys(): if "name" in my_dict.keys(): key_list = list(my_dict.keys()) |
.values() |
所有值的视图 | 只需要值,不需要键 对值进行统计、计算 提取所有值进行处理 | for value in my_dict.values(): total = sum(my_dict.values()) value_list = list(my_dict.values()) |
.items() |
(键, 值) 元组的视图 | 需要同时使用键和值(最常用) 基于键值对进行逻辑判断 需要同时修改键和值 | for key, value in my_dict.items(): if value > 60: print(key) new_dict = {k: v*2 for k, v in my_dict.items()} |
实际应用示例
场景1:统计学生成绩(使用 .values())
python
# 只关心分数,不关心学号
scores = student_info.values()
total_score = sum(info["score"] for info in scores)
average_score = total_score / len(student_info)
print(f"平均分:{average_score:.1f}")
场景2:查找特定条件的学生(使用 .items())
python
# 找出分数大于85分的学生
high_scorers = {}
for student_id, info in student_info.items():
if info["score"] > 85:
high_scorers[student_id] = info["name"]
print(f"高分学生:{high_scorers}")
场景3:获取所有学号列表(使用 .keys())
python
# 需要学号列表进行其他操作
student_ids = list(student_info.keys())
print(f"所有学号:{student_ids}")
性能提示
- 视图对象 :
.keys()、.values()、.items()返回的是视图对象,不是列表。它们会动态反映字典的变化。 - 转换为列表 :如果需要真正的列表,可以使用
list()转换:list(my_dict.keys()) - 内存效率:视图对象比创建真正的列表更节省内存,特别是对于大型字典。
python
# 视图对象会动态更新
my_dict = {"a": 1, "b": 2}
keys_view = my_dict.keys()
print(list(keys_view)) # ['a', 'b']
my_dict["c"] = 3 # 添加新键
print(list(keys_view)) # ['a', 'b', 'c'] 视图自动更新
掌握这三种遍历方法,能让你在处理字典数据时更加得心应手。根据具体需求选择合适的方法,可以让代码更简洁、更高效。# 定义一个表示个人信息的字典
person = {
"name": "李四",
"age": 28,
"city": "北京",
"is_member": True
}
访问值:通过键名,而不是索引
print(person"name") # 输出:李四
print(person"age") # 输出:28
修改值
person"age" = 29 # 李四过生日了
person"city" = "上海" # 李四搬家了
增加新键值对
person"job" = "工程师" # 如果键不存在,会自动创建
删除
del person"is_member" # 删除会员状态
**注意**:如果你访问一个不存在的键(如 `person["phone"]`),程序会报错。为了安全起见,推荐使用 `.get()` 方法,它可以在键不存在时返回 `None` 或者你指定的默认值,而不会让程序崩溃。
```python
# 安全访问
phone = person.get("phone", "未登记")
print(phone) # 输出:未登记
遍历字典:同时获取键和值
常见错误与排查
新手在使用字典时,经常会遇到以下几个典型错误:
1. 键不存在导致 KeyError
错误示例:
python
person = {"name": "张三", "age": 25}
print(person["phone"]) # 报错:KeyError: 'phone'
问题分析: 直接使用 dict[key] 访问不存在的键会引发 KeyError 异常,导致程序崩溃。
修正方案:
python
person = {"name": "张三", "age": 25}
# 方法1:使用 get() 方法(推荐)
phone = person.get("phone") # 不存在时返回 None
print(f"电话:{phone if phone else '未登记'}")
# 方法2:get() 方法带默认值
phone = person.get("phone", "未登记")
print(f"电话:{phone}") # 输出:电话:未登记
# 方法3:使用 in 关键字先检查
if "phone" in person:
print(f"电话:{person['phone']}")
else:
print("电话:未登记")
# 方法4:使用 setdefault() 设置默认值(同时会设置到字典)
phone = person.setdefault("phone", "未登记")
print(f"电话:{phone}") # 输出:电话:未登记,同时字典中会添加 "phone": "未登记"
2. 使用可变类型作为键
错误示例:
python
# 尝试用列表作为键(会报错)
key_list = [1, 2, 3]
my_dict = {key_list: "value"} # 报错:TypeError: unhashable type: 'list'
问题分析: 字典的键必须是不可变类型(immutable),因为字典需要根据键的哈希值来快速查找。列表、字典、集合等可变类型不能作为键。
修正方案:
python
# 正确:使用元组(tuple)作为键
coordinates = (10, 20) # 元组是不可变的
location_dict = {coordinates: "北京"}
print(location_dict[(10, 20)]) # 输出:北京
# 正确:使用字符串、数字、布尔值等不可变类型
student_scores = {
"张三": 85, # 字符串 ✓
1001: "优秀", # 整数 ✓
True: "通过", # 布尔值 ✓
(1, 2): "坐标" # 元组 ✓
}
# 错误:尝试用字典作为键
# nested_dict = {{"a": 1}: "value"} # 报错
3. 遍历时修改字典结构
错误示例:
python
scores = {"Alice": 85, "Bob": 92, "Charlie": 78}
for name in scores:
if scores[name] < 80:
del scores[name] # 报错:RuntimeError: dictionary changed size during iteration
问题分析: 在遍历字典的同时添加或删除键,会改变字典的大小,导致迭代器失效。
修正方案:
python
scores = {"Alice": 85, "Bob": 92, "Charlie": 78}
# 方法1:先收集要删除的键
keys_to_delete = []
for name, score in scores.items():
if score < 80:
keys_to_delete.append(name)
for key in keys_to_delete:
del scores[key]
print(scores) # 输出:{'Alice': 85, 'Bob': 92}
# 方法2:使用字典推导式创建新字典
scores = {"Alice": 85, "Bob": 92, "Charlie": 78}
scores = {name: score for name, score in scores.items() if score >= 80}
print(scores) # 输出:{'Alice': 85, 'Bob': 92}
# 方法3:遍历字典的键的副本
scores = {"Alice": 85, "Bob": 92, "Charlie": 78}
for name in list(scores.keys()): # list() 创建键的副本
if scores[name] < 80:
del scores[name]
print(scores) # 输出:{'Alice': 85, 'Bob': 92}
排查技巧:
- 访问不确定是否存在的键时,优先使用
get()方法 - 确保字典的键是不可变类型(字符串、数字、元组)
- 需要修改字典结构时,先收集要修改的内容,遍历完成后再统一处理
- 使用
print()输出字典内容,或用type()检查键的类型
有时候我们需要同时知道"是谁"以及"他的信息是什么"。这时可以使用 .items() 方法,它会返回一个个 (键,值) 的元组。
python
user_scores = {"Alice": 95, "Bob": 88, "Charlie": 72}
# 同时遍历键和值
for name, score in user_scores.items():
if score > 90:
level = "优秀"
elif score > 60:
level = "合格"
else:
level = "待努力"
print(f"学员 {name} 得分 {score},评级:{level}")
这段代码完美融合了第三课学到的 if-elif-else 逻辑和 for 循环,展示了如何用字典管理结构化数据。
实战演练:打造简易通讯录
光看不练假把式。现在,我们将列表、字典、循环和判断结合起来,做一个经典的命令行简易通讯录。
需求分析:
- 需要一个容器存储所有联系人 -> 列表。
- 每个联系人有多个属性(姓名、电话、邮箱) -> 字典。
- 程序需要一直运行,直到用户选择退出 ->
while循环。 - 用户通过输入数字选择功能 ->
if-elif判断。
代码实现
运行效果演示
下面是一个模拟运行该通讯录程序的终端交互过程,展示了添加联系人、查看所有联系人和查找联系人的完整流程:
bash
=== 简易通讯录管理系统 ===
1. 添加联系人
2. 查看所有联系人
3. 查找联系人
4. 退出系统
========================
请输入功能编号 (1-4): 1
姓名:张三
电话:13800138000
邮箱:zhangsan@example.com
✓ 成功添加联系人:张三
=== 简易通讯录管理系统 ===
1. 添加联系人
2. 查看所有联系人
3. 查找联系人
4. 退出系统
========================
请输入功能编号 (1-4): 1
姓名:李四
电话:13900139000
邮箱:lisi@example.com
✓ 成功添加联系人:李四
=== 简易通讯录管理系统 ===
1. 添加联系人
2. 查看所有联系人
3. 查找联系人
4. 退出系统
========================
请输入功能编号 (1-4): 2
共有 2 位联系人:
1. 姓名:张三, 电话:13800138000, 邮箱:zhangsan@example.com
2. 姓名:李四, 电话:13900139000, 邮箱:lisi@example.com
=== 简易通讯录管理系统 ===
1. 添加联系人
2. 查看所有联系人
3. 查找联系人
4. 退出系统
========================
请输入功能编号 (1-4): 3
请输入要查找的姓名:张三
找到啦!
姓名:张三
电话:13800138000
邮箱:zhangsan@example.com
=== 简易通讯录管理系统 ===
1. 添加联系人
2. 查看所有联系人
3. 查找联系人
4. 退出系统
========================
请输入功能编号 (1-4): 3
请输入要查找的姓名:王五
抱歉,没有找到名为 '王五' 的联系人。
=== 简易通讯录管理系统 ===
1. 添加联系人
2. 查看所有联系人
3. 查找联系人
4. 退出系统
========================
请输入功能编号 (1-4): 4
感谢使用,再见!
交互过程解读:
- 添加联系人 :用户输入
1进入添加模式,依次输入姓名、电话、邮箱,程序会将这些信息打包成字典并添加到contacts列表中。 - 查看所有联系人 :用户输入
2,程序会先检查列表是否为空,如果不为空则使用enumerate遍历所有联系人并格式化输出。 - 查找联系人 :用户输入
3并输入要查找的姓名,程序会遍历列表中的每个字典,比较name字段,找到后立即显示详细信息并跳出循环;如果遍历完都没找到,则提示未找到。 - 退出系统 :用户输入
4,程序执行break跳出while循环,结束运行。
通过这个模拟演示,你可以清晰地看到程序如何响应用户输入,以及数据如何在列表和字典之间流动。建议你复制上面的代码到 Python 环境中实际运行,体验完整的交互过程。
python
# 初始化一个空列表,用来存放所有联系人字典
contacts = []
def show_menu():
print("\n=== 简易通讯录管理系统 ===")
print("1. 添加联系人")
print("2. 查看所有联系人")
print("3. 查找联系人")
print("4. 退出系统")
print("========================")
while True:
show_menu()
choice = input("请输入功能编号 (1-4): ")
if choice == '1':
# --- 添加联系人 ---
name = input("姓名:")
phone = input("电话:")
email = input("邮箱:")
# 将单人信息打包成字典
new_contact = {
"name": name,
"phone": phone,
"email": email
}
# 将字典添加到列表中
contacts.append(new_contact)
print(f"✓ 成功添加联系人:{name}")
elif choice == '2':
# --- 查看所有联系人 ---
if not contacts:
print("通讯录为空,先去添加几个人吧!")
else:
print(f"\n共有 {len(contacts)} 位联系人:")
# 遍历列表中的每一个字典
for index, person in enumerate(contacts, 1):
print(f"{index}. 姓名:{person['name']}, 电话:{person['phone']}, 邮箱:{person['email']}")
elif choice == '3':
# --- 查找联系人 ---
search_name = input("请输入要查找的姓名:")
found = False
for person in contacts:
if person["name"] == search_name:
print(f"\n找到啦!")
print(f"姓名:{person['name']}")
print(f"电话:{person['phone']}")
print(f"邮箱:{person['email']}")
found = True
break # 找到后就可以停止循环了
if not found:
print(f"抱歉,没有找到名为 '{search_name}' 的联系人。")
elif choice == '4':
print("感谢使用,再见!")
break # 跳出 while 循环,结束程序
else:
print("输入无效,请重新输入 1-4 的数字。")
代码解析
在这个小项目中,数据结构的设计思路非常清晰:
- 外层列表
contacts:就像一个文件夹,里面存了很多张"名片"。 - 内层字典
new_contact:每一张"名片"上记录了具体的字段(姓名、电话等)。 - 逻辑控制 :
while True保证了程序不会执行一次就关闭;if/elif负责分发用户的指令;for循环负责遍历数据展示或查找。
你可以试着运行这段代码,添加几个朋友的信息,然后尝试查找他们。你会发现,原本零散的变量现在被井井有条地管理起来了。无论你要存 10 个人还是 1000 个人,代码结构完全不需要改变,这就是数据结构带来的扩展性。
课后思考题
光看理论还不够,动手实践才能真正掌握列表和字典的精髓。以下是几个编程练习题,建议你尝试独立完成,遇到问题时可以回顾本文中的「常见错误与排查」部分。
练习题 1:学生成绩管理系统
需求描述:
你需要管理一个班级的学生成绩,每位学生有学号、姓名、语文成绩、数学成绩、英语成绩。
任务要求:
- 数据结构设计:思考你会选择列表还是字典来存储所有学生信息?为什么?
- 字典设计:如果使用字典,键应该是什么?值应该是什么结构?
- 功能实现 :
- 实现按学号快速查找某位学生的所有成绩
- 计算全班语文成绩的平均分
- 找出数学成绩最高的学生
- 统计各科目不及格(<60分)的学生人数
示例数据结构参考:
python
# 方案1:列表存储字典
students = [
{"id": "2023001", "name": "张三", "chinese": 85, "math": 92, "english": 78},
{"id": "2023002", "name": "李四", "chinese": 76, "math": 88, "english": 90},
# ... 更多学生
]
# 方案2:字典嵌套字典
students_dict = {
"2023001": {"name": "张三", "chinese": 85, "math": 92, "english": 78},
"2023002": {"name": "李四", "chinese": 76, "math": 88, "english": 90},
# ... 更多学生
}
练习题 2:简易购物车系统
需求描述:
设计一个简易的购物车系统,包含商品管理和购物车功能。
任务要求:
-
商品管理:
- 商品信息包括:商品ID、名称、单价、库存
- 使用合适的数据结构存储商品信息
-
购物车功能:
- 购物车需要记录:商品ID、购买数量
- 实现添加商品到购物车
- 从购物车移除商品
- 修改商品购买数量
- 计算购物车总价(考虑库存限制)
-
用户交互:
- 显示所有商品列表
- 显示购物车内容
- 结算时检查库存是否充足
示例代码框架:
python
# 商品数据
products = [
{"id": "P001", "name": "Python编程书", "price": 59.9, "stock": 50},
{"id": "P002", "name": "无线鼠标", "price": 129.0, "stock": 30},
{"id": "P003", "name": "USB-C数据线", "price": 29.9, "stock": 100},
]
# 购物车
cart = {} # 格式:{"商品ID": 购买数量}
def show_products():
"""显示所有商品"""
pass
def add_to_cart(product_id, quantity):
"""添加商品到购物车"""
pass
def calculate_total():
"""计算购物车总价"""
pass
练习题 3:单词频率统计器
需求描述:
编写一个程序,统计一段文本中每个单词出现的次数。
任务要求:
- 输入处理:从用户获取一段文本输入
- 文本清洗:去除标点符号,统一转为小写
- 单词分割:按空格分割成单词列表
- 频率统计:使用字典统计每个单词出现的次数
- 结果展示:按出现次数从高到低排序显示
示例输入输出:
输入文本:Hello world! Hello Python. Python is great, world is big.
统计结果:
hello: 2
world: 2
python: 2
is: 2
great: 1
big: 1
提示:
- 使用字符串的
.lower()方法转为小写 - 使用字符串的
.replace()或正则表达式去除标点 - 使用字典的
.get()方法进行计数 - 使用
sorted()函数对字典按值排序
动手实践建议
- 分步实现:不要试图一次性完成所有功能,先实现核心数据结构,再逐步添加功能
- 测试驱动:为每个功能编写简单的测试用例,验证代码是否正确
- 调试技巧 :
- 使用
print()输出中间结果 - 使用
type()检查变量类型 - 使用
len()检查数据结构大小
- 使用
- 代码优化:完成基本功能后,思考如何优化代码结构,提高可读性
遇到困难时:
- 回顾本文中的「常见错误与排查」部分
- 将大问题拆解成小问题逐个解决
- 在 Python 交互式环境中测试单个语句的效果
完成这些练习后,你将能够:
- 根据实际需求选择合适的数据结构
- 熟练使用列表和字典的组合解决实际问题
- 理解数据在程序中的流动和处理过程
- 为后续学习文件操作、函数等更高级概念打下基础
迈向下一步
总结与回顾
通过本课的学习,你已经掌握了 Python 中两个最核心的数据结构:列表和字典。它们是你从处理单个数据迈向批量数据管理的关键工具。让我们最后再回顾一下它们的核心区别与适用场景:
核心区别与适用场景
为了帮助你更直观地理解列表和字典的区别,下表从多个维度进行了对比:
| 对比维度 | 列表 (List) | 字典 (Dictionary) |
|---|---|---|
| 数据结构 | 有序的序列(元素集合) | 无序的键值对集合 |
| 访问方式 | 通过**索引(位置)**访问,如 my_list[0] |
通过**键(Key)**访问,如 my_dict["name"] |
| 性能特点 | - 按索引访问:O(1)(极快) - 按值查找:O(n)(需要遍历) - 插入/删除中间元素:可能较慢 | - 按键查找:O(1)(极快) - 内存占用:通常比列表大 - 键必须是不可变类型(字符串、数字、元组) |
| 适用场景 | - 有自然顺序的数据(如时间序列、队列) - 需要频繁按位置访问或遍历 - 存储同质化数据(如所有学生姓名) | - 有唯一标识符的数据(如学号、身份证号) - 需要快速按键查找 - 存储结构化数据(如学生信息:姓名、年龄、成绩) |
| 常用操作 | - append() / insert() 添加 - remove() / pop() 删除 - [index] 修改 - in 查询是否存在 |
- dict[key] = value 添加/修改 - del dict[key] 删除 - dict.get(key, default) 安全访问 - dict.items() 遍历键值对 |
| 核心优势 | 保持元素顺序,便于按位置操作和遍历 | 按键快速查找,适合表示映射关系 |
实际应用选择指南:
详细性能对比表格
为了帮助你更全面地理解列表和字典的性能差异,下表从多个维度进行了详细对比:
| 对比维度 | 列表 (List) | 字典 (Dictionary) | 说明与建议 |
|---|---|---|---|
| 数据结构 | 有序序列,元素按插入顺序存储 | 无序键值对集合,基于哈希表实现 | 列表保持顺序,字典不保证顺序(Python 3.7+ 保持插入顺序,但不应依赖) |
| 访问方式 | 通过索引(位置)访问:my_list[0] |
通过键(Key)访问:my_dict["name"] |
列表适合按位置访问,字典适合按键查找 |
| 内存占用 | 较低,只存储元素本身 | 较高,需存储键、值、哈希表结构 | 存储相同数据量时,字典通常比列表多占用 2-3 倍内存 |
| 时间复杂度(大O表示法) | 时间复杂度表示操作耗时随数据量增长的趋势 | ||
| - 按索引/键访问 | O(1) - 极快 | O(1) - 极快 | 两者都能直接定位,速度极快 |
| - 按值查找 | O(n) - 需要遍历整个列表 | O(1) - 通过哈希表直接定位 | 字典的查找优势明显,尤其在大数据量时 |
| - 检查元素是否存在 | O(n) - 需要遍历 | O(1) - 通过哈希表 | 字典的 in 操作比列表快得多 |
| - 在末尾添加 | O(1) - 极快 | O(1) - 极快 | 两者在末尾添加都很快 |
| - 在开头/中间插入 | O(n) - 可能需要移动后续元素 | O(1) - 平均情况 | 列表插入中间元素较慢,字典插入速度稳定 |
| - 删除元素 | O(n) - 可能需要移动后续元素 | O(1) - 平均情况 | 列表删除中间元素较慢,字典删除速度快 |
| 适用场景 | 根据主要操作选择数据结构 | ||
| - 数据特征 | 有自然顺序、同质化数据 | 有唯一标识符、结构化数据 | 列表适合队列、时间序列;字典适合映射关系 |
| - 主要操作 | 频繁按位置访问、顺序遍历 | 频繁按键查找、快速检索 | 列表适合遍历,字典适合查找 |
| - 典型用例 | 学生名单、日志记录、待办事项 | 学生信息表、配置参数、缓存数据 | 根据业务需求选择 |
| 常用操作 | append(), insert(), remove(), pop(), in |
dict[key], get(), items(), keys(), values() |
掌握核心方法提高编码效率 |
| 核心优势 | 保持元素顺序,便于按位置操作和遍历 | 按键快速查找,适合表示映射关系 | 各有侧重,根据需求选择 |
| 注意事项 | 索引从0开始,注意越界;遍历时避免修改 | 键必须是不可变类型;注意键不存在时的处理 | 理解常见错误,编写健壮代码 |
性能选择指南:
- 数据量小(< 1000):性能差异不明显,按数据结构需求选择即可
- 数据量中等(1000-10万):根据主要操作类型选择
- 主要操作为遍历、按位置访问 → 选择列表
- 主要操作为查找、按键访问 → 选择字典
- 数据量大(> 10万):需要实测性能,考虑内存限制
- 内存敏感场景 → 优先考虑列表
- 查找速度优先 → 优先考虑字典
实际测试示例:
python
import time
# 创建测试数据
test_size = 100000
my_list = list(range(test_size))
my_dict = {i: i for i in range(test_size)}
# 测试查找性能
target = test_size - 1 # 查找最后一个元素
# 列表查找
start = time.time()
found = target in my_list
list_time = time.time() - start
# 字典查找
start = time.time()
found = target in my_dict
dict_time = time.time() - start
print(f"数据量: {test_size:,}")
print(f"列表查找耗时: {list_time:.6f} 秒")
print(f"字典查找耗时: {dict_time:.6f} 秒")
print(f"字典比列表快 {list_time/dict_time:.1f} 倍")
关键结论:
- 列表优势:顺序访问、内存效率、保持元素顺序
- 字典优势:快速查找、按键访问、灵活的数据结构
- 组合使用:实际项目中经常组合使用,如列表存储字典,兼顾顺序和查找
理解这些性能特征,能帮助你在不同场景下做出更明智的选择,编写出更高效的程序。
- 当数据有自然顺序 或需要频繁按位置访问 时,选择列表。
- 当数据有唯一标识符 或需要快速按键查找 时,选择字典。
- 实际项目中经常组合使用:如通讯录案例中,用列表存储多个联系人字典,既保持了顺序又方便按姓名查找。
性能与边界条件
了解列表和字典在不同操作下的性能表现,以及数据量极大时的注意事项,能帮助你编写更高效的程序。
时间复杂度对比
时间复杂度(用大 O 表示法)描述了算法执行时间随数据规模增长的变化趋势。对于列表和字典,常见操作的时间复杂度如下:
| 操作 | 列表 (List) | 字典 (Dictionary) |
|---|---|---|
| 按索引/键访问 | O(1) - 极快 | O(1) - 极快 |
| 按值查找 | O(n) - 需要遍历整个列表 | O(1) - 通过哈希表直接定位 |
| 在末尾添加 | O(1) - 极快 | O(1) - 极快 |
| 在开头/中间插入 | O(n) - 可能需要移动后续元素 | O(1) - 平均情况 |
| 删除元素 | O(n) - 可能需要移动后续元素 | O(1) - 平均情况 |
| 检查元素是否存在 | O(n) - 需要遍历 | O(1) - 通过哈希表 |
关键理解:
- 列表的优势在于按索引访问和保持顺序,但查找特定值需要遍历。
- 字典的优势在于按键查找极快,但需要额外内存维护哈希表结构。
内存占用与数据量考量
当数据量极大时(如百万级以上),除了时间复杂度,还需要关注内存占用:
-
字典的内存开销通常更大
- 字典需要维护哈希表结构,存储键的哈希值、键和值三部分信息
- 列表只存储元素本身和少量元数据
- 示例:存储 100 万个整数,字典可能比列表多占用 2-3 倍内存
-
使用
sys.getsizeof()探查内存Python 的
sys模块提供了查看对象内存占用的方法:pythonimport sys # 比较列表和字典的内存占用 my_list = [i for i in range(1000)] # 1000个整数 my_dict = {i: i for i in range(1000)} # 1000个键值对 print(f"列表内存占用: {sys.getsizeof(my_list)} 字节") print(f"字典内存占用: {sys.getsizeof(my_dict)} 字节") # 查看单个元素的内存 print(f"单个列表元素平均: {sys.getsizeof(my_list) / len(my_list):.2f} 字节") print(f"单个字典键值对平均: {sys.getsizeof(my_dict) / len(my_dict):.2f} 字节") -
大数据量下的选择建议
- 优先考虑查找速度:如果需要频繁按键查找,即使字典内存更大也值得使用
- 内存敏感场景:如果内存有限且主要操作为顺序访问,列表更合适
- 组合使用:对于超大数据集,可考虑数据库或专门的数据结构库
实际开发中的平衡
在实际项目中,通常遵循以下原则:
- 数据量小(< 1000):性能差异不明显,按数据结构需求选择即可
- 数据量中等(1000-10万):根据主要操作类型选择,列表适合遍历,字典适合查找
- 数据量大(> 10万):需要实测性能,考虑内存限制,必要时使用专业数据结构
简单性能测试示例:
python
import time
# 测试列表查找性能
big_list = list(range(1000000))
start = time.time()
999999 in big_list # 查找最后一个元素
list_time = time.time() - start
# 测试字典查找性能
big_dict = {i: i for i in range(1000000)}
start = time.time()
999999 in big_dict # 查找最后一个键
dict_time = time.time() - start
print(f"列表查找耗时: {list_time:.6f} 秒")
print(f"字典查找耗时: {dict_time:.6f} 秒")
记住:没有绝对的好坏,只有适合的场景。理解这些性能特征,能帮助你在不同场景下做出更明智的选择。
课后思考题
为了巩固所学知识,建议你尝试完成以下练习:
-
学生成绩管理系统设计
假设你需要管理一个班级的学生成绩,每位学生有学号、姓名、语文成绩、数学成绩、英语成绩。请思考:
- 你会选择列表还是字典来存储所有学生信息?为什么?
- 如果要用字典,键应该是什么?值应该是什么结构?
- 如何实现按学号快速查找某位学生的所有成绩?
- 如何计算全班语文成绩的平均分?
-
购物车功能模拟
设计一个简易的购物车系统:
- 商品信息包括:商品ID、名称、单价、库存
- 购物车需要记录:商品ID、购买数量
- 请思考如何用列表和字典组合实现商品展示、加入购物车、计算总价等功能
动手实践建议 :先在白纸或注释中写下你的设计思路,然后用代码实现核心功能。遇到问题时,回顾本文中的「常见错误与排查」部分,使用 print() 调试输出中间结果。
掌握了列表和字典,你已经具备了处理大多数日常数据任务的能力。下一课我们将学习如何将这些内存中的数据持久化保存到文件中,让你的程序真正「记住」数据。准备好了吗?让我们继续前进!
掌握了列表和字典,你就拥有了处理复杂数据的能力。无论是处理 Excel 表格中的数据行(列表套字典),还是解析网络 API 返回的 JSON 格式(本质上就是字典和列表的组合),核心逻辑都是相通的。
不过,细心的你可能发现了:当我们关闭程序再次运行时,刚才辛苦输入的通讯录数据又不见了。这是因为目前所有数据都只保存在内存中。如何让这些数据永久保存下来,哪怕关掉电脑下次打开还在?这就需要引入文件操作了。在下一节课中,我们将学习如何将这些列表和字典写入到文本文件或 CSV 文件中,真正实现数据的持久化存储。