随着 AI 任务复杂度增加,如果把所有代码堆叠在单个 Notebook 单元格或主脚本中,程序会迅速变得难以调试、难以复用,也极易出现全局变量污染。函数(Function) 是编程中最重要的抽象手段,它把复杂的计算逻辑封装为带明确输入(参数)和输出(返回值)的黑盒。
完成本节后,你应能:
/)与仅关键字参数(*),写出接口明确的可复用函数。global/nonlocal 机制以及可变默认参数陷阱的底层原理。*args 和 **kwargs 处理不定长参数与配置解包。lambda、map、filter 与列表推导式的适用边界。把函数看作一台工业流水线机器:给它输入原料(参数),它经过内部逻辑加工,产出成品(返回值)。外界不需要关心机器内部的局部临时变量;机器也不应该隐式修改外界的全局状态。明确的输入与输出契约,是编写高质量 AI 系统的核心。
在 Python 中使用 def 关键字定义函数。函数体需缩进,使用 return 语句返回结果。若没有显示的 return,默认返回 None。
def calculate_loss(y_true: float, y_pred: float) -> float:
"""计算均方误差 (MSE) 损失值。"""
error = y_true - y_pred
return error ** 2
loss = calculate_loss(1.0, 0.8)
print(f"Loss: {loss:.4f}") # Loss: 0.0400
在处理边界条件时,推荐使用 卫语句(Guard Clause) 提前返回,避免深层嵌套的 if-else:
def safe_divide(numerator: float, denominator: float) -> float | None:
# 卫语句:优先拦截非法输入
if denominator == 0:
print("警告:分母为 0,返回 None")
return None
return numerator / denominator
Python 函数可以返回多个值,本质上是把多个对象包装为一个元组(Tuple)返回:
def evaluate_predictions(y_true: list[int], y_pred: list[int]):
correct = sum(t == p for t, p in zip(y_true, y_pred))
total = len(y_true)
accuracy = correct / total if total > 0 else 0.0
return correct, total, accuracy # 返回元组 (correct, total, accuracy)
# 使用元组解包接收返回值
correct_cnt, total_cnt, acc = evaluate_predictions([1, 0, 1], [1, 1, 1])
print(f"准确率: {acc:.2%} ({correct_cnt}/{total_cnt})")
name=value 形式传参,顺序可自由调整。def train_model(data: list, epochs: int = 10, lr: float = 0.001):
print(f"训练数据量: {len(data)}, 轮数: {epochs}, 学习率: {lr}")
# 混合调用
train_model([1, 2, 3]) # 均使用默认配置
train_model([1, 2, 3], 20) # 位置传参覆盖 epochs
train_model([1, 2, 3], lr=0.01, epochs=5) # 关键字传参指定配置
/) 与仅关键字参数 (*)在复杂的 AI 框架或可复用工具库中,为了保护 API 契约或防止误传实参,使用 / 和 * 进行参数边界分割:
/):/ 左侧的参数只能通过位置传递,不能使用 name=value。*):* 右侧的参数必须通过关键字传递。def configure_pipeline(
dataset_path: str, # 只能按位置传递 (仅位置参数)
/,
mode: str = "train", # 位置或关键字传递
*,
batch_size: int = 32, # 只能按关键字传递 (仅关键字参数)
learning_rate: float = 0.01
):
print(f"加载路径: {dataset_path}, 模式: {mode}, BatchSize: {batch_size}, LR: {learning_rate}")
# 正确调用:
configure_pipeline("./data/iris.csv", "train", batch_size=64, learning_rate=0.005)
# 错误调用示例:
# configure_pipeline(dataset_path="./data/iris.csv")
# -> TypeError: configure_pipeline() got some positional-only arguments passed as keyword arguments: 'dataset_path'
# configure_pipeline("./data/iris.csv", "train", 64)
# -> TypeError: configure_pipeline() takes from 1 to 2 positional arguments but 3 were given
参数排列的标准公式如下:
作用域(Scope)决定了变量在代码中的可见性与生命周期。
当 Python 遇到一个变量名时,会严格按照 L E G B 的单向顺序查找变量:
.py 模块顶层定义的全局变量。len、range、ValueError)。
点击放大查看图:Python 作用域与 LEGB 查找规则
global 与 nonlocal 关键字默认情况下,在函数内部给变量赋值会创建一个局部变量。如果需要修改外层作用域的变量,需显式声明:
global:声明修改全局模块层级的变量。nonlocal:声明修改外层嵌套函数的变量(常用于闭包)。counter = 0
def increment_global():
global counter
counter += 1 # 显式修改全局变量
def make_accumulator():
total = 0
def add(val: float):
nonlocal total # 声明修改闭包外层变量
total += val
return total
return add
acc = make_accumulator()
print(acc(10)) # 10
print(acc(5)) # 15
避坑防范:内置名称遮蔽 (Built-in Shadowing)
切勿将变量或参数命名为内置函数名(如 len, list, dict, str, type, input 等)。这会把内置名称遮蔽掉,导致后续代码调用内置函数报错:
list = [1, 2, 3] # 危险!遮蔽了内置类型 list
# new_data = list("abc") # TypeError: 'list' object is not callable
这是 Python 初学者最容易遇到的隐蔽 Bug:函数的默认参数对象在函数定义(模块装载)时仅被创建一次,而不是在每次调用时新建!
若使用可变对象(列表、字典、集合)作为默认参数,多次调用将共享同一个内存地址。
点击放大查看图:可变默认参数陷阱内存对比图
当不确定调用者会传入多少参数,或者需要透传配置字典时,使用可变参数。
*args 与 **kwargs 机制*args:将多余的位置实参打包为一个元组 (tuple)。**kwargs:将多余的关键字实参打包为一个字典 (dict)。在调用函数时,使用 * 和 ** 可以将已有的列表、元组或字典解包展开后传给函数:
点击放大查看图:*args 与 **kwargs 参数打包与解包图
lambda 匿名函数lambda 用于定义简洁的单行匿名函数。语法为 lambda arg1, arg2: expression。它的主体只能是一个单行表达式,不能包含多行赋值或复杂的循环语句。
# 常见用法:作为排序或过滤的 key
samples = [("cat", 0.95), ("dog", 0.88), ("bird", 0.99)]
# 按置信度得分降序排序
sorted_samples = sorted(samples, key=lambda item: item[1], reverse=True)
print(sorted_samples) # [('bird', 0.99), ('cat', 0.95), ('dog', 0.88)]
map、filter 与列表推导式的对比map() 和 filter() 返回惰性迭代器(Iterator),在现代 Python 中,通常更推荐使用列表推导式,因为它具有更好的可读性和效率。
| 工具 | 语法示例 | 特点与适用边界 |
|---|---|---|
map | map(lambda x: x*2, data) | 惰性求值;需要配合 list() 显式转换;在处理现有内置函数(如 map(int, str_list))时极简 |
filter | filter(lambda x: x>0, data) | 惰性过滤符合条件的元素 |
| 列表推导式 | [x*2 for x in data if x>0] | (推荐) 语法直观,可同时完成变换与过滤,读写性能优异 |
raw_scores = [-0.5, 0.2, 0.8, -0.1]
# map + filter 方式
positive_doubled_map = list(map(lambda x: x * 2, filter(lambda x: x > 0, raw_scores)))
# 列表推导式方式 (更具可读性)
positive_doubled_comp = [x * 2 for x in raw_scores if x > 0]
print(positive_doubled_comp) # [0.4, 1.6]
为了让团队和 AI 助手准确理解函数用途,应遵循 Google 风格的文档字符串规范:
def normalize_features(features: list[float], min_val: float = 0.0, max_val: float = 1.0) -> list[float]:
"""对连续数值特征进行 Min-Max 归一化缩放。
Args:
features: 待缩放的原始数值列表。
min_val: 缩放后的最小值,默认为 0.0。
max_val: 缩放后的最大值,默认为 1.0。
Returns:
缩放到指定范围 [min_val, max_val] 后的新列表。
Raises:
ValueError: 当输入列表为空或 max_val <= min_val 时抛出。
"""
if min_val >= max_val:
raise ValueError("max_val 必须大于 min_val")
if not features:
return []
f_min, f_max = min(features), max(features)
if f_min == f_max:
return [min_val] * len(features)
return [min_val + (x - f_min) / (f_max - f_min) * (max_val - min_val) for x in features]
类型标注提升了 IDE 智能补全和 mypy 静态检查的能力。
关键澄清:Python 的 Type Hints 属于静态契约,解释器在运行时默认不会强制拦截不匹配的类型。如需运行时类型校验,可结合 pydantic 或 beartype 等第三方库。
版本兼容性写法区别:
# Python 3.10+ 原生简洁写法:
def parse_config(path: str | None = None) -> dict[str, float]:
...
# Python 3.7 - 3.9 兼容写法 (通过 typing 模块导入):
from typing import Optional, Union, Any
def parse_config_compat(path: Optional[str] = None) -> dict[str, Any]:
...
在机器学习实践中,应当把数据加载、前处理、预测规则与评估绘图拆解为解耦的独立函数。
点击放大查看图:模块化 AI 数据预处理与评估流水线架构图
| 现象 / 报错信息 | 常见根本原因 | 规范处理方式 |
|---|---|---|
UnboundLocalError: local variable 'x' referenced before assignment | 在函数内部给与全局同名的变量 x 赋值,导致 x 被 Python 判定为局部变量,但在赋值前就尝试读取它 | 若需读取并修改全局变量,显式加上 global x;更推荐将 x 作为参数传入并返回 |
TypeError: func() missing 1 required positional argument | 函数定义中缺少默认值的必填形参在调用时未被传入 | 检查形参列表,补全对应位置的实参 |
| 默认参数每次调用互相影响、数据污染 | 使用了列表、字典或集合等可变对象作为默认参数(如 def fn(x=[])) | 改用 def fn(x=None) 并在函数内部初始化 if x is None: x = [] |
TypeError: load() got positional-only arguments passed as keyword: 'path' | 试图给在 / 仅位置参数定界符左侧的参数使用 path="..." 关键字传参 | 对 / 左侧的参数一律使用位置传参 |
TypeError: func() takes 0 positional arguments but 1 was given | 参数定义中使用了 *(仅关键字参数),但调用时使用了位置传参 | 在调用该参数时显式加上关键字名称 key=value |
| 变量类型与 Type Hints 不符但代码正常运行未报错 | Python 解释器默认不会在运行时自动拦截校验 Type Hints | 使用 mypy 静态检查,或借助 pydantic/beartype 库进行运行时强制强校验 |
TypeError: 'list' object is not callable | 局部或全局变量覆盖了 Python 内置标识符 list = [...] | 检查变量名,禁止使用 list, dict, len, str, input 等内置关键字命名 |
/(仅位置参数)与 *(仅关键字参数)规范函数契约。global 与 nonlocal。None 哨兵模式。*args 和 **kwargs,并在实参传递时进行展开解包。lambda、map、filter 与列表推导式的适用场景边界。def append_to(element, target=[]) 会在多次调用中产生隐蔽的数据污染?如何安全修复?func(*args, **kwargs) 中,* 和 ** 分别代表什么含义?def train(model_name, /, *, lr=0.01) 中,/ 和 * 分别对 model_name 和 lr 的传参方式做了怎样的限制?len = 10 会发生什么?下一章:06.模块、包与第三方库。你将学习如何把本章编写的独立函数拆分保存到不同的 .py 模块中,并通过 import 组织大中型 AI 项目。