01-函数基础
Python 版本要求:Python 3.11+ 贯穿项目:functions_demo/ 代码位置:
app/core/functions.py测试验证:cd functions_demo && uv run pytest -k TestFunctionsBasics -v
本章代码基于 Python 3.11+ 编写
函数是代码复用的基础,掌握函数定义和调用是编程的核心技能。
概念铺垫
为什么需要函数?一个真实的重复代码场景
问题场景: 你在开发一个学生成绩管理系统,需要计算多个班级的平均分。
不使用函数的麻烦:
# 班级 A 的成绩
scores_a = [85, 92, 78, 96, 88]
total_a = 0
for score in scores_a:
total_a += score
average_a = total_a / len(scores_a)
print(f"班级 A 平均分:{average_a}")
# 班级 B 的成绩
scores_b = [90, 87, 93, 85, 91]
total_b = 0
for score in scores_b:
total_b += score
average_b = total_b / len(scores_b)
print(f"班级 B 平均分:{average_b}")
# 班级 C 的成绩...又要复制粘贴一遍?问题:
- 同样的代码重复写多次
- 如果计算逻辑要修改,需要改多处
- 代码冗长,难以维护
使用函数的解决方案:
def calculate_average(scores: list[int]) -> float:
"""计算平均分"""
return sum(scores) / len(scores)
# 简洁调用
scores_a = [85, 92, 78, 96, 88]
scores_b = [90, 87, 93, 85, 91]
print(f"班级 A 平均分:{calculate_average(scores_a)}")
print(f"班级 B 平均分:{calculate_average(scores_b)}")这就是函数的价值:把重复的代码打包起来,一次定义,多次使用。
函数解决了什么问题?
函数的本质是:给一段代码起个名字,需要时直接调用。
就像你把常用的菜谱写下来,以后照着做就行,不用每次重新想。
函数的优势:
- 避免重复:同样的代码写一次,调用多次
- 分而治之:把大问题拆成小函数,各个击破
- 易于修改:改一处函数,所有调用处自动生效
- 便于测试:可以单独验证每个函数是否正确
L1 理解层:会用
函数的最简用法
函数的核心操作:定义、调用、返回值。
# 定义函数
def greet(name: str) -> str:
"""打招呼"""
return f"你好,{name}!"
# 调用函数
message = greet("张三")
print(message) # 你好,张三!这就是函数的基本用法。接下来我们详细学习函数的所有功能。
第一部分:函数的定义和调用
什么是函数
函数(Function) 是一段有名字的、可以反复调用的代码块。
┌─────────────────────────────────────────────────────────────┐
│ 函数 = 打包好的代码 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 输入(参数) │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 函数内部 │ ← 处理逻辑 │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ 输出(返回值) │
│ │
└─────────────────────────────────────────────────────────────┘基本语法
def 函数名(参数列表) -> 返回类型:
"""文档字符串(可选)"""
函数体
return 返回值示例代码
# 定义函数
def greet() -> None:
"""打招呼的函数"""
print("Hello, World!")
# 调用函数(可以多次调用)
greet() # 输出:Hello, World!
greet() # 输出:Hello, World!
greet() # 输出:Hello, World!语法要点:
| 元素 | 说明 |
|---|---|
def | 关键字,宣告"我要定义一个函数" |
| 函数名 | 用动词命名,如 calculate_area、get_user |
() | 括号,即使没有参数也不能省略 |
-> | 返回类型注解(Python 3.5+,可选) |
: | 冒号,不能省略 |
| 缩进 | 函数体必须缩进(通常 4 个空格) |
第二部分:带参数的函数
单个参数
def greet(name: str) -> None:
"""向指定的人打招呼"""
print(f"Hello, {name}!")
greet("Alice") # Hello, Alice!
greet("Bob") # Hello, Bob!多个参数
def introduce(name: str, age: int, city: str) -> None:
"""自我介绍"""
print(f"我叫 {name},今年 {age} 岁,来自 {city}")
introduce("张三", 25, "北京")
# 我叫 张三,今年 25 岁,来自 北京第三部分:返回值
return 语句
def add(a: int, b: int) -> int:
"""计算两个数的和"""
result = a + b
return result
answer = add(3, 5)
print(answer) # 8
# 返回值可以直接参与计算
print(add(10, 20) * 2) # 60没有 return 的函数
def say_hello() -> None:
print("Hello!")
result = say_hello() # 打印 Hello!
print(result) # None重要: Python 中所有函数都有返回值。没有写 return 时,自动返回 None。
多个返回值
def get_info() -> tuple[str, int, str]:
"""返回多个值(实际返回元组)"""
name = "Alice"
age = 25
city = "北京"
return name, age, city
# 接收多个返回值
n, a, c = get_info()
print(n, a, c) # Alice 25 北京
# 或者作为元组接收
info = get_info()
print(info) # ('Alice', 25, '北京')第四部分:函数文档字符串
docstring 的作用
def calculate_area(width: float, height: float) -> float:
"""
计算矩形面积
Args:
width: 宽度(正数)
height: 高度(正数)
Returns:
面积值
Example:
>>> calculate_area(5, 3)
15
"""
return width * height
# 查看文档
print(calculate_area.__doc__)
# 使用 help() 查看完整文档
help(calculate_area)渐进复杂:从简单到复杂的函数
层级 1:无参数无返回值
def say_hi() -> None:
print("Hi!")
say_hi()层级 2:有参数无返回值
def greet(name: str) -> None:
print(f"Hello, {name}!")
greet("张三")层级 3:有参数有返回值
def add(a: int, b: int) -> int:
return a + b
result = add(1, 2)层级 4:多参数多返回值
def divide_and_remainder(a: int, b: int) -> tuple[int, int]:
"""返回商和余数"""
return a // b, a % b
quotient, remainder = divide_and_remainder(10, 3)
print(f"商:{quotient},余数:{remainder}")层级 5:带类型注解的完整函数
def calculate_grade(
scores: list[float],
weights: list[float] | None = None
) -> dict[str, float | str]:
"""
计算成绩等级
Args:
scores: 分数列表
weights: 权重列表(可选)
Returns:
包含平均分和等级的字典
"""
if weights:
total = sum(s * w for s, w in zip(scores, weights))
avg = total / sum(weights)
else:
avg = sum(scores) / len(scores)
if avg >= 90:
grade = "A"
elif avg >= 80:
grade = "B"
elif avg >= 70:
grade = "C"
elif avg >= 60:
grade = "D"
else:
grade = "F"
return {"average": avg, "grade": grade}
# 使用
result = calculate_grade([85, 90, 78, 92])
print(result) # {'average': 86.25, 'grade': 'B'}关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
weights: list[float] | None = None | 权重参数默认为 None | 用 None 而非 [] 作默认值,避免可变默认参数陷阱 |
sum(s * w for s, w in zip(scores, weights)) | 将每个分数与对应权重相乘后求和 | zip 把两个列表配对,生成器表达式避免创建中间列表 |
total / sum(weights) | 除以权重总和而非元素数量 | 权重可能不等于 1,必须按实际权重比例归一化 |
-> dict[str, float | str] | 返回值为含 float 或 str 的字典 | 函数同时返回数值(平均分)和字符串(等级),用联合类型精确标注 |
实际应用:项目代码示例
本节展示 functions_demo/app/core/functions.py 中的实际代码,涵盖基础函数定义、高阶函数和递归。
基础函数:成绩等级与平均分
def letter_grade(score: float) -> str:
"""按百分制返回等级"""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
return "F"
def calculate_average(scores: list[float]) -> float:
"""计算平均分"""
if not scores:
return 0.0
return sum(scores) / len(scores)这两个函数展示了单一职责原则——letter_grade 只负责等级转换,calculate_average 只负责平均分计算。
高阶函数:函数作为参数和返回值
from collections.abc import Callable
def apply_to_all(
scores: list[float], transform: Callable[[float], float]
) -> list[float]:
"""对所有分数应用变换(函数作为参数)"""
return [transform(s) for s in scores]
def make_threshold_checker(threshold: float) -> Callable[[float], bool]:
"""返回一个"是否达线"检查函数(函数作为返回值)"""
def checker(score: float) -> bool:
return score >= threshold
return checker使用示例:
# 函数作为参数:对每个成绩应用平方根变换
import math
scores = [81, 64, 100]
transformed = apply_to_all(scores, math.sqrt) # [9.0, 8.0, 10.0]
# 函数作为返回值:创建个性化的检查器
is_excellent = make_threshold_checker(90)
is_excellent(95) # True
is_excellent(85) # False递归:归并排序
项目代码中包含一个递归实现的 merge_sort,展示了函数调用自身的经典场景:
def merge_sort(items: list[float]) -> list[float]:
"""归并排序(递归实现)
拆分 → 递归排序左右子列 → 合并
"""
if len(items) <= 1:
return list(items)
mid = len(items) // 2
left = merge_sort(items[:mid])
right = merge_sort(items[mid:])
return _merge(left, right)递归的核心是基线条件(len(items) <= 1)和递归步骤(拆分后调用自身)。
L2 实践层:最佳实践
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
| 单一职责 | 每个函数只做一件事,便于测试和复用 | calculate_average() 只计算平均分,不打印结果 |
| 函数名用动词 | 语义清晰,一看就知道做什么 | get_user()、calculate_total()、validate_input() |
| 参数数量 <= 5 | 参数太多难以理解和调用 | 超过 5 个参数考虑用数据类或字典封装 |
| 添加类型注解 | IDE 提示更准确,静态检查可发现错误 | def add(a: int, b: int) -> int: |
| 编写 docstring | 自动生成文档,help() 可查看说明 | """计算平均分""" |
| 用 None 作默认值 | 避免可变默认参数陷阱 | def func(items: list | None = None): |
| 早返回 | 减少嵌套层级,代码更扁平 | 不符合条件的尽早 return |
反模式:不要这样做
# ❌ 函数做了太多事
def process_user(name, scores):
# 计算、判断、打印、保存...全在一起
avg = sum(scores) / len(scores)
if avg >= 60:
print(f"{name} 通过")
save_to_db(name, avg)
else:
print(f"{name} 未通过")
# 问题:
# 1. 一个函数做了计算、判断、打印、保存四个动作
# 2. 无法单独测试"计算"部分
# 3. 如果不需要打印,必须修改函数
# 4. 如果保存逻辑变化,也要修改这个函数# ✅ 正确做法:拆分为单一职责的函数
def calculate_average(scores: list[float]) -> float:
"""只负责计算平均分"""
return sum(scores) / len(scores)
def is_passed(average: float) -> bool:
"""只负责判断是否通过"""
return average >= 60
def format_result(name: str, passed: bool) -> str:
"""只负责格式化结果字符串"""
status = "通过" if passed else "未通过"
return f"{name} {status}"
def process_user(name: str, scores: list[float]) -> None:
"""协调各函数完成流程"""
avg = calculate_average(scores)
passed = is_passed(avg)
print(format_result(name, passed))
# 保存逻辑可以单独处理# ❌ 函数名不清楚含义
def f(x, y):
return x * y + 10
def data(a):
return a * 0.8
# 问题:看到调用 f(3, 5) 时,完全不知道做什么# ✅ 正确做法:用描述性的动词命名
def calculate_adjusted_price(base_price: float, quantity: int) -> float:
"""计算调整后的价格"""
return base_price * quantity + 10
def apply_discount(price: float) -> float:
"""应用折扣"""
return price * 0.8# ❌ 参数太多,顺序容易搞混
def create_user(name, age, city, email, phone, address, company, department):
pass
create_user("张三", 25, "北京", "test@email.com", "13800000000",
"朝阳区", "ABC公司", "研发部") # 顺序错了很难发现# ✅ 正确做法:用数据类封装参数
from dataclasses import dataclass
@dataclass
class UserInfo:
name: str
age: int
city: str
email: str
phone: str = ""
address: str = ""
company: str = ""
department: str = ""
def create_user(user: UserInfo) -> dict:
"""创建用户"""
return {"name": user.name, "email": user.email}
# 调用时更清晰
user_data = UserInfo(name="张三", age=25, city="北京", email="test@email.com")
create_user(user_data)# ❌ 深层嵌套
def process_data(data):
if data:
if data["type"] == "A":
if data["value"] > 0:
if data["status"] == "active":
return data["value"] * 2
return 0
# 问题:4层嵌套,难以阅读和测试# ✅ 正确做法:早返回(Guard Clause)
def process_data(data: dict | None) -> int:
"""处理数据"""
if not data:
return 0
if data["type"] != "A":
return 0
if data["value"] <= 0:
return 0
if data["status"] != "active":
return 0
return data["value"] * 2适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 计算逻辑 | ✅ 推荐 | 纯函数,无副作用,易于测试 |
| 数据转换 | ✅ 推荐 | 输入→输出,语义清晰 |
| 验证检查 | ✅ 推荐 | 返回布尔值,可复用 |
| 打印输出 | ❓ 看情况 | 如果只是为了打印,考虑直接写脚本 |
| 全局状态修改 | ❌ 不推荐 | 副作用难以追踪,优先用类封装 |
| 超过 20 行 | ❌ 不推荐 | 拆分成多个小函数 |
L3 专家层:底层原理
Python 如何实现函数调用
Python 函数调用涉及调用栈和帧对象(Frame Object)的底层机制。
函数调用栈结构:
┌─────────────────────────────────────────────────────────────┐
│ Python 调用栈 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 栈顶(当前执行) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Frame: inner() │ │
│ │ 局部变量: z = "inner" │ │
│ │ 代码指针: 执行到 print(z) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ 压栈 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Frame: outer() │ │
│ │ 局部变量: y = "outer" │ │
│ │ 代码指针: inner() 调用处 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ 压栈 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Frame: main 模块 │ │
│ │ 局部变量: x = "global" │ │
│ │ 代码指针: outer() 调用处 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 调用流程: │
│ 1. 函数调用 → 创建新帧对象压入栈顶 │
│ 2. 帧对象存储:局部变量、代码位置、返回地址 │
│ 3. 函数返回 → 弹出帧对象,恢复上一帧执行 │
│ │
└─────────────────────────────────────────────────────────────┘# 查看帧对象
import sys
def show_frame() -> None:
"""演示帧对象"""
frame = sys._getframe() # 获取当前帧
print(f"函数名: {frame.f_code.co_name}")
print(f"局部变量: {frame.f_locals}")
print(f"行号: {frame.f_lineno}")
print(f"调用者: {frame.f_back.f_code.co_name if frame.f_back else 'None'}")
def caller() -> None:
x = 10
show_frame()
caller()
# 函数名: show_frame
# 局部变量: {'frame': <frame object at ...>}
# 行号: 8
# 调用者: caller帧对象的关键属性
| 属性 | 含义 | 用途 |
|---|---|---|
f_code | 代码对象 | 包含函数的字节码、常量、参数信息 |
f_locals | 局部变量字典 | 函数内的所有局部变量 |
f_globals | 全局变量字典 | 模块级别的变量 |
f_lineno | 当前执行行号 | 调试时定位错误位置 |
f_back | 上一帧(调用者) | 追溯调用链 |
性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 函数调用 | O(1) | 帧对象创建是固定开销 |
| 参数传递 | O(n) | n 为参数数量,打包成元组 |
| 返回值传递 | O(1) | 直接返回对象引用 |
| 局部变量查找 | O(1) | 使用数组索引,不是字典查找 |
性能测试:函数调用开销
import timeit
# 直接计算
inline_time = timeit.timeit("1 + 1", number=1000000)
# 约 0.03 秒
# 函数调用计算
def add(a: int, b: int) -> int:
return a + b
func_time = timeit.timeit("add(1, 1)", globals=globals(), number=1000000)
# 约 0.08 秒
# 结论:函数调用有额外开销(约 2-3 倍)
# 但代码组织性更重要,除非极端性能场景优化建议:
# ❌ 不必要的函数调用(循环内反复调用)
def is_valid(x: int) -> bool:
return x > 0
result = []
for x in range(1000000):
if is_valid(x): # 每次循环都调用函数
result.append(x)
# ✅ 简单判断直接写(热点代码)
result = []
for x in range(1000000):
if x > 0: # 直接判断,无函数调用开销
result.append(x)
# ✅ 或用列表推导式(编译优化)
result = [x for x in range(1000000) if x > 0]设计动机
Python 为什么这样设计函数?
| 设计选择 | 原因 | 替代方案对比 |
|---|---|---|
def 关键字 | 语义明确,一眼识别函数定义 | JavaScript 用 function,更长 |
| 无类型声明(可选注解) | 动态灵活,不强制类型 | Java/C++ 强制类型,编译检查但繁琐 |
return 可选 | 没有 return 自动返回 None | Go 强制返回值,更严格 |
| 多返回值(元组) | 简洁,无需定义结构体 | C 返回单一值或指针参数 |
| docstring 一等公民 | 文档与代码绑定,help() 直接看 | Java 文档分离(Javadoc) |
函数对象本质:
# 函数是对象,可以像普通对象操作
def greet(name: str) -> str:
return f"Hello, {name}"
# 函数对象属性
print(greet.__name__) # 'greet'
print(greet.__doc__) # None(没写 docstring)
print(greet.__code__) # 代码对象
# 函数可以赋值、传递、存储
say_hello = greet # 赋值给另一个变量
functions = [greet, len] # 存在列表里
result = say_hello("Alice") # 通过新名字调用知识关联
函数知识关联图:
┌───────────────┐
│ 函数对象 │
│ __code__ │
└───────────────┘
│
↓
┌─────────────┐ ┌───────────────┐ ┌───────────────┐
│ 帧对象 │────→│ 函数基础 │────→│ 调用栈 │
│ sys._getframe│ │ def/return │ │ 执行流程 │
└─────────────┘ └───────────────┘ └───────────────┘
│
↓
┌───────────────┐
│ 闭包 │
│ nonlocal │
└───────────────┘
│
↓
┌───────────────┐
│ 装饰器 │
│ 函数包装 │
└───────────────┘交互演示
运行项目 CLI 查看本章代码的实际执行效果:
cd functions_demo && uv run python -m app # 选 1本章小结
┌─────────────────────────────────────────────────────────────┐
│ 函数基础 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ L1 理解层: │
│ ✓ 函数是打包好的代码块,可以反复调用 │
│ ✓ 作用:避免重复、分而治之、易于修改、便于测试 │
│ ✓ 定义语法:def 函数名(参数) -> 返回类型: │
│ ✓ return 返回结果,没有 return 返回 None │
│ │
│ L2 实践层: │
│ ✓ 单一职责:一个函数只做一件事 │
│ ✓ 函数名用动词:语义清晰 │
│ ✓ 参数数量 <= 5:太多则封装为数据类 │
│ ✓ 早返回:减少嵌套层级 │
│ │
│ L3 专家层: │
│ ✓ 函数调用涉及帧对象和调用栈 │
│ ✓ 帧对象存储局部变量、代码位置、返回地址 │
│ ✓ 函数调用有固定开销,热点代码可内联 │
│ ✓ 函数是一等对象,可赋值、传递、存储 │
│ │
└─────────────────────────────────────────────────────────────┘