流程控制与函数
# 第 3 章 Python 流程控制与函数
# 目录介绍
# 3.1 流程控制
📖 本章定位:程序 = 数据 + 逻辑。第 1、2 章讲了"数据"(变量、容器),本章和下一章讲"逻辑"——流程控制、函数、装饰器、然后通向第 4 章的面向对象。
📌 前章回顾:第 2 章的列表、字典、集合将在本章的循环和条件中频繁出现——它们是流程控制操作的对象。 📌 下章预告:学完函数和装饰器后,第 4 章会进入 Python OOP——用 class 封装"数据 + 逻辑"为整体。
# 3.1.1 if-elif-else 条件分支
Python 的条件分支用缩进代替 {}——这是和 C/C++/Java 最明显的视觉差异:
score = 85
if score >= 90:
grade = "A" # 缩进 = 4 个空格(PEP 8 标准)
elif score >= 80: # elif = else if
grade = "B"
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"
print(f"成绩等级:{grade}") # 成绩等级:B
2
3
4
5
6
7
8
9
10
11
12
13
14
🔑 和 C/C++ 核心差异:
// C++:用 {} 和大括号
if (score >= 90) {
grade = "A";
} else if (score >= 80) { // ← else if 中间有空格
grade = "B";
}
2
3
4
5
6
| C++ | Python |
|---|---|
else if | elif(5 个字母合成 4 个) |
() 包条件 | 括号可省略 |
{} 包代码块 | 缩进包代码块 |
; 结尾 | 无需分号 |
Python 独有的优雅写法:
# 条件表达式(三元运算符)
age = 20
status = "成年" if age >= 18 else "未成年" # 一行搞定
# 嵌套在一行(可读时用)
x = 15
result = "大于10" if x > 10 else "等于10" if x == 10 else "小于10"
# 判断是否在范围内——链式比较
if 18 <= age <= 60: # Python 独有!等价于 18 <= age and age <= 60
print("适龄劳动力")
2
3
4
5
6
7
8
9
10
11
# 3.1.2 for 循环与 range
Python 的 for 不是 C 的计数循环——它是迭代器循环,遍历任何可迭代对象:
# 遍历列表
fruits = ["苹果", "香蕉", "橘子"]
for fruit in fruits:
print(fruit)
# 苹果
# 香蕉
# 橘子
# 遍历字符串(字符串是可迭代对象)
for ch in "ABC":
print(ch, end=" ") # A B C
# 遍历字典
d = {"a": 1, "b": 2, "c": 3}
for key in d: # 默认遍历键
print(key, d[key])
for key, val in d.items(): # 同时遍历键和值
print(f"{key} → {val}")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# range() 的三种形态
# range(stop):从 0 到 stop-1
print(list(range(5))) # [0, 1, 2, 3, 4]
# range(start, stop):从 start 到 stop-1
print(list(range(2, 7))) # [2, 3, 4, 5, 6]
# range(start, stop, step):带步长
print(list(range(0, 10, 2))) # [0, 2, 4, 6, 8]
print(list(range(10, 0, -1))) # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1](倒序)
# range 是惰性的——不创建完整列表,内存 O(1)
r = range(10 ** 9) # 10 亿个元素,不占内存
print(r[1000000]) # 1000000(按需计算)
2
3
4
5
6
7
8
9
10
11
12
13
# enumerate():同时取索引和值
names = ["张三", "李四", "王五"]
for i, name in enumerate(names, start=1):
print(f"{i}. {name}")
# 1. 张三
# 2. 李四
# 3. 王五
2
3
4
5
6
# zip():并行遍历多个序列
names = ["张三", "李四", "王五"]
scores = [85, 92, 78]
cities = ["北京", "上海", "深圳"]
for name, score, city in zip(names, scores, cities):
print(f"{name}({city}):{score}分")
# 张三(北京):85分
# 李四(上海):92分
# 王五(深圳):78分
# zip 按最短序列截断
for a, b in zip([1, 2, 3], ["a", "b"]):
print(a, b)
# 1 a
# 2 b (3 被跳过了)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.1.3 while 循环
while 和 C 语义一致——条件为真就持续执行:
# 基本形态
count = 0
while count < 5:
print(count, end=" ")
count += 1
# 0 1 2 3 4
# 无限循环 + break
while True:
answer = input("继续?(y/n): ")
if answer.lower() == "n":
print("退出")
break
2
3
4
5
6
7
8
9
10
11
12
13
🔑 Python while vs C while 的差异:
// C 中常见的计数循环写法
int i = 0;
while (i < 10) { printf("%d ", i); i++; }
// 同样的逻辑,Python 更推荐 for + range
for i in range(10):
print(i, end=" ")
2
3
4
5
6
7
📌 选择 for 还是 while? Python 哲学:知道遍历次数用
for,不知道次数用while。for+range比while安全——不可能写出死循环。
# 3.1.4 break / continue / pass
# break:立即退出整个循环
for i in range(1, 10):
if i == 5:
break # 遇到 5 就停
print(i, end=" ") # 1 2 3 4
# continue:跳过本次循环,进入下一次
for i in range(1, 6):
if i == 3:
continue # 跳过 3
print(i, end=" ") # 1 2 4 5
# pass:什么都不做——占位符
if True:
pass # 临时留空,语法正确
def not_implemented_yet():
pass # 函数体还没写好,先占位
class FutureFeature:
pass # 类也先占位
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
🔑 pass 是 Python 独有的——C/C++ 用 ; 或空 {},Python 必须有显式占位符。这在开发初期极其有用:先把架构写出来(全是 pass),再逐个填充。
# 3.1.5 循环的 else 子句
这是 Python 最令人困惑又最强大的特性之一——else 在循环里不是"条件不满足时执行",而是"循环正常结束时执行":
# 场景 1:正常结束——执行 else
for i in range(3):
print(i, end=" ")
else:
print("\n循环正常结束") # ✅ 会执行
# 0 1 2
# 循环正常结束
# 场景 2:break 退出——不执行 else
for i in range(5):
if i == 3:
break
print(i, end=" ")
else:
print("\n循环正常结束") # ❌ 不会执行——break 跳出了
# 0 1 2
# 场景 3:空循环——也会执行 else
for i in []:
print(i)
else:
print("空循环——仍然执行 else") # ✅ 执行!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
🔑 for...else 经典用途——查找元素,"没找到"用 else 而不是标志位:
# 传统写法(标志位)
found = False
for item in items:
if condition(item):
found = True
break
if not found:
print("没找到")
# Pythonic 写法(for...else)
for item in items:
if condition(item):
break
else:
print("没找到") # 没找到才到这里
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.1.6 海象运算符 :=
Python 3.8 引入的 :=(walrus operator)——在表达式内部同时赋值和取值:
# 经典场景 1:while 循环中避免重复调用
# 旧写法:regex 调了两次
import re
while True:
line = input()
if line == "q":
break
print(f"输入:{line}")
# 海象写法:赋值 + 判断一行搞定
while (line := input()) != "q":
print(f"输入:{line}")
# 经典场景 2:if 中复用计算结果
# 旧写法:len 调了两次
data = [1, 2, 3]
n = len(data)
if n > 2:
print(f"长度 {n} 大于 2")
# 海象写法:
if (n := len(data)) > 2:
print(f"长度 {n} 大于 2")
# 经典场景 3:列表推导中复用
# 旧写法
results = []
for x in range(10):
y = x ** 2
if y > 20:
results.append(y)
# 海象写法——一行
results = [(y := x ** 2) for x in range(10) if y > 20]
print(results) # [25, 36, 49, 64, 81]
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
⚠️ 节制使用:
:=很酷,但滥用会让代码难读——只在"不赋值就重复计算"的场景用。
# 3.1.7 综合案例与思考
综合案例:猜数字游戏——流程控制全家桶
"""
猜数字游戏——展示 if/while/break/else/海象运算符的综合运用
"""
import random
def guess_number():
MIN, MAX = 1, 100
secret = random.randint(MIN, MAX)
attempts = 0
max_attempts = 7
print("\n" + "=" * 45)
print(f"{'猜数字游戏':^45}")
print("=" * 45)
print(f"数字范围:{MIN} ~ {MAX},你只有 {max_attempts} 次机会!")
while attempts < max_attempts:
# 海象运算符:输入 + 验证 + 赋值 一行完成
if (raw := input(f"\n第 {attempts + 1}/{max_attempts} 次,请输入数字:")) == "q":
print("放弃啦!")
break
# 输入验证:多重条件判断
if not raw.isdigit() and not (raw.startswith("-") and raw[1:].isdigit()):
print("⚠️ 请输入有效数字!")
continue
guess = int(raw)
if guess < MIN or guess > MAX:
print(f"⚠️ 数字必须在 {MIN}~{MAX} 之间!")
continue
attempts += 1
# 核心判断:二分支
if guess == secret:
print(f"\n🎉 猜对了!用时 {attempts} 次。")
# 评价等级——多分支 elif
if attempts == 1:
print("🏆 神级——一次命中!")
elif attempts <= 3:
print("👍 不错——很快!")
elif attempts <= 5:
print("😊 还行——多练练。")
else:
print("💪 险胜——下次更快!")
break
elif guess < secret:
print(f"📈 太小了——再大一点")
else:
print(f"📉 太大了——再小一点")
# 倒数第二次时给提示(海象运算 + 条件)
if (left := max_attempts - attempts) == 1:
print(f"⚠️ 最后一次机会了!")
else:
print(f"还剩 {left} 次机会")
else: # while 的 else:循环正常结束(没用 break)才执行
print(f"\n😞 次数用完了!正确答案是 {secret}。")
# 询问是否再玩一局
play_again = input("\n再来一局?(y/n):").strip().lower()
return play_again.startswith("y")
# 主程序
if __name__ == "__main__":
while True:
if not guess_number():
print("\n谢谢游玩,再见!👋")
break
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
72
交互演示:
=============================================
猜数字游戏
=============================================
数字范围:1 ~ 100,你只有 7 次机会!
第 1/7 次,请输入数字:50
📈 太小了——再大一点
还剩 6 次机会
第 2/7 次,请输入数字:75
📉 太大了——再小一点
还剩 5 次机会
第 3/7 次,请输入数字:63
🎉 猜对了!用时 3 次。
👍 不错——很快!
再来一局?(y/n):n
谢谢游玩,再见!👋
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
案例知识融合:这个案例覆盖了流程控制的全部知识——if/elif/else 多路分支、while 条件循环 + else 子句、break/continue、海象运算符 :=(3 处使用)、输入验证、main guard——60 行代码完成了一个完整、健壮、可交互的游戏。
思考题:
- 第
max_attempts次时while条件是attempts < max_attempts,如果改成<=会发生什么?这种情况下else分支还会执行吗? for...else和while...else——else的语义一直有争议。你认为它的设计合理吗?如果去掉else,这个游戏需要怎么改?- 海象运算
if (raw := input(...)) == "q"为什么外层要加括号?去掉括号会发生什么(提示:=和==的优先级)?
# 3.2 函数的定义与调用
# 3.2.1 def 定义函数
Python 用 def 定义函数——没有返回类型声明、没有参数类型声明(类型注解可选):
# 最简形式
def greet():
print("Hello, World!")
greet() # 调用
# 带参数
def greet(name):
print(f"Hello, {name}!")
greet("张三") # Hello, 张三!
# 带参数和返回
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
🔑 和 C/C++ 对比——差异巨大:
// C++ 版
int add(int a, int b) { // 必须声明返回类型和参数类型
return a + b;
}
2
3
4
# Python 版
def add(a, b): # 没有任何类型声明
return a + b
2
3
| 维度 | C++ | Python |
|---|---|---|
| 关键字 | int / void 等 | def |
| 返回类型 | 必须声明 | 无需声明 |
| 参数类型 | 必须声明 | 无需声明 + 可选类型注解 |
| 代码块 | {} | 缩进 |
| 声明 vs 定义 | 需要分离(头文件) | 无需分离 |
| 重载 | 支持(名称修饰) | 不支持——同名覆盖 |
# 3.2.2 函数返回值
Python 函数默认返回 None——不需要显式写 return None:
# 没有 return 语句的函数返回 None
def no_return():
print("这条语句执行了")
result = no_return() # 这条语句执行了
print(result) # None
# 返回多个值——本质是返回元组
def get_position():
return 10, 20, 30 # 等价于 return (10, 20, 30)
x, y, z = get_position() # 解包
print(x, y, z) # 10 20 30
# 返回不同类型的多值
def analyze(numbers):
return min(numbers), max(numbers), sum(numbers) / len(numbers)
mi, ma, avg = analyze([3, 1, 4, 1, 5, 9])
print(f"最小:{mi}, 最大:{ma}, 平均:{avg:.2f}")
# 最小:1, 最大:9, 平均:3.83
# 文档字符串(docstring)——Python 的"函数说明"
def calculate_bmi(weight_kg, height_m):
"""计算 BMI 指数。
Args:
weight_kg: 体重(公斤)
height_m: 身高(米)
Returns:
float: BMI 值
"""
return weight_kg / (height_m ** 2)
# 查看文档
print(calculate_bmi.__doc__)
help(calculate_bmi)
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
# 3.2.3 综合案例与思考
综合案例:工具函数库——函数定义、文档、多值返回综合
"""
数学工具函数库——展示函数定义、docstring、参数处理
"""
def statistics(numbers):
"""计算列表的统计信息。
Args:
numbers: 数字列表
Returns:
tuple: (最小值, 最大值, 总和, 平均值, 中位数)
"""
if not numbers:
return 0, 0, 0, 0, 0
sorted_nums = sorted(numbers)
n = len(sorted_nums)
total = sum(numbers)
avg = total / n
# 中位数:奇数为中间,偶数为中间两数平均
mid = n // 2
median = sorted_nums[mid] if n % 2 else (sorted_nums[mid - 1] + sorted_nums[mid]) / 2
return min(numbers), max(numbers), total, avg, median
def is_prime(n: int) -> bool:
"""判断一个数是否为素数。
Args:
n: 待判定的整数
Returns:
是素数返回 True,否则返回 False
"""
if n <= 1:
return False
if n <= 3:
return True
if n % 2 == 0 or n % 3 == 0:
return False
# 6k ± 1 检查——只需检查到 sqrt(n)
i = 5
while i * i <= n:
if n % i == 0 or n % (i + 2) == 0:
return False
i += 6
return True
def fibonacci(n: int) -> list[int]:
"""生成前 n 个斐波那契数。
Args:
n: 要生成的数量
Returns:
斐波那契数列列表
"""
if n <= 0:
return []
if n == 1:
return [0]
fib = [0, 1]
while (l := len(fib)) < n: # 海象运算复用
fib.append(fib[l - 1] + fib[l - 2])
return fib
# ===== 测试 =====
if __name__ == "__main__":
# 测试 statistics
mi, ma, total, avg, median = statistics([85, 92, 78, 90, 88, 95, 72])
print("=== 统计函数测试 ===")
print(f"最小:{mi} 最大:{ma} 总和:{total}")
print(f"平均:{avg:.1f} 中位数:{median}")
# 测试 is_prime
print("\n=== 素数判断 ===")
primes = [n for n in range(2, 50) if is_prime(n)]
print(f"2~49 内的素数:{primes}")
# 测试 fibonacci
print("\n=== 斐波那契数列 ===")
print(f"前 15 个:{fibonacci(15)}")
# 读取 docstring
print(f"\nstatistics 文档:\n{statistics.__doc__}")
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
运行输出:
=== 统计函数测试 ===
最小:72 最大:95 总和:600
平均:85.7 中位数:88
=== 素数判断 ===
2~49 内的素数:[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
=== 斐波那契数列 ===
前 15 个:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
statistics 文档:
计算列表的统计信息。
...
2
3
4
5
6
7
8
9
10
11
12
13
案例知识融合:这个案例展示了函数的三个核心方面——statistics 演示多值返回和空值保护、is_prime 演示类型注解 -> bool 和高效算法、fibonacci 演示循环 + 海象运算符——三个函数通过 __doc__ 统一文档风格。
思考题:
- Python 函数多值返回的底层是什么?如果只用一个变量接收
statistics([1,2,3]),这个变量是什么类型? def is_prime(n: int) -> bool中的类型注解会被 Python 运行时强制检查吗?它的主要作用是什么?(提示:IDE 提示、mypy 静态检查)- Python 不支持函数重载——如果需要同一个函数名处理不同类型的参数,Python 通常怎么做?
# 3.3 函数参数详解
Python 的函数参数系统是四大语言(C++/Java/Python/JavaScript)里最灵活的一个——五种参数类型覆盖了所有传参场景。
# 3.3.1 位置参数
按位置一一匹配——最基础的形式:
def describe(name, age, city):
print(f"{name},{age}岁,来自{city}")
describe("张三", 25, "深圳") # 顺序必须一一对应
# describe("张三", "深圳", 25) # 错了!25 传给了 city
# 可以用关键字指定——跳过位置依赖
describe(age=25, name="李四", city="北京") # 关键词参数——顺序随意
describe("王五", city="上海", age=30) # 混用:位置在前、关键词在后
# describe(name="赵六", 30, "深圳") # ❌ 位置不能在关键词之后
2
3
4
5
6
7
8
9
10
# 3.3.2 默认参数
给参数提供默认值——调用时可省略:
def greet(name, greeting="Hello", punctuation="!"):
print(f"{greeting}, {name}{punctuation}")
greet("张三") # Hello, 张三!
greet("李四", "Hi") # Hi, 李四!
greet("王五", punctuation=".") # Hello, 王五.
greet("赵六", "Hey", "~") # Hey, 赵六~
# ⚠️ 默认参数必须从右向左——无默认的不能放有默认的后面
# def f(a=1, b): # ❌ SyntaxError
# pass
2
3
4
5
6
7
8
9
10
11
🔑 默认参数的陷阱——可变对象只创建一次(§3.6 新手陷阱详讲):
# ❌ 错误写法:默认参数在函数定义时只计算一次
def add_to_list(item, lst=[]):
lst.append(item)
return lst
print(add_to_list(1)) # [1]
print(add_to_list(2)) # [1, 2]——lst 被共享了!
# ✅ 正确写法:用 None 做哨兵
def add_to_list(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
# 3.3.3 可变参数 *args
*args 接受任意多个位置参数,打包成元组:
def sum_all(*args):
print(f"args 类型:{type(args)},内容:{args}")
return sum(args) if args else 0
print(sum_all()) # 0
print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5)) # 15
# args 可以取任意名字——惯例叫 args
def print_each(*items):
for item in items:
print(f"- {item}")
print_each("苹果", "香蕉", "橘子")
# - 苹果
# - 香蕉
# - 橘子
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.3.4 关键字参数 **kwargs
**kwargs 接受任意多个关键字参数,打包成字典:
def show_info(**kwargs):
print(f"kwargs 类型:{type(kwargs)}")
for key, val in kwargs.items():
print(f" {key} = {val}")
show_info(name="张三", age=25, city="深圳")
# kwargs 类型:<class 'dict'>
# name = 张三
# age = 25
# city = 深圳
# 实战:通用的配置函数
def create_user(required_name, **options):
user = {"name": required_name, "active": True}
user.update(options) # 将可选配置合并进去
return user
u1 = create_user("张三")
print(u1) # {'name': '张三', 'active': True}
u2 = create_user("李四", age=30, city="上海", vip=True)
print(u2) # {'name': '李四', 'active': True, 'age': 30, 'city': '上海', 'vip': True}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 3.3.5 参数组合顺序
当五种参数同时出现时,顺序是强制的:
# 完整参数顺序:位置 → *args → 关键字 → **kwargs
# (前三种可用关键字替代)
def func(a, b, c=0, *args, d, e=0, **kwargs):
print(f"a={a}, b={b}, c={c}, args={args}, d={d}, e={e}, kwargs={kwargs}")
func(1, 2) # ❌ TypeError: missing required keyword-only argument 'd'
func(1, 2, d=99) # a=1, b=2, c=0, args=(), d=99, e=0, kwargs={}
func(1, 2, 3, 4, 5, d=6, e=7, x=8, y=9)
# a=1, b=2, c=3, args=(4, 5), d=6, e=7, kwargs={'x': 8, 'y': 9}
# 星号 * 在中间的作用:强制后面的参数必须用关键字传入
def register(name, *, age, city):
print(f"{name}, {age}, {city}")
register("张三", age=25, city="深圳") # ✅
# register("张三", 25, "深圳") # ❌ TypeError
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
参数分类速查表:
| 参数类型 | 写法 | 示例调用 | 本质 |
|---|---|---|---|
| 位置参数 | def f(a, b) | f(1, 2) | 最基础 |
| 默认参数 | def f(a, b=0) | f(1) | 有默认值 |
*args | def f(*args) | f(1,2,3) | 打包成 tuple |
| 仅限关键字 | def f(*, a) | f(a=1) | 必须写名字 |
**kwargs | def f(**kw) | f(a=1, b=2) | 打包成 dict |
# 3.3.6 综合案例与思考
综合案例:灵活的命令分发器——五种参数的实战
"""
命令分发器——展示位置参数、默认参数、*args、**kwargs 的实战组合
"""
from datetime import datetime
def log_command(message, level="INFO", *, timestamp=True, **extra):
"""通用日志函数——* 强制 level 之后都必须用关键字传入。"""
prefix = f"[{datetime.now():%H:%M:%S}] " if timestamp else ""
extras = " | " + " | ".join(f"{k}={v}" for k, v in extra.items()) if extra else ""
print(f"{prefix}[{level}] {message}{extras}")
def dispatch(cmd_name, *args, **kwargs):
"""命令分发器:根据 cmd_name 调用不同的处理函数。
Args:
cmd_name: 命令名
*args: 位置参数(传给具体处理函数)
**kwargs: 关键字参数(传给具体处理函数)
"""
handlers = {
"hello": lambda name="世界", **kw: print(f"你好,{name}!"),
"echo": lambda *items: print(" ".join(str(i) for i in items)),
"calc": lambda op, a, b, **kw: {
"+": a + b, "-": a - b, "*": a * b, "/": a / b if b else "零除错误"
}.get(op, f"未知运算符 {op}"),
"info": lambda **kw: print("\n".join(f" {k}: {v}" for k, v in kw.items())),
}
handler = handlers.get(cmd_name)
if handler is None:
log_command(f"未知命令:{cmd_name}", level="ERROR", available=list(handlers.keys()))
return
try:
result = handler(*args, **kwargs)
if result is not None:
print(f"结果:{result}")
except Exception as e:
log_command(f"命令执行失败:{e}", level="ERROR", cmd=cmd_name)
# ===== 测试 =====
if __name__ == "__main__":
# 位置参数 + 默认参数
dispatch("hello") # 默认 "世界"
dispatch("hello", "杨充") # 自定义
# *args
dispatch("echo", "Hello", "World", 2025) # 任意数量
# 位置 + 关键字
dispatch("calc", "+", 10, 20) # 纯位置
result = None
exec("dispatch('calc', op='*', a=6, b=7)") # 关纯键字
# **kwargs
dispatch("info", name="杨充", age=28, lang="Python")
# 日志
log_command("程序启动")
log_command("数据库连接失败", level="ERROR", retry=3, db="users")
log_command("请求完成", duration_ms=42.5, status=200, timestamp=False)
# 错误命令
dispatch("unknown") # 触发错误处理
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
案例知识融合:这个案例通过命令分发器展现了 Python 参数系统的全部灵活性——dispatch 用 *args 和 **kwargs 透传参数、log_command 用 * 强制 timestamp 为关键字参数、**extra 收集任意日志属性——30 行的 dispatcher 可以扩展到任意数量的命令。
思考题:
dispatch函数里的handler(*args, **kwargs)是什么?这一行背后发生了什么?def log_command(message, level="INFO", *, timestamp=True, **extra)中的*不是*args——它是什么意思?什么时候应该用它?- Python 的
*args小号 C 的va_list——它们有什么区别?*args为什么比va_list安全得多?
# 3.4 作用域与命名空间
# 3.4.1 LEGB 规则
Python 查找变量时按 LEGB 顺序——这是理解作用域的唯一钥匙:
L → Local 局部作用域(函数内部)
E → Enclosing 闭包作用域(外层函数的局部)
G → Global 全局作用域(模块级别)
B → Built-in 内置作用域(print、len 等)
2
3
4
x = "global" # G:全局作用域
def outer():
x = "enclosing" # E:闭包作用域(对 inner 来说)
def inner():
x = "local" # L:局部作用域
print(f"inner 里的 x = {x}")
inner()
print(f"outer 里的 x = {x}")
outer()
print(f"全局的 x = {x}")
2
3
4
5
6
7
8
9
10
11
12
13
14
输出:
inner 里的 x = local ← L 找到了
outer 里的 x = enclosing ← E(inner 内的 x 不影响 outer 的 x)
全局的 x = global ← G(outer 内的 x 不影响全局的 x)
2
3
🔑 python -i 交互式演示 LEGB:
>>> len # B:内置函数(Built-in)
<built-in function len>
>>> len = 10 # 在模块作用域定义了 len(覆盖!)
>>> len # G:10(Built-in 的 len 被屏蔽了)
10
>>> del len # 删掉全局的 len
>>> len # B:又恢复内置的了
<built-in function len>
2
3
4
5
6
7
8
9
10
⚠️ 永远不要给变量起名叫
len、list、
# 3.4.2 global 与 nonlocal
# 问题:函数内无法直接修改全局变量——只会创建一个同名的局部变量
count = 0
def increment_wrong():
count += 1 # ❌ UnboundLocalError!Python 认为 count 是局部变量
# 解决方案 1:global——告诉 Python "这是全局变量"
def increment():
global count
count += 1
increment()
print(count) # 1
# 解决方案 2:nonlocal——修改外层函数的局部变量(而非全局)
def make_counter():
n = 0 # enclosing 作用域
def inc():
nonlocal n # 告诉 Python:n 不是局部的,找外层的
n += 1
return n
return inc
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 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
28
🔑 global 和 nonlocal 的选择决策:
| 场景 | 使用 | 原因 |
|---|---|---|
| 修改模块级变量 | global | 声明后直接赋值 |
| 修改外层函数变量 | nonlocal | 闭包场景专属 |
| 只读取外层/全局变量 | 什么都不写 | LEGB 查找自动找到 |
| 修改可变对象的内部状态 | 不需要 | lst.append(1) 不改变 lst 的绑定 |
# 3.4.3 闭包初探
闭包 = 函数 + 它捕获的外层变量。Python 的闭包不需要 [] 捕获列表——自动按引用捕获:
def make_multiplier(factor):
"""返回一个函数,该函数把输入乘以 factor。"""
def multiply(x):
return x * factor # factor 来自外层函数——被闭包捕获了
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10)) # 20(factor=2)
print(triple(10)) # 30(factor=3)
2
3
4
5
6
7
8
9
10
11
图解闭包捕获状态:
make_multiplier(2) 调用结束后:
factor = 2(本应被销毁——但被 multiply 捕获,保留在闭包中)
double ──→ [multiply 函数 + factor=2]
triple ──→ [multiply 函数 + factor=3]
2
3
4
5
🔑 C++ 闭包的对比:
// C++ Lambda——需要显式指定捕获方式
auto make_multiplier = [](int factor) {
return [factor](int x) { return x * factor; }; // [=] 或 [factor]
};
2
3
4
Python 的闭包 "自动 + 按引用" 更简单,但也有坑——延迟绑定的经典 bug(见 §3.6 陷阱)。
# 3.4.4 综合案例与思考
综合案例:配置化日志系统——LEGB、闭包、作用域的综合实战
"""
配置化日志系统——展示全局配置、闭包捕获、nonlocal 计数器的实战场景
"""
import time
# GLOBAL:全局日志配置
_log_config = {
"min_level": "DEBUG",
"show_timestamp": True,
"max_history": 100,
}
_log_history: list[str] = [] # 全局日志缓存(GLOBAL 作用域)
# -- 辅助:级别比较 --
_LEVEL_ORDER = {"DEBUG": 10, "INFO": 20, "WARN": 30, "ERROR": 40}
def set_log_level(level: str):
"""修改全局日志级别(需要 global)。"""
global _log_config
_log_config["min_level"] = level
def make_logger(name: str):
"""创建带名称前缀的日志器(闭包 + nonlocal)。
返回一个 log 函数,自动在每条日志前加 [name] 前缀,
并统计该日志器的调用次数。
"""
count = 0 # ENCLOSING:日志器专属计数器
def log(message, level="INFO"):
nonlocal count # 修改外层的 count
if _LEVEL_ORDER.get(level, 0) < _LEVEL_ORDER.get(_log_config["min_level"], 0):
return # 级别不够,静默丢弃
count += 1
ts = time.strftime("%H:%M:%S") if _log_config["show_timestamp"] else ""
line = f"{ts} [{level}][{name}] {message} (第{count}条)"
print(line)
# 写全局历史
global _log_history
_log_history.append(line)
if len(_log_history) > _log_config["max_history"]:
_log_history.pop(0)
return log
# ===== 测试 =====
if __name__ == "__main__":
# 创建两个日志器
api_log = make_logger("API")
db_log = make_logger("DB")
api_log("服务启动", "INFO")
db_log("数据库连接建立")
api_log("请求处理完成", "DEBUG") # 级别是 DEBUG——会被打印
api_log("内存使用率 85%", "WARN")
db_log("查询超时", "ERROR")
print(f"\n--- 下调日志级别到 WARN ---")
set_log_level("WARN")
api_log("这条件不会被打印", "DEBUG") # 静默丢弃
api_log("这条件会被打印", "WARN") # ✅ 打印
api_log("严重警告", "ERROR") # ✅ 打印
print(f"\n--- 日志历史(最近 5 条)---")
for line in _log_history[-5:]:
print(f" {line}")
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
72
73
74
75
运行输出:
14:30:00 [INFO][API] 服务启动 (第1条)
14:30:00 [INFO][DB] 数据库连接建立 (第1条)
14:30:00 [DEBUG][API] 请求处理完成 (第2条)
14:30:00 [WARN][API] 内存使用率 85% (第3条)
14:30:00 [ERROR][DB] 查询超时 (第2条)
--- 下调日志级别到 WARN ---
14:30:00 [WARN][API] 这条件会被打印 (第4条)
14:30:00 [ERROR][API] 严重警告 (第5条)
--- 日志历史(最近 5 条)---
[INFO][API] 服务启动 (第1条)
[INFO][DB] 数据库连接建立 (第1条)
[DEBUG][API] 请求处理完成 (第2条)
[WARN][API] 内存使用率 85% (第3条)
[ERROR][DB] 查询超时 (第2条)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
案例知识融合:这个案例是 LEGB 规则的完美教科书——_log_config 用 global 修改、count 用 nonlocal 修改、make_logger 返回闭包 log(携带 name 和 count)、_log_history 在 log 内部通过 global 写入——四个作用域层全部在同一个系统中自然登场。
思考题:
make_logger返回的log函数每次调用时name从哪里来?它是怎么"记住"创建时的name参数的?_log_history.pop(0)是 O(n) 的操作——如果日志量很大(10万条),这会成为瓶颈吗?怎么优化?- 如果去掉
nonlocal count,count += 1会报什么错?为什么 Python 不允许直接修改外层变量?
# 3.5 Lambda 匿名函数
Lambda 是一行函数——唯一的限制是函数体必须是单个表达式:
# 语法:lambda 参数: 表达式
# 例 1:最简 lambda
square = lambda x: x ** 2
print(square(5)) # 25——和 def 效果一样
# 例 2:多个参数
add = lambda a, b: a + b
print(add(3, 5)) # 8
# 例 3:作为排序的 key——lambda 最常见的用武之地
students = [("张三", 85), ("李四", 92), ("王五", 78)]
students.sort(key=lambda x: x[1]) # 按分数排序
print(students) # [('王五', 78), ('张三', 85), ('李四', 92)]
# 例 4:作为 map/filter 的参数(虽然列表推导式更好)
nums = [1, 2, 3, 4, 5]
evens = list(filter(lambda x: x % 2 == 0, nums)) # [2, 4]
doubled = list(map(lambda x: x * 2, nums)) # [2, 4, 6, 8, 10]
# 例 5:IIFE——立即调用的 lambda(少见但有妙用)
result = (lambda a, b: a ** b)(2, 10)
print(result) # 1024
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
🔑 lambda vs def——什么时候用哪个?
| 场景 | lambda | def |
|---|---|---|
| 一行表达式 | ✅ 推荐 | 也可以用 |
| 需要多行/多条语句 | ❌ 不支持 | ✅ 必须 |
| 需要 docstring | ❌ 不支持 | ✅ |
| 作为 sort/map/filter 的一次性参数 | ✅ 推荐 | 可以但啰嗦 |
| 给函数起名复用 | ❌ 不推荐 | ✅ 必须 |
📌 金科玉律:lambda 只做一行搞定的事。一旦需要换行写——立刻换
def。任何人都不该读三行 lambda。
# 3.5.1 综合案例与思考
综合案例:高级排序工具箱——lambda 实战
"""
高级排序工具箱——lambda 作为自定义排序键的各种场景
"""
# 数据集:商品列表(名称, 价格, 评分, 销量)
products = [
("iPhone 15", 7999, 4.8, 12000),
("MacBook Pro", 14999, 4.9, 8500),
("AirPods", 1299, 4.6, 30000),
("iPad Air", 4999, 4.7, 15000),
("Apple Watch", 2999, 4.5, 22000),
]
print("=" * 60)
print(f"{'商品排序演示':^60}")
print("=" * 60)
# 1. 按价格升序
by_price = sorted(products, key=lambda p: p[1])
print("\n📌 按价格升序:")
for p in by_price:
print(f" {p[0]:<15} ¥{p[1]:>6,} 评分 {p[2]} 销量 {p[3]:>6,}")
# 2. 按评分降序
by_rating = sorted(products, key=lambda p: p[2], reverse=True)
print("\n📌 按评分降序:")
for p in by_rating:
print(f" {p[0]:<15} 评分 {p[2]} ¥{p[1]:>6,}")
# 3. 多级排序:先按评分降序,评分相同按价格升序
by_multi = sorted(products, key=lambda p: (-p[2], p[1]))
print("\n📌 先评分降序、再价格升序:")
for p in by_multi:
print(f" {p[0]:<15} 评分 {p[2]} ¥{p[1]:>6,}")
# 4. 自定义综合评分(加权:评分 60% + 销量归一化 40%)
max_sales = max(p[3] for p in products)
by_weighted = sorted(
products,
key=lambda p: (p[2] * 0.6 + (p[3] / max_sales) * 0.4),
reverse=True
)
print("\n📌 综合加权排序(评分60% + 销量40%):")
for p in by_weighted:
score = p[2] * 0.6 + (p[3] / max_sales) * 0.4
print(f" {p[0]:<15} 综合分 {score:.3f}")
# 5. 分组展示——lambda 做 dict 分类键
from collections import defaultdict
price_groups = defaultdict(list)
for p in products:
level = (lambda price: # IIFE 风格 lambda
"高端" if price > 10000 else
"中端" if price > 3000 else
"入门"
)(p[1])
price_groups[level].append(p[0])
print("\n📌 价格分组:")
for level, items in price_groups.items():
print(f" {level}:{', '.join(items)}")
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
运行输出:
============================================================
商品排序演示
============================================================
📌 按价格升序:
AirPods ¥ 1,299 评分 4.6 销量 30,000
Apple Watch ¥ 2,999 评分 4.5 销量 22,000
iPad Air ¥ 4,999 评分 4.7 销量 15,000
iPhone 15 ¥ 7,999 评分 4.8 销量 12,000
MacBook Pro ¥14,999 评分 4.9 销量 8,500
📌 按评分降序:
MacBook Pro 评分 4.9 ¥14,999
iPhone 15 评分 4.8 ¥ 7,999
iPad Air 评分 4.7 ¥ 4,999
AirPods 评分 4.6 ¥ 1,299
Apple Watch 评分 4.5 ¥ 2,999
📌 先评分降序、再价格升序:
MacBook Pro 评分 4.9 ¥14,999
iPhone 15 评分 4.8 ¥ 7,999
iPad Air 评分 4.7 ¥ 4,999
AirPods 评分 4.6 ¥ 1,299
Apple Watch 评分 4.5 ¥ 2,999
📌 综合加权排序(评分60% + 销量40%):
AirPods 综合分 1.160
iPhone 15 综合分 1.040
iPad Air 综合分 1.020
MacBook Pro 综合分 1.053
Apple Watch 综合分 0.993
📌 价格分组:
入门:AirPods
中端:iPad Air, Apple Watch
高端:iPhone 15, MacBook Pro
============================================================
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
案例知识融合:这个案例覆盖了 lambda 的全部典型场景——单级排序、多级排序(lambda p: (-p[2], p[1]) 用负号取反实现降序)、加权排序(lambda 内嵌复杂表达式)、IIFE 风格的价格分组——用无状态的纯函数式写法完成了多维度数据分析。
思考题:
key=lambda p: (-p[2], p[1])中的-p[2]为什么能实现降序?这种技巧有什么局限性(提示:非数值类型)?- 本案例中 IIFE lambda
(lambda price: ...)(p[1])和直接用if/elif/else有什么区别?哪种写法更好? sort的key参数接受一个函数——除了 lambda,能不能传普通def定义的函数?如果能,什么场景下用def比 lambda 更好?
# 3.6 装饰器
装饰器(Decorator)是 Python 最强大的语法特性之一——在不修改原函数代码的前提下,给函数添加额外的行为。它来自 §3.4 闭包和 §3.5 lambda 的思想合流:"函数是对象" + "闭包 = 捕获外层变量"。
# 3.6.1 装饰器的本质
装饰器的本质是高阶函数——接受一个函数,返回一个新函数:
# 没有装饰器——手动包装
def greet():
return "你好!"
def with_log(func): # 高阶函数:接受函数作参数
def wrapper(*args, **kwargs): # 闭包:捕获 func
print(f"[LOG] 调用 {func.__name__}()")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__}() 返回 {result}")
return result
return wrapper # 返回新函数
logged_greet = with_log(greet) # 手动包装
print(logged_greet())
# [LOG] 调用 greet()
# [LOG] greet() 返回 你好!
# 你好!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
🔑 @ 语法糖——把上面三行变成一行:
@with_log # 等价于 greet = with_log(greet)
def greet():
return "你好!"
# greet 已经被"装饰"过了——直接调用就有日志
print(greet()) # 和上面完全一样的输出
2
3
4
5
6
图解装饰器做了什么:
greet(原始函数)──→ with_log(greet) ──→ greet(新的 wrapper 函数,但保持原名)
│
└─ wrapper 内部:先 log → 调原 greet → log → 返回结果
2
3
# 3.6.2 超时器装饰器——完整案例
import time
from functools import wraps
def timer(func):
"""测量函数执行时间的装饰器"""
@wraps(func) # 保留原函数的元信息(名称、docstring)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"⏱ {func.__name__}() 耗时 {elapsed:.4f} 秒")
return result
return wrapper
@timer
def slow_sum(n):
"""计算 1 到 n 的和(故意用慢方法)"""
total = 0
for i in range(n + 1):
total += i
return total
print(slow_sum(10_000_000))
# ⏱ slow_sum() 耗时 0.3456 秒
# 50000005000000
# 验证元信息被保留(感谢 @wraps)
print(slow_sum.__name__) # slow_sum(不是 wrapper!)
print(slow_sum.__doc__) # 计算 1 到 n 的和(故意用慢方法)
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
🔑 functools.wraps 的作用:装饰器默认返回 wrapper 函数,它的 __name__ 和 __doc__ 变成了 wrapper 的——@wraps(func) 把这些元信息复制回来。这是新手最容易忘的一步——但很关键。
# 3.6.3 带参数的装饰器
当装饰器本身需要参数时,要再包一层——"三层嵌套"的最外层是装饰器工厂:
import time
from functools import wraps
def retry(max_attempts=3, delay=0.5):
"""重试装饰器——带参数版本。
三层嵌套:
① retry(max_attempts, delay) → 返回真正的装饰器
② decorator(func) → 返回 wrapper
③ wrapper(*args, **kwargs) → 执行逻辑 + 重试
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"[重试 {attempt}/{max_attempts}] {func.__name__} 失败:{e}")
if attempt == max_attempts:
raise # 最后一次仍失败——向上抛
time.sleep(delay)
return None # 不会执行到这里——但 linter 喜欢
return wrapper
return decorator
# 用法:直接传参数到装饰器
@retry(max_attempts=5, delay=1.0)
def fetch_data(url):
"""模拟不稳定的网络请求"""
import random
if random.random() < 0.7:
raise ConnectionError("网络超时")
return f"数据来自 {url}"
# 测试
try:
print(fetch_data("https://api.example.com"))
except Exception:
print("最终请求失败")
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
🖼️ 图解三层嵌套的执行流程:
@retry(max_attempts=5, delay=1.0)
def fetch_data(url):
...
→ retry(5, 1.0) ← 第一层:返回 decorator
→ decorator(fetch_data) ← 第二层:返回 wrapper(捕获了 max_attempts、delay)
→ wrapper("url") ← 第三层:实际调用,带重试逻辑(处于第一层的闭包中)
2
3
4
5
6
7
# 3.6.4 多个装饰器叠加
装饰器可以从下往上叠加——离函数最近的先执行:
@timer # 第 2 层:先套上的最后执行
@retry(max_attempts=3) # 第 1 层:先执行
def unstable_work():
...
# 等价于:unstable_work = timer(retry(unstable_work, max_attempts=3))
# retry 先装饰 → timer 再装饰
2
3
4
5
6
7
# 3.6.5 装饰器 vs C++ 对比
| 维度 | Python 装饰器 | C++ |
|---|---|---|
| 实现 | 函数/类 + @ 语法 | 没有直接等价物 |
| 最接近的 C++ 特性 | — | 包装器类(template<typename F> class Retry: public F {...}) 或宏 |
| 侵入性 | 零——不改原函数 | 必须改调用方或写模板包装 |
| AOP(面向切面编程) | 天然支持 | 需要代码生成/宏 |
# 3.6.6 常用内置装饰器速查
| 装饰器 | 作用 | 示例 |
|---|---|---|
@property | 方法变属性(见第 4 章 §4.1.4) | @property; def x(self): ... |
@staticmethod | 静态方法——无需 self | @staticmethod; def util(): ... |
@classmethod | 类方法——第一个参数是 cls | @classmethod; def from_json(cls, data): ... |
@functools.wraps | 保留被装饰函数的元信息 | @wraps(func) |
@functools.lru_cache | LRU 缓存——自动记忆化 | @lru_cache(maxsize=128) |
@dataclass(Python 3.7+) | 自动生成 __init__/__repr__ | @dataclass; class Point: x: int; y: int |
# 3.6.7 综合案例与思考
综合案例:登录校验装饰器 + 性能缓存组合
"""
装饰器组合实战——登录验证 + 函数结果缓存
"""
import time
from functools import wraps, lru_cache
# ---- 模拟登录状态 ----
_current_user = None
def login(username):
global _current_user
_current_user = username
def logout():
global _current_user
_current_user = None
def require_login(func):
"""装饰器:要求已登录才能调用"""
@wraps(func)
def wrapper(*args, **kwargs):
if _current_user is None:
raise PermissionError("请先登录")
print(f"[{_current_user}] 正在调用 {func.__name__}()")
return func(*args, **kwargs)
return wrapper
# ---- 组合使用 ----
@timer # 第二层:计时(先套的最后执行)
@require_login # 第一层:鉴权(先执行)
@lru_cache(maxsize=128) # 最里层:缓存(对函数调用方能消费为止)
def compute_heavy(n: int) -> int:
"""模拟一个耗时的计算"""
time.sleep(0.2)
return n * n
# ===== 测试 =====
if __name__ == "__main__":
# 未登录调用——被 require_login 拦截
try:
compute_heavy(10)
except PermissionError as e:
print(f"❌ {e}")
# 登录后调用
login("张三")
print(compute_heavy(10)) # 第 1 次:0.2s
print(compute_heavy(10)) # 第 2 次:缓存命中,瞬间返回
print(compute_heavy(2)) # 新参数:计算一次
print(compute_heavy(10)) # 依旧缓存命中
# 查看缓存统计
print(f"\n缓存状态:{compute_heavy.cache_info()}")
logout()
print("已登出")
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
运行输出:
❌ 请先登录
[张三] 正在调用 compute_heavy()
⏱ compute_heavy() 耗时 0.2045 秒
100
100(瞬时——缓存命中)
⏱ compute_heavy() 耗时 0.2012 秒
4
100(瞬时——缓存命中)
缓存状态:CacheInfo(hits=2, misses=2, maxsize=128, currsize=2)
已登出
2
3
4
5
6
7
8
9
10
11
案例知识融合:这个案例展示了装饰器的三个层次——自定义装饰器(require_login、timer)与内置装饰器(@lru_cache)的组合叠加、@wraps 保留元信息、functools.lru_cache 的自动记忆化——一个 @ 符号背后是完整的闭包 + 高阶函数 + 函数组合的体系。
思考题:
- 三个装饰器
@timer→@require_login→@lru_cache从下往上套——它们的执行顺序是如何确定的?画图说明。 @lru_cache(maxsize=128)的缓存原理是什么?如果被缓存的函数参数是可变对象(如list),会发生什么?@classmethod和@staticmethod有什么本质区别?(提示:第 4 章 §4.1.1 会讨论类——读完回来对照看)
# 3.7 新手陷阱
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | 可变默认参数 | def f(lst=[]) 的 lst 只有一份——多次调用共享,用 lst=None + 内部创建 |
| 2 | 循环中 lambda 延迟绑定 | [lambda: i for i in range(3)] 全部返回 2——lambda 在调用时才取 i 当前值,用 lambda i=i: i 固定 |
| 3 | 修改全局变量忘写 global | count += 1 报 UnboundLocalError——读不需要 global,写必须声明 |
| 4 | 缩进混用 Tab 和空格 | IndentationError——统一 4 空格,IDE 设 "Tab 转空格" |
| 5 | 装饰器忘写 @wraps | 被装饰函数的 __name__/__doc__ 丢失——永远在 wrapper 里加 @functools.wraps(func) |
陷阱 2 详解——闭包延迟绑定:
# ❌ 经典错误:lambda 在调用时才查找 i——此时 i 已经是 2
funcs = [lambda: print(i) for i in range(3)]
for f in funcs:
f()
# 输出:2 2 2(全部打印 2!不是 0 1 2!)
# ✅ 修复:用默认参数在定义时"冻结"i 的值
funcs = [lambda i=i: print(i) for i in range(3)]
for f in funcs:
f()
# 输出:0 1 2 ✅
2
3
4
5
6
7
8
9
10
11
# 3.8 综合思考题
for...else的设计争议:Guido(Python 之父)曾承认for...else的命名不够直观——很多人以为是"for 跑完了 else"。如果让你重新设计这个特性,你会怎么命名?你会用怎样的语法来表达"循环正常结束没有 break"的语义?海象运算符
:=的边界::=可以用在if、while、列表推导中,但不能用在赋值语句中。a := 3是语法错误——必须写成(a := 3)。为什么 Python 做这个限制?:=和=的核心语义区别是什么?参数传递:传值还是传引用? Python 经常被描述为"传对象引用"(pass by object reference)——既不是传值也不是传引用。不可变对象(如 int)"表现像传值",可变对象(如 list)"表现像传引用"。这个描述准确吗?从
id()的角度论证你的观点。闭包 vs 类:闭包和类都能封装状态——
make_counter返回的闭包和class Counter在功能上是等价的。在什么场景下闭包优于类?在什么场景下类优于闭包?Python 社区的主流倾向是什么?lambda 的局限性:Python 的 lambda 被故意设计成只能包含单个表达式——这与其他语言(JS 的
=>、C++ 的[]{})形成对比。你认同这个设计吗?如果 lambda 能包含多条语句,Python 的函数式编程生态会改变吗?