序列与集合类型
# 第 2 章 Python 序列与集合类型
# 目录介绍
# 2.1 字符串进阶操作
📖 本章定位:深入 Python 的数据容器——字符串、列表、元组、字典、集合。它们是编写任何 Python 程序的"砖块"。
📌 前章回顾:第 1 章讲了变量、类型、运算符——本章的每一个容器操作都建立在那之上。 📌 下章预告:学完容器后,第 3 章会教你如何用流程控制和函数来操作它们——让数据"动起来"。
# 2.1.1 索引与切片
Python 的字符串是序列——支持所有序列的通用操作。切片是 Python 被评价"优雅"的根源之一。
# 索引
s = "Hello, Python!"
# 正向索引(0 开始)
print(s[0]) # H
print(s[7]) # P
# 反向索引(-1 是最后一个)
print(s[-1]) # !
print(s[-3]) # o
# 越界会抛出 IndexError
# print(s[100]) # IndexError: string index out of range
2
3
4
5
6
7
8
9
10
11
12
# 切片 [start🔚step]
切片的三个参数都可以省略——这就是它优雅的秘密:
s = "Hello, Python!"
print(s[0:5]) # Hello(索引 0~4)
print(s[:5]) # Hello(省略 start = 从头开始)
print(s[7:]) # Python!(省略 end = 直到末尾)
print(s[:]) # Hello, Python!(全复制)
print(s[::2]) # Hlo yhn!(step=2,每隔一个取)
print(s[::-1]) # !nohtyP ,olleH(反转字符串!)
print(s[-6:-1]) # ython(倒数第6到倒数第1,不含-1)
2
3
4
5
6
7
8
9
🔑 切片不会抛出 IndexError——这是 Python 的"宽容哲学":
s = "hi"
print(s[5:10]) # ""(空字符串——超出范围就空,不报错)
print(s[0:100]) # "hi"(截取到实际末尾)
2
3
# 切片底层原理
s = "abcdef"
索引图(正数和负数对应):
a b c d e f
0 1 2 3 4 5 ← 正索引
-6 -5 -4 -3 -2 -1 ← 负索引
切片 s[1:4] = "bcd"
│ │
│ └─ end=4:取到索引 4 之前(即索引 3)
└─── start=1:从索引 1 开始
切片 s[-5:-2] = "bcde"
│ │
│ └─ end=-2:索引 -2(即 e)之前 → 到 d
└────── start=-5:索引 -5(即 b)开始
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.1.2 常用方法大全
s = " Hello, World! "
# ---- 查找 ----
print(s.find("World")) # 9(返回索引,找不到返回 -1)
print(s.index("World")) # 9(和 find 一样,但找不到抛 ValueError)
print(s.rfind("l")) # 11(从右往左找)
print(s.count("l")) # 3(统计出现次数)
# ---- 判断 ----
print(s.startswith(" He")) # True
print(s.endswith("! ")) # True
print("123".isdigit()) # True(全是数字)
print("abc".isalpha()) # True(全是字母)
print("abc123".isalnum()) # True(全是字母或数字)
print(" ".isspace()) # True(全是空白字符)
print("Hello".isupper()) # False
print("HELLO".islower()) # False
print("Hello World".istitle())# True(每个单词首字母大写)
# ---- 转换 ----
print(s.strip()) # "Hello, World!"(去首尾空白)
print(s.lstrip()) # "Hello, World! "(去左空白)
print(s.rstrip()) # " Hello, World!"(去右空白)
print(s.lower()) # " hello, world! "
print(s.upper()) # " HELLO, WORLD! "
print(s.capitalize()) # " hello, world! "(仅首字母大写)
print(s.title()) # " Hello, World! "(每词首字母大写)
print(s.swapcase()) # " hELLO, wORLD! "(大小写反转)
# ---- 替换与分割 ----
print(s.replace("World", "Python")) # " Hello, Python! "
print(s.replace("l", "L", 1)) # " HeLlo, World! "(只替换1次)
words = "apple,banana,orange"
print(words.split(",")) # ['apple', 'banana', 'orange']
print(words.split(",", 1)) # ['apple', 'banana,orange'](最多分割1次)
lines = "line1\nline2\nline3"
print(lines.splitlines()) # ['line1', 'line2', 'line3']
parts = ['a', 'b', 'c']
print("-".join(parts)) # "a-b-c"
print("".join(parts)) # "abc"
# ---- 对齐与填充 ----
name = "Python"
print(f"|{name:<10}|") # |Python |(左对齐,10宽度)
print(f"|{name:>10}|") # | Python|(右对齐)
print(f"|{name:^10}|") # | Python |(居中)
print(f"|{name:*^10}|") # |**Python**|(星号填充)
# str 方法也可直接调用(f-string 出现前的老方式)
print(name.ljust(10)) # "Python "
print(name.rjust(10)) # " Python"
print(name.center(10, "*")) # "**Python**"
print(name.zfill(10)) # "0000Python"(左侧填0)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 2.1.3 字符串格式化全家桶
Python 有三代格式化方案——你应该只用第三代:
name = "张三"
age = 25
height = 1.75
score = 95.678
# 第一代:% 格式化(C 风格——不推荐)
print("我叫%s,%d岁,身高%.2f" % (name, age, height))
# 第二代:str.format()(Python 2.6——也不推荐了)
print("我叫{},{}岁,身高{:.2f}".format(name, age, height))
print("我叫{0},{1}岁,{0}再次出现".format(name, age)) # 位置索引
# 第三代:f-string(Python 3.6+——唯一推荐!)
print(f"我叫{name},{age}岁,身高{height:.2f}")
# f-string 的强大功能
print(f"明年{age + 1}岁") # 内嵌表达式
print(f"名字大写:{name.upper()}") # 内嵌方法调用
print(f"分数:{score:.1f}") # 格式说明符
print(f"十六进制:{255:#x}") # 进制转换
print(f"百分比:{0.8567:.1%}") # 百分数显示
print(f"千分位:{123456789:,}") # 加逗号
print(f"填充对齐:|{name:*^20}|") # 对齐+填充+宽度
# 调试利器:直接打印变量名和值(Python 3.8+)
x, y = 10, 20
print(f"{x=}, {y=}, {x+y=}") # x=10, y=20, x+y=30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
🔑 f"{x=}" 是 Python 3.8 的隐藏神器——调试时最省力的 print。
# 2.1.4 编码与解码
编码问题是 Python 新手最常见的"玄学报错"——理解原理一次通:
# 字符串 ↔ 字节串
s = "你好,世界!"
# 编码:str → bytes
b_utf8 = s.encode("utf-8")
print(b_utf8) # b'\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8c\xe4\xb8\x96\xe7\x95\x8c\xef\xbc\x81'
print(len(b_utf8)) # 15(UTF-8 下一个中文字符 = 3 字节)
b_gbk = s.encode("gbk")
print(len(b_gbk)) # 10(GBK 下一个中文字符 = 2 字节)
# 解码:bytes → str
print(b_utf8.decode("utf-8")) # 你好,世界!
print(b_gbk.decode("gbk")) # 你好,世界!
# print(b_utf8.decode("gbk")) # 乱码或报错!编码方式必须匹配
# 错误处理策略
b_broken = b'\xff\xff'
print(b_broken.decode("utf-8", errors="ignore")) # ""(忽略无法解码的字节)
print(b_broken.decode("utf-8", errors="replace")) # "��"(替换为 �)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
编码知识速查表:
| 编码 | 英文 | 中文 | 适用场景 |
|---|---|---|---|
| ASCII | 1 字节 | ❌ 不支持 | 纯英文文本 |
| UTF-8 | 1 字节 | 3 字节 | 现代首选——Python 默认 |
| GBK | 1 字节 | 2 字节 | 国内遗留系统 |
| latin-1 | 1 字节 | ❌ | 每字节都能解码,永不出错 |
📌 金科玉律:永远用 UTF-8;文件读写时显式指定
encoding="utf-8";遇到UnicodeDecodeError时先查编码。
# 2.1.5 正则表达式初探
字符串方法能处理 80% 的场景——正则处理剩下 20%:
import re
text = "联系方式:张三 13812345678,李四 15987654321"
# 1. 提取所有手机号(1 开头的 11 位数字)
pattern = r"1[3-9]\d{9}"
phones = re.findall(pattern, text)
print(phones) # ['13812345678', '15987654321']
# 2. 查找第一个匹配
match = re.search(r"(\w+)\s+(\d+)", text)
if match:
print(match.group(0)) # 张三 13812345678(完整匹配)
print(match.group(1)) # 张三(第一个括号组)
print(match.group(2)) # 13812345678(第二个括号组)
# 3. 替换——脱敏手机号中间四位
masked = re.sub(r"(\d{3})\d{4}(\d{4})", r"\1****\2", text)
print(masked) # 联系方式:张三 138****5678,李四 159****4321
# 4. 分割
parts = re.split(r"[,,]\s*", text) # 按逗号分割(中英文逗号都行)
print(parts) # ['联系方式:张三 13812345678', '李四 15987654321']
# 5. 验证格式
def is_valid_email(s: str) -> bool:
return bool(re.match(r"^[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,}$", s))
print(is_valid_email("test@example.com")) # True
print(is_valid_email("not-an-email")) # False
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
常用正则速查:
| 模式 | 含义 | 示例 |
|---|---|---|
\d | 数字 | \d{11} → 11 位数字 |
\w | 字母/数字/下划线 | \w+ → 一个单词 |
\s | 空白字符 | \s* → 0 或多个空白 |
. | 任意字符(除换行) | a.b → a 任意字符 b |
+ | 1 次或多次 | \d+ → 至少一个数字 |
* | 0 次或多次 | \s* → 可选空白 |
? | 0 次或 1 次 | colou?r → color 或 colour |
[] | 字符集 | [aeiou] → 任意元音 |
() | 捕获组 | (\d{3})-(\d{4}) → 两个组 |
^ $ | 开始 / 结束 | ^\d+$ → 纯数字字符串 |
(?P<name>...) | 命名组 | (?P<year>\d{4}) → 组名叫 year |
# 2.1.6 综合案例与思考
综合案例:日志解析器——字符串方法全家桶
"""
日志解析器——展示字符串查找、分割、正则、格式化的综合运用
"""
# 模拟 Nginx 访问日志
log_lines = [
'192.168.1.1 - - [08/Jun/2025:14:30:00 +0800] "GET /api/users HTTP/1.1" 200 1234',
'10.0.0.5 - admin [08/Jun/2025:14:30:01 +0800] "POST /api/login HTTP/1.1" 401 88',
'192.168.1.2 - - [08/Jun/2025:14:30:02 +0800] "GET /api/products?id=42 HTTP/1.1" 200 5678',
'172.16.0.1 - - [08/Jun/2025:14:30:03 +0800] "GET /static/style.css HTTP/1.1" 304 0',
'192.168.1.1 - - [08/Jun/2025:14:30:04 +0800] "PUT /api/users/3 HTTP/1.1" 500 234',
]
import re
from collections import Counter
ip_counts = Counter()
status_codes = Counter()
total_bytes = 0
api_calls = []
for line in log_lines:
# 1. 提取 IP(按空格分割取第一段)
ip = line.split()[0]
ip_counts[ip] += 1
# 2. 提取状态码(正则:数字 + 空格 + 数字 在行末附近)
match = re.search(r'" (\d{3}) (\d+)', line)
if match:
status = int(match.group(1))
body_bytes = int(match.group(2))
status_codes[status] += 1
total_bytes += body_bytes
# 3. 提取 API 路径(正则取引号内的请求行)
req_match = re.search(r'"(GET|POST|PUT|DELETE) (/[\w/-]*)', line)
if req_match:
method, path = req_match.group(1), req_match.group(2)
if path.startswith("/api/"):
api_calls.append({"method": method, "path": path, "status": status if match else 0})
# 4. 检查是否包含错误(字符串成员检查)
if " 500 " in line or " 401 " in line:
print(f"⚠️ 异常请求:{ip} → {line}")
# 5. 格式化输出报告
print("=" * 55)
print(f"{'Nginx 日志分析报告':^55}")
print("=" * 55)
print(f"\n{'📊 IP 访问统计':-^55}")
for ip, count in ip_counts.most_common():
bar = "█" * count
print(f" {ip:<20} {count}次 {bar}")
print(f"\n{'📊 HTTP 状态码分布':-^55}")
status_names = {200: "OK", 304: "Not Modified", 401: "Unauthorized", 404: "Not Found", 500: "Server Error"}
for code, count in sorted(status_codes.items()):
pct = count / len(log_lines) * 100
name = status_names.get(code, "?")
print(f" {code} {name:<14} {count}次 ({pct:.0f}%)")
print(f"\n{'📊 API 调用详情':-^55}")
for call in api_calls:
status_icon = "✅" if call["status"] < 400 else "❌"
print(f" {status_icon} {call['method']:<6} {call['path']}")
print(f"\n总请求数:{len(log_lines)}")
print(f"总流量:{total_bytes:,} 字节")
print(f"独立 IP 数:{len(ip_counts)}")
print("=" * 55)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
运行输出:
⚠️ 异常请求:10.0.0.5 → 10.0.0.5 - admin [...] "POST /api/login HTTP/1.1" 401 88
⚠️ 异常请求:192.168.1.1 → 192.168.1.1 - - [...] "PUT /api/users/3 HTTP/1.1" 500 234
=======================================================
Nginx 日志分析报告
=======================================================
------------------📊 IP 访问统计-------------------
192.168.1.1 3次 ███
10.0.0.5 1次 █
172.16.0.1 1次 █
-----------------📊 HTTP 状态码分布-----------------
200 OK 3次 (60%)
304 Not Modified 1次 (20%)
401 Unauthorized 1次 (20%)
500 Server Error 1次 (20%)
------------------📊 API 调用详情-------------------
✅ GET /api/users
❌ POST /api/login
✅ GET /api/products
❌ PUT /api/users/3
总请求数:5
总流量:7,234 字节
独立 IP 数:3
=======================================================
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
案例知识融合:这个案例覆盖了字符串的全部核心操作——split() 分割取字段、re.search() 正则提取 + 捕获组、in 成员检查、f-string 格式化报告、Counter 统计聚合——60 行代码完成了生产级日志分析。其中 r'" (\d{3}) (\d+)' 揭示了一个核心技巧:用已知的定界符(引号)锚定正则,而非试图"模糊匹配所有字段"。
思考题:
line.split()[0]取 IP 依赖日志格式——如果某条日志格式错误(空格数不对),会发生什么?如何防御?- 正则
r'" (\d{3}) (\d+)'中的引号前导空格是什么作用?如果去掉会匹配到什么? Counter的most_common()方法内部原理是什么?如果不需要排序,dict加+=和Counter加+=谁更快?
# 2.2 列表与元组
# 2.2.1 列表基础操作
列表是 Python 最常用的可变序列——可以装任何类型,支持增删改查:
# 创建
empty = []
nums = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True, [1, 2]] # 混合类型(生产代码不推荐!)
repeated = [0] * 5 # [0, 0, 0, 0, 0]
# 索引与切片(和字符串完全一致)
fruits = ["苹果", "香蕉", "橘子", "葡萄", "西瓜"]
print(fruits[0]) # 苹果
print(fruits[-1]) # 西瓜
print(fruits[1:4]) # ['香蕉', '橘子', '葡萄']
print(fruits[::-1]) # ['西瓜', '葡萄', '橘子', '香蕉', '苹果']
# 增
fruits.append("草莓") # 末尾追加
fruits.insert(1, "芒果") # 指定位置插入
fruits.extend(["梨", "桃"]) # 扩展另一个可迭代对象
print(fruits)
# ['苹果', '芒果', '香蕉', '橘子', '葡萄', '西瓜', '草莓', '梨', '桃']
# 删
del fruits[1] # 按索引删除
popped = fruits.pop() # 弹出末尾(返回被删元素)
popped2 = fruits.pop(0) # 弹出指定位置
fruits.remove("橘子") # 按值删除(只删第一个匹配)
# fruits.remove("不存在") # ValueError——用前先检查
fruits.clear() # 清空整个列表
# 改
nums = [10, 20, 30]
nums[1] = 99 # 直接赋值
nums[0:2] = [1, 2, 3] # 切片替换(长度不必相同!)
print(nums) # [1, 2, 3, 30]——原来的 [10,20] 被 [1,2,3] 替换
# 查
print(30 in nums) # True(成员检查)
print(nums.index(30)) # 3(返回索引,找不到抛 ValueError)
print(nums.count(2)) # 1(统计出现次数)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 2.2.2 列表推导式
列表推导式是 Python 最标志性的语法糖——一行代替 for 循环:
# 基本形式:[expression for item in iterable if condition]
# 例 1:生成 1~10 的平方
squares = [x ** 2 for x in range(1, 11)]
print(squares) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# 例 2:过滤——取偶数平方
even_squares = [x ** 2 for x in range(1, 11) if x % 2 == 0]
print(even_squares) # [4, 16, 36, 64, 100]
# 例 3:嵌套循环——矩阵展平
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 例 4:条件表达式(if-else 的三元形式)
labels = ["偶数" if x % 2 == 0 else "奇数" for x in range(1, 6)]
print(labels) # ['奇数', '偶数', '奇数', '偶数', '奇数']
# 例 5:字典/集合也有推导式
squares_dict = {x: x ** 2 for x in range(1, 6)} # {1:1, 2:4, 3:9, 4:16, 5:25}
squares_set = {x ** 2 for x in range(1, 11)} # {64, 1, 4, 36, 100, 9, 16, 81, 49, 25}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
🔑 推导式 vs 传统 for 循环:
# 传统写法(5 行)
result = []
for x in range(10):
if x % 2 == 0:
result.append(x ** 2)
# 推导式(1 行)
result = [x ** 2 for x in range(10) if x % 2 == 0]
2
3
4
5
6
7
8
可读性上推导式完胜——但当逻辑超过两层 if/循环时,就改用传统 for 循环。推导式的"一句话"优势会逆转成"一句话看不懂"。
⚠️ 列表推导式的变量泄漏(Python 2 时代的遗留问题在 Python 3 已修复):
x = 10
# Python 3:推导式有自己的作用域——外面的 x 不变
result = [x for x in range(5)]
print(x) # 10(Python 3) vs 4(Python 2——已修复!)
2
3
4
# 2.2.3 列表排序与高级切片
# 排序
nums = [3, 1, 4, 1, 5, 9, 2, 6]
# sort():原地排序(修改原列表)
nums.sort()
print(nums) # [1, 1, 2, 3, 4, 5, 6, 9]
nums.sort(reverse=True) # 降序
print(nums) # [9, 6, 5, 4, 3, 2, 1, 1]
# sorted():返回新列表(不修改原列表)
original = [3, 1, 4, 1, 5]
new_list = sorted(original) # 原列表保持不变
print(original) # [3, 1, 4, 1, 5]
print(new_list) # [1, 1, 3, 4, 5]
# key 参数——按自定义规则排序
words = ["apple", "Banana", "cherry", "Date"]
words.sort(key=str.lower) # 不区分大小写排序
print(words) # ['apple', 'Banana', 'cherry', 'Date']
pairs = [(1, "one"), (3, "three"), (2, "two")]
pairs.sort(key=lambda x: x[0]) # 按元组第一个元素排序
print(pairs) # [(1, 'one'), (2, 'two'), (3, 'three')]
# 多级排序:先按长度,再按字母
words = ["a", "ccc", "bb", "dddd"]
words.sort(key=lambda s: (len(s), s))
print(words) # ['a', 'bb', 'ccc', 'dddd']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 切片赋值和拷贝
# 切片赋值——列表独有的强大操作
nums = [0, 1, 2, 3, 4, 5]
nums[2:4] = [20, 30] # 替换中间两元素
print(nums) # [0, 1, 20, 30, 4, 5]
nums[1:1] = [99, 100] # 在位置 1 插入(end==start 时是插入)
print(nums) # [0, 99, 100, 1, 20, 30, 4, 5]
nums[2:5] = [] # 删除切片(赋空列表 = 删除)
print(nums) # [0, 99, 30, 4, 5]
# 深浅拷贝——新人最易踩坑
import copy
original = [1, 2, [3, 4]]
shallow = original.copy() # 浅拷贝:只复制最外层
deep = copy.deepcopy(original) # 深拷贝:递归复制所有层
original[2][0] = 999 # 修改内层列表
print(shallow) # [1, 2, [999, 4]]——shallow 内层被影响了!
print(deep) # [1, 2, [3, 4]]——deep 不受影响
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
图解浅拷贝 vs 深拷贝:
original ──→ [1, 2, ●]
│
└──→ [3, 4]
shallow ──→ [1, 2, ●] ← 内层指向同一个 [3, 4]!
(复制外层)
deep ──→ [1, 2, ●]
│
└──→ [3, 4] ← 内层是独立副本
(递归复制)
2
3
4
5
6
7
8
9
10
11
# 2.2.4 元组:不可变的列表
元组和列表的唯一区别:元组不可变——一旦创建,内容不能改:
# 创建
t1 = (1, 2, 3)
t2 = 1, 2, 3 # 括号可以省略
t3 = (1,) # 单元素元组——逗号是灵魂!(1) 是整数 1
t4 = () # 空元组
# 不可变性
# t1[0] = 10 # TypeError! 元组不支持修改
# t1.append(4) # AttributeError! 元组没有 append
# 但"可变元素"内部可变(元组本身不变,被指向的对象可变)
t5 = (1, 2, [3, 4])
t5[2].append(5)
print(t5) # (1, 2, [3, 4, 5])——列表变了,但元组结构没变
# Packing & Unpacking(打包与解包)
a, b, c = (10, 20, 30) # 解包——赋给三个变量
print(a, b, c) # 10 20 30
# 星号解包(Python 3 特色)
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]——中间所有元素
print(last) # 5
# 交换变量——Python 最优雅的操作之一
x, y = 10, 20
x, y = y, x # 背后是元组打包再解包:(y, x) → 元组 → 解包
print(x, y) # 20 10
# 函数多值返回——本质是返回元组
def min_max(arr):
return min(arr), max(arr) # 返回元组 (min_val, max_val)
mi, ma = min_max([3, 1, 4, 1, 5]) # 调用处自动解包
print(mi, ma) # 1 5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
🔑 列表 vs 元组:什么时候用哪个?
| 场景 | 选择 | 原因 |
|---|---|---|
| 装固定结构的数据(坐标、RGB) | 元组 | 不可变 = 安全 |
| 函数返回多值 | 元组 | Python 惯例——return a, b 返回的就是元组 |
| 字典的 key | 元组 | dict key 必须可哈希——列表不可哈希 |
| 需要增删改的数据集合 | 列表 | 元组不可变 |
| 作为函数的默认参数 | 元组 | def f(items=()) 比 items=[] 安全(见 §2.4 新手陷阱) |
# 2.2.5 序列通用操作
字符串、列表、元组都是序列——它们共享一套通用操作:
# 以下操作对 str / list / tuple 都通用
s = "hello"
L = [1, 2, 3, 4, 5]
T = (10, 20, 30)
# len():获取长度
print(len(s), len(L), len(T)) # 5 5 3
# 索引和切片(全部适用)
print(s[0], L[-1], T[1:]) # h 5 (20, 30)
# in / not in
print("h" in s, 6 not in L) # True True
# + 拼接
print([1, 2] + [3, 4]) # [1, 2, 3, 4]
print((1, 2) + (3, 4)) # (1, 2, 3, 4)
print("ab" + "cd") # "abcd"
# * 重复
print([0] * 5) # [0, 0, 0, 0, 0]
# min / max / sum(元素必须可比较)
print(max(L), min(L), sum(L)) # 5 1 15
# enumerate():同时取索引和值
for i, val in enumerate(["a", "b", "c"], start=1):
print(f"{i}: {val}")
# 1: a
# 2: b
# 3: c
# zip():并行遍历多个序列
names = ["张三", "李四", "王五"]
scores = [85, 92, 78]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# 张三: 85
# 李四: 92
# 王五: 78
# range():生成整数序列
print(list(range(5))) # [0, 1, 2, 3, 4]
print(list(range(2, 10, 3))) # [2, 5, 8]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 2.2.6 综合案例与思考
综合案例:学生成绩管理系统——列表/元组/排序全家桶
"""
学生成绩管理系统——列表、元组、推导式、排序的综合运用
"""
# 1. 原始数据:列表 of 元组(学号, 姓名, 语, 数, 英)
students = [
(2024001, "张三", 85, 92, 78),
(2024002, "李四", 92, 88, 95),
(2024003, "王五", 78, 85, 82),
(2024004, "赵六", 65, 70, 68),
(2024005, "孙七", 95, 91, 88),
]
# 2. 计算每个人的总分和平均分(推导式)
scored_students = [
(sid, name, ch, ma, en, ch + ma + en, round((ch + ma + en) / 3, 1))
for sid, name, ch, ma, en in students
]
# 3. 排名:按总分降序、总分相同按学号升序
scored_students.sort(key=lambda x: (-x[5], x[0]))
# 4. 统计信息
all_scores = [s for _, _, ch, ma, en, *_ in scored_students for s in (ch, ma, en)]
avg_total = sum(all_scores) / len(all_scores)
avg_per_subject = { # 字典推导式
subject: round(sum(scores) / len(scores), 1)
for subject, idx in [("语文", 2), ("数学", 3), ("英语", 4)]
for scores in [[s[idx] for s in scored_students]]
}
passed_all = [s[1] for s in scored_students if min(s[2:5]) >= 60]
failed_any = [s[1] for s in scored_students if min(s[2:5]) < 60]
# 5. 格式化输出
print("=" * 60)
print(f"{'学生成绩总览':^60}")
print("=" * 60)
print(f"{'排名':<4} {'学号':<10} {'姓名':<6} {'语文':<6} {'数学':<6} {'英语':<6} {'总分':<6} {'平均':<6}")
print("-" * 60)
for rank, (sid, name, ch, ma, en, total, avg) in enumerate(scored_students, 1):
medal = "🥇" if rank == 1 else "🥈" if rank == 2 else "🥉" if rank == 3 else f"{rank:2d}"
print(f"{medal:<4} {sid:<10} {name:<6} {ch:<6} {ma:<6} {en:<6} {total:<6} {avg:<6}")
print("=" * 60)
print(f"\n📊 统计数据:")
print(f" 总平均分:{avg_total:.1f}")
print(f" 各科平均:语文 {avg_per_subject['语文']} | 数学 {avg_per_subject['数学']} | 英语 {avg_per_subject['英语']}")
print(f" 全科及格:{len(passed_all)} 人 → {passed_all}")
print(f" 有挂科:{len(failed_any)} 人 → {failed_any if failed_any else '无'}")
# 6. 各区段人数(切片 + 统计)
sorted_totals = sorted([s[5] for s in scored_students], reverse=True)
segments = {"优秀 (≥270)": 0, "良好 (240-269)": 0, "合格 (180-239)": 0, "待提升 (<180)": 0}
for t in sorted_totals:
if t >= 270: segments["优秀 (≥270)"] += 1
elif t >= 240: segments["良好 (240-269)"] += 1
elif t >= 180: segments["合格 (180-239)"] += 1
else: segments["待提升 (<180)"] += 1
print(f"\n📊 分数段分布:")
for label, count in segments.items():
bar = "█" * count + "░" * (len(students) - count)
print(f" {label:<20} {count}人 {bar}")
print("=" * 60)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
运行输出:
============================================================
学生成绩总览
============================================================
排名 学号 姓名 语文 数学 英语 总分 平均
------------------------------------------------------------
🥇 2024005 孙七 95 91 88 274 91.3
🥈 2024002 李四 92 88 95 275 91.7
3 2024001 张三 85 92 78 255 85.0
4 2024003 王五 78 85 82 245 81.7
5 2024004 赵六 65 70 68 203 67.7
============================================================
📊 统计数据:
总平均分:82.8
各科平均:语文 83.0 | 数学 85.2 | 英语 82.2
全科及格:5 人 → ['孙七', '李四', '张三', '王五', '赵六']
有挂科:0 人 → 无
📊 分数段分布:
优秀 (≥270) 2人 ██░░░
良好 (240-269) 2人 ██░░░
合格 (180-239) 1人 █░░░░
待提升 (<180) 0人 ░░░░░
============================================================
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
案例知识融合:这个案例在真实成绩管理场景中串联了列表/元组/推导式的全部核心知识——元组存储不可变的学生记录、列表推导式计算总分、sort(key=lambda) 多级排序、字典推导式统计各科平均、enumerate 生成排名——50 行完成了一个实用的成绩管理系统。
思考题:
sorted_students.sort(key=lambda x: (-x[5], x[0]))中-x[5]为什么能实现降序?如果总分可能为负数,这个技巧还安全吗?遇到非数字类型怎么办?- 本案例用元组
(sid, name, ch, ma, en)存储学生信息——如果用列表[sid, name, ch, ma, en]有什么风险?在大项目中元组的不可变性带来了什么好处? - 推导式
[s for _, _, ch, ma, en, *_ in scored_students for s in (ch, ma, en)]中的*_表示什么?for s in (ch, ma, en)为什么写在这里?
# 2.3 字典与集合
# 2.3.1 字典基础操作
字典是 Python 的哈希表——O(1) 时间复杂度的键值对容器:
# 创建
d1 = {"name": "张三", "age": 25, "city": "深圳"}
d2 = dict(name="李四", age=30) # 关键字参数方式
d3 = dict([("a", 1), ("b", 2)]) # 可迭代对象转字典
d4 = dict.fromkeys(["x", "y", "z"], 0) # {'x': 0, 'y': 0, 'z': 0}
# 增 / 改
d1["email"] = "zhangsan@example.com" # 有则改,无则增
d1.update({"age": 26, "phone": "138xxx"}) # 批量更新
# 删
del d1["phone"] # 删除键,不存在抛出 KeyError
removed = d1.pop("city") # 弹出并返回值
default = d1.pop("nonexist", "默认值") # 不存在时返回默认值
d1.clear() # 清空
# 查
print(d1.get("name")) # "张三"(不存在返回 None)
print(d1.get("nonexist", "未知")) # "未知"(提供默认值)
print("name" in d1) # True(检查键是否存在)
# setdefault():如果有就返回,没有就设置并返回
counter = {}
for word in ["a", "b", "a", "c", "b", "a"]:
counter.setdefault(word, 0)
counter[word] += 1
# 等价于 counter[word] = counter.get(word, 0) + 1
# Python 3.7+ 字典保证插入顺序(CPython 3.6 就实现了)
d = {}
d["c"] = 3; d["a"] = 1; d["b"] = 2
print(list(d.keys())) # ['c', 'a', 'b']——按插入顺序!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 2.3.2 字典遍历与推导式
d = {"a": 1, "b": 2, "c": 3}
# 遍历键
for key in d: # 等价于 for key in d.keys()
print(key, end=" ") # a b c
# 遍历值
for val in d.values():
print(val, end=" ") # 1 2 3
# 遍历键值对
for key, val in d.items():
print(f"{key} → {val}")
# 字典推导式
squares = {x: x ** 2 for x in range(1, 6)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# 反转键值对(值不重复时)
d = {"a": 1, "b": 2, "c": 3}
reversed_d = {v: k for k, v in d.items()}
# {1: 'a', 2: 'b', 3: 'c'}
# 过滤
data = {"张三": 85, "李四": 92, "王五": 58, "赵六": 73}
passed = {name: score for name, score in data.items() if score >= 60}
# {'张三': 85, '李四': 92, '赵六': 73}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 2.3.3 集合:去重与交并差
集合是不重复、无序的哈希容器——去重和集合运算的效率之王:
# 创建
s1 = {1, 2, 3, 4, 5}
s2 = set([3, 4, 5, 6, 7]) # 从可迭代对象创建
s3 = set() # 空集合(注意:{} 是空字典!)
# 去重——集合的看家本领
nums = [1, 2, 2, 3, 3, 3, 4]
unique = list(set(nums)) # [1, 2, 3, 4](顺序不保证)
unique_sorted = sorted(set(nums)) # [1, 2, 3, 4](保序)
# 基本操作
s1.add(6) # 添加
s1.remove(6) # 删除(不存在抛 KeyError)
s1.discard(6) # 删除(不存在不报错——更安全)
s1.pop() # 随机弹出一个元素
# 集合运算
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b) # {1, 2, 3, 4, 5, 6}(并集)
print(a & b) # {3, 4}(交集)
print(a - b) # {1, 2}(差集:a 有 b 没有)
print(a ^ b) # {1, 2, 5, 6}(对称差集:只在一边的)
# 集合比较
c = {1, 2}
print(c <= a) # True(c 是 a 的子集)
print(a >= c) # True(a 是 c 的超集)
print(c < a) # True(c 是 a 的真子集——c < a 且 c ≠ a)
# 集合推导式
even_squares = {x ** 2 for x in range(10) if x % 2 == 0}
# {0, 64, 4, 36, 16}
# 实战:找两个列表的共同元素
list1 = [1, 2, 3, 4, 5]
list2 = [3, 4, 5, 6, 7]
common = list(set(list1) & set(list2)) # 比双重循环快 N 倍!
print(common) # [3, 4, 5]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 2.3.4 哈希原理与可变陷阱
🔑 为什么列表不能做字典的 key?——因为列表可变,hash 值会变:
# ✅ 可哈希的类型:int, float, str, tuple(元素全可哈希), frozenset
d = {}
d[42] = "int key"
d["name"] = "str key"
d[(1, 2)] = "tuple key" # 元组可哈希
# d[[1, 2]] = "list key" # TypeError! 列表不可哈希
# ❌ 不可哈希的类型:list, dict, set
# 原因:可变对象 hash 值会变——字典里就找不到它了
# 故意演示坑:用可变对象做 key 会发生什么
# PyPy / CPython 的行为不同——这就是为什么 Python 禁止它
# frozenset:不可变的集合(可哈希,可以做 dict key)
fs = frozenset([1, 2, 3])
d[fs] = "set key" # ✅ frozenset 可哈希
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
哈希碰撞的简单演示:
# 两个相等的对象,hash 值一定相等
print(hash("hello")) # 某个正数(每次运行可能不同)
print(hash(42)) # 42(小整数的 hash 就是自己)
print(hash(-1)) # -2(特殊情况:-1 被保留为错误标志)
# True / False / None 的特殊 hash
print(hash(True)) # 1
print(hash(False)) # 0
print(1 == True) # True——这就是 bool 是 int 子类的后果
2
3
4
5
6
7
8
9
# 2.3.5 综合案例与思考
综合案例:词频统计器——字典/集合的实战
"""
词频统计器——字典统计、集合去重、推导式格式化的综合运用
"""
text = """
Python is an interpreted high-level programming language.
Python's design philosophy emphasizes code readability.
Python is dynamically typed and garbage-collected.
It supports multiple programming paradigms including structured,
object-oriented and functional programming.
Python is often described as a batteries included language.
"""
import re
from collections import Counter
# 1. 分词——正则分割(忽略标点和大小写)
words = re.findall(r"\b[a-zA-Z]+\b", text.lower())
print(f"总单词数:{len(words)}")
# 2. 去重——集合
unique_words = set(words)
print(f"不同单词数:{len(unique_words)}")
# 3. 词频统计——字典累加(手写版)
word_count = {}
for w in words:
word_count[w] = word_count.get(w, 0) + 1
# 4. 词频统计——Counter 版(更 Pythonic)
counter = Counter(words)
# 5. 输出 TOP 10
print("\n" + "=" * 50)
print(f"{'词频统计 TOP 10':^50}")
print("=" * 50)
print(f"{'排名':<5} {'单词':<15} {'次数':<6} {'柱状图'}")
for rank, (word, count) in enumerate(counter.most_common(10), 1):
bar = "█" * count
print(f"{rank:<5} {word:<15} {count:<6} {bar}")
# 6. 集合运算——找同时出现在两个列表中的词
keywords = {"python", "programming", "language", "code", "design"}
mentioned = unique_words & keywords
not_mentioned = keywords - unique_words
print(f"\n提到的关键词:{mentioned}")
print(f"未提到的关键词:{not_mentioned if not_mentioned else '无'}")
# 7. 长单词统计(推导式 + 过滤)
long_words = {w for w in unique_words if len(w) > 8}
print(f"长度 > 8 的单词:{sorted(long_words)}")
# 8. 字母频率——用 Counter 统计字母
letter_counts = Counter("".join(words))
print(f"\n最常用字母 TOP 5:{letter_counts.most_common(5)}")
print("=" * 50)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
运行输出:
总单词数:41
不同单词数:33
==================================================
词频统计 TOP 10
==================================================
排名 单词 次数 柱状图
1 python 4 ████
2 programming 4 ████
3 is 3 ███
4 language 2 ██
5 and 2 ██
6 an 1 █
7 interpreted 1 █
8 high 1 █
9 level 1 █
10 design 1 █
提到的关键词:{'python', 'programming', 'language', 'design'}
未提到的关键词:{'code'}
长度 > 8 的单词:['emphasizes', 'functional', 'garbage-collected', 'interpreted', 'object-oriented', 'readability']
最常用字母 TOP 5:[('e', 18), ('n', 12), ('i', 11), ('a', 10), ('l', 10)]
==================================================
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
案例知识融合:这个案例覆盖了字典/集合/推导式的全套知识——re.findall() 分词、set 去重、dict.get() 累加词频、Counter 更 Pythonic 的替代、& 集合交集找关键词、集合推导式过滤长单词、Counter 字母频率——30 行统计数据、10 行格式化输出,完整走完了一个 NLP 预处理管线。
思考题:
word_count[w] = word_count.get(w, 0) + 1和counter[w] += 1在性能上有差别吗?Counter内部是如何实现的?set(words)去重后失去了单词顺序——如何既去重又保持原序?Python 3.7+ 有什么内置工具可以实现?- 如果有 100 万单词的文本,
set和dict的去重/统计操作时间复杂度是多少?内存大约占用多少?
# 2.4 新手陷阱
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | 可变默认参数 | def f(lst=[]) 的 lst 只有一份——多次调用共享同一个列表,用 lst=None + 内部初始化 |
| 2 | 列表乘法复制引用 | [[]] * 3 产生三个指向同一内部列表的引用——改一个全变,用 [[] for _ in range(3)] |
| 3 | copy() 只是浅拷贝 | 列表 copy() 只复制最外层——内层对象还是共享的,深层嵌套用 copy.deepcopy() |
| 4 | 遍历时修改列表 | for x in lst: lst.remove(x) 会跳过元素——用列表推导式新建,或遍历副本 for x in lst[:]: |
| 5 | 字符串 join() 方向反了 | ",".join(["a","b"]) 正确,["a","b"].join(",") 错误——分隔符在前、列表在后 |
陷阱 1 详解——可变默认参数:
# ❌ 错误:默认参数 lst=[] 在函数定义时只创建一次
def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2]——哈?上一轮的 1 还在!
print(add_item(3)) # [1, 2, 3]——累积了!
# ✅ 正确:用 None 做哨兵,函数体内创建新列表
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
2
3
4
5
6
7
8
9
10
11
12
13
14
15
陷阱 2 详解——列表乘法的引用陷阱:
# ❌ 错误:三个子列表指向同一个对象
matrix = [[0] * 3] * 3
matrix[0][0] = 999
print(matrix) # [[999, 0, 0], [999, 0, 0], [999, 0, 0]]——全变了!
# ✅ 正确:每个子列表独立创建
matrix = [[0] * 3 for _ in range(3)]
matrix[0][0] = 999
print(matrix) # [[999, 0, 0], [0, 0, 0], [0, 0, 0]]——只有第一行变了
2
3
4
5
6
7
8
9
# 2.5 综合思考题
字符串不可变的设计哲学:Python 选择让字符串不可变——
s[0] = "H"报错。这与 C 的char[]可变设计相反。不可变字符串在什么场景下带来了好处(提示:字典 key、线程安全、字符串驻留)?在什么场景下是负担(提示:大量拼接)?列表推导式 vs map/filter:Python 有两种函数式风格——
[x*2 for x in lst if x>0]和list(map(lambda x: x*2, filter(lambda x: x>0, lst)))。哪种更 Pythonic?Guido(Python 之父)本人对map/filter的态度是什么?为什么 Python 3 把map改成了惰性迭代器?字典的有序性:Python 3.7+ 字典保证插入顺序——这是实现细节变成语言保证的罕见案例。这一保证是如何实现的(提示:紧凑数组 + 哈希表)?如果在需要顺序的场景你用
OrderedDict,和普通dict有性能差别吗?集合 vs frozenset:
set可变、frozenset不可变——这个设计和list/tuple的设计对称。frozenset可以做 dict 的 key——你能想到什么样的实际场景需要"集合作为 key"(提示:图的连通分量)?哈希冲突的应对:Python 字典用开放寻址法解决哈希冲突。如果你自定义类的
__hash__方法返回常数(如总返回 1),字典会退化为什么时间复杂度?这在实际项目中可能导致什么样的性能灾难?