编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • ScriptHub 脚本工具箱
  • Python

    • Python 从入门到实战
    • 入门与基础类型
    • 序列与集合类型
    • 流程控制与函数
      • 3.1 流程控制
        • 3.1.1 if-elif-else 条件分支
        • 3.1.2 for 循环与 range
        • range() 的三种形态
        • enumerate():同时取索引和值
        • zip():并行遍历多个序列
        • 3.1.3 while 循环
        • 3.1.4 break / continue / pass
        • 3.1.5 循环的 else 子句
        • 3.1.6 海象运算符 :=
        • 3.1.7 综合案例与思考
      • 3.2 函数的定义与调用
        • 3.2.1 def 定义函数
        • 3.2.2 函数返回值
        • 3.2.3 综合案例与思考
      • 3.3 函数参数详解
        • 3.3.1 位置参数
        • 3.3.2 默认参数
        • 3.3.3 可变参数 *args
        • 3.3.4 关键字参数 **kwargs
        • 3.3.5 参数组合顺序
        • 3.3.6 综合案例与思考
      • 3.4 作用域与命名空间
        • 3.4.1 LEGB 规则
        • 3.4.2 global 与 nonlocal
        • 3.4.3 闭包初探
        • 3.4.4 综合案例与思考
      • 3.5 Lambda 匿名函数
        • 3.5.1 综合案例与思考
      • 3.6 装饰器
        • 3.6.1 装饰器的本质
        • 3.6.2 超时器装饰器——完整案例
        • 3.6.3 带参数的装饰器
        • 3.6.4 多个装饰器叠加
        • 3.6.5 装饰器 vs C++ 对比
        • 3.6.6 常用内置装饰器速查
        • 3.6.7 综合案例与思考
      • 3.7 新手陷阱
      • 3.8 综合思考题
    • 面向对象与工程
    • 爬虫全流程实战
    • 数据分析三件套
    • 办公自动化实战
    • 开发环境与规范
    • 调试与性能优化
    • 部署与并发实战
    • 函数高级特性剖析:装饰器 / 生成器 / 上下文管理器
    • 并发底层原理揭秘
    • 面向对象与类型系统:元类 / 描述符 / 鸭子类型
    • 解释器源码初探
  • Shell-Bash

  • 工具脚本

  • ScriptHub
  • Python
杨充
2021-06-08
目录

流程控制与函数

# 第 3 章 Python 流程控制与函数

# 目录介绍

  • 3.1 流程控制
    • 3.1.1 if-elif-else 条件分支
    • 3.1.2 for 循环与 range
    • 3.1.3 while 循环
    • 3.1.4 break / continue / pass
    • 3.1.5 循环的 else 子句
    • 3.1.6 海象运算符 :=
    • 3.1.7 综合案例与思考
  • 3.2 函数的定义与调用
    • 3.2.1 def 定义函数
    • 3.2.2 函数返回值
    • 3.2.3 综合案例与思考
  • 3.3 函数参数详解
    • 3.3.1 位置参数
    • 3.3.2 默认参数
    • 3.3.3 可变参数 *args
    • 3.3.4 关键字参数 **kwargs
    • 3.3.5 参数组合顺序
    • 3.3.6 综合案例与思考
  • 3.4 作用域与命名空间
    • 3.4.1 LEGB 规则
    • 3.4.2 global 与 nonlocal
    • 3.4.3 闭包初探
    • 3.4.4 综合案例与思考
  • 3.5 Lambda 匿名函数
    • 3.5.1 综合案例与思考
  • 3.6 装饰器
    • 3.6.1 装饰器的本质
    • 3.6.2 超时器装饰器
    • 3.6.3 带参数的装饰器
    • 3.6.4 多个装饰器叠加
    • 3.6.5 装饰器 vs C++ 对比
    • 3.6.6 常用内置装饰器速查
    • 3.6.7 综合案例与思考
  • 3.7 新手陷阱 Top 5
  • 3.8 综合思考题

# 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
1
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";
}
1
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("适龄劳动力")
1
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}")
1
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(按需计算)
1
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. 王五
1
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 被跳过了)
1
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
1
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=" ")
1
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                       # 类也先占位
1
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")   # ✅ 执行!
1
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("没找到")                   # 没找到才到这里
1
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]
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

⚠️ 节制使用::= 很酷,但滥用会让代码难读——只在"不赋值就重复计算"的场景用。

# 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
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
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

谢谢游玩,再见!👋
1
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 行代码完成了一个完整、健壮、可交互的游戏。

思考题:

  1. 第 max_attempts 次时 while 条件是 attempts < max_attempts,如果改成 <= 会发生什么?这种情况下 else 分支还会执行吗?
  2. for...else 和 while...else——else 的语义一直有争议。你认为它的设计合理吗?如果去掉 else,这个游戏需要怎么改?
  3. 海象运算 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
1
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;
}
1
2
3
4
# Python 版
def add(a, b):                 # 没有任何类型声明
    return a + b
1
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)
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

# 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__}")
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
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 文档:
        计算列表的统计信息。
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13

案例知识融合:这个案例展示了函数的三个核心方面——statistics 演示多值返回和空值保护、is_prime 演示类型注解 -> bool 和高效算法、fibonacci 演示循环 + 海象运算符——三个函数通过 __doc__ 统一文档风格。

思考题:

  1. Python 函数多值返回的底层是什么?如果只用一个变量接收 statistics([1,2,3]),这个变量是什么类型?
  2. def is_prime(n: int) -> bool 中的类型注解会被 Python 运行时强制检查吗?它的主要作用是什么?(提示:IDE 提示、mypy 静态检查)
  3. 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, "深圳")        # ❌ 位置不能在关键词之后
1
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
1
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
1
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("苹果", "香蕉", "橘子")
# - 苹果
# - 香蕉
# - 橘子
1
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}
1
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
1
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")                            # 触发错误处理
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
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 可以扩展到任意数量的命令。

思考题:

  1. dispatch 函数里的 handler(*args, **kwargs) 是什么?这一行背后发生了什么?
  2. def log_command(message, level="INFO", *, timestamp=True, **extra) 中的 * 不是 *args——它是什么意思?什么时候应该用它?
  3. 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 等)
1
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}")
1
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)
1
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>
1
2
3
4
5
6
7
8
9
10

⚠️ 永远不要给变量起名叫 len、list、print——这会遮蔽内置函数,引发灾难级的 debug 噩梦。

# 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
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

🔑 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)
1
2
3
4
5
6
7
8
9
10
11

图解闭包捕获状态:

make_multiplier(2) 调用结束后:
  factor = 2(本应被销毁——但被 multiply 捕获,保留在闭包中)

double ──→ [multiply 函数 + factor=2]
triple ──→ [multiply 函数 + factor=3]
1
2
3
4
5

🔑 C++ 闭包的对比:

// C++ Lambda——需要显式指定捕获方式
auto make_multiplier = [](int factor) {
    return [factor](int x) { return x * factor; };   // [=] 或 [factor]
};
1
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}")
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
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条)
1
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 写入——四个作用域层全部在同一个系统中自然登场。

思考题:

  1. make_logger 返回的 log 函数每次调用时 name 从哪里来?它是怎么"记住"创建时的 name 参数的?
  2. _log_history.pop(0) 是 O(n) 的操作——如果日志量很大(10万条),这会成为瓶颈吗?怎么优化?
  3. 如果去掉 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
1
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)
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
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
============================================================
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

案例知识融合:这个案例覆盖了 lambda 的全部典型场景——单级排序、多级排序(lambda p: (-p[2], p[1]) 用负号取反实现降序)、加权排序(lambda 内嵌复杂表达式)、IIFE 风格的价格分组——用无状态的纯函数式写法完成了多维度数据分析。

思考题:

  1. key=lambda p: (-p[2], p[1]) 中的 -p[2] 为什么能实现降序?这种技巧有什么局限性(提示:非数值类型)?
  2. 本案例中 IIFE lambda (lambda price: ...)(p[1]) 和直接用 if/elif/else 有什么区别?哪种写法更好?
  3. 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() 返回 你好!
# 你好!
1
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())                     # 和上面完全一样的输出
1
2
3
4
5
6

图解装饰器做了什么:

greet(原始函数)──→ with_log(greet) ──→ greet(新的 wrapper 函数,但保持原名)
                     │
                     └─ wrapper 内部:先 log → 调原 greet → log → 返回结果
1
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 的和(故意用慢方法)
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

🔑 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("最终请求失败")
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
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")              ← 第三层:实际调用,带重试逻辑(处于第一层的闭包中)
1
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 再装饰
1
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("已登出")
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
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)
已登出
1
2
3
4
5
6
7
8
9
10
11

案例知识融合:这个案例展示了装饰器的三个层次——自定义装饰器(require_login、timer)与内置装饰器(@lru_cache)的组合叠加、@wraps 保留元信息、functools.lru_cache 的自动记忆化——一个 @ 符号背后是完整的闭包 + 高阶函数 + 函数组合的体系。

思考题:

  1. 三个装饰器 @timer→@require_login→@lru_cache 从下往上套——它们的执行顺序是如何确定的?画图说明。
  2. @lru_cache(maxsize=128) 的缓存原理是什么?如果被缓存的函数参数是可变对象(如 list),会发生什么?
  3. @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 ✅
1
2
3
4
5
6
7
8
9
10
11

# 3.8 综合思考题

  1. for...else 的设计争议:Guido(Python 之父)曾承认 for...else 的命名不够直观——很多人以为是"for 跑完了 else"。如果让你重新设计这个特性,你会怎么命名?你会用怎样的语法来表达"循环正常结束没有 break"的语义?

  2. 海象运算符 := 的边界::= 可以用在 if、while、列表推导中,但不能用在赋值语句中。a := 3 是语法错误——必须写成 (a := 3)。为什么 Python 做这个限制?:= 和 = 的核心语义区别是什么?

  3. 参数传递:传值还是传引用? Python 经常被描述为"传对象引用"(pass by object reference)——既不是传值也不是传引用。不可变对象(如 int)"表现像传值",可变对象(如 list)"表现像传引用"。这个描述准确吗?从 id() 的角度论证你的观点。

  4. 闭包 vs 类:闭包和类都能封装状态——make_counter 返回的闭包和 class Counter 在功能上是等价的。在什么场景下闭包优于类?在什么场景下类优于闭包?Python 社区的主流倾向是什么?

  5. lambda 的局限性:Python 的 lambda 被故意设计成只能包含单个表达式——这与其他语言(JS 的 =>、C++ 的 []{})形成对比。你认同这个设计吗?如果 lambda 能包含多条语句,Python 的函数式编程生态会改变吗?

#Python#基础
上次更新: 2026/06/17, 12:47:39
序列与集合类型
面向对象与工程

← 序列与集合类型 面向对象与工程→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式