定义函数
函数是组织代码的基本单元:把一段可复用的逻辑封装起来,给它取个名字,通过参数接收输入、通过返回值输出结果。当脚本超过几十行,就该用函数拆分逻辑。本节讲解 def 定义、参数类型、返回值、默认参数陷阱、*args / **kwargs,以及文档字符串与类型提示。
前置知识
阅读本节前,建议先了解:条件判断与循环,掌握基本控制流。
基础概念
Python 函数用 def 关键字定义。参数(Parameter) 是定义时的占位名,实参(Argument) 是调用时传入的具体值,二者常被混称但含义不同。
Python 是动态类型语言,参数不声明类型,但推荐使用类型提示(Type Hints) 标注参数与返回值,便于静态检查与 IDE 补全:
- 位置参数(Positional Argument):按顺序传值。
- 关键字参数(Keyword Argument):按名称传值。
- 默认参数(Default Argument):不传时使用默认值。
- 可变参数(Variadic Argument):
*args收集多余位置参数,**kwargs收集多余关键字参数。
语法与用法
函数定义与调用的最基本写法:
python
# 最简可运行示例
def greet(name: str) -> str:
"""返回一句问候语"""
return f"你好,{name}!"
message = greet("小明")
print(message)
print(greet(name="小红")) # 关键字调用text
你好,小明!
你好,小红!详细说明
参数类型详解
python
def create_profile(
name: str, # 位置参数
age: int, # 位置参数
city: str = "北京", # 默认参数(必须放在位置参数之后)
*, # 之后的参数只能按关键字传递
email: str | None = None,
) -> dict[str, object]:
"""创建用户档案字典"""
return {"name": name, "age": age, "city": city, "email": email}
# 全部按位置传递
print(create_profile("alice", 30))
# 混合使用:位置 + 关键字
print(create_profile("bob", 25, city="上海"))
# 星号 `*` 之后的参数必须用关键字传递
print(create_profile("cathy", 28, email="cathy@example.com"))
# 错误!email 在 * 之后,不能按位置传递
# create_profile("david", 40, "深圳", "david@example.com") # 错误!text
{'name': 'alice', 'age': 30, 'city': '北京', 'email': None}
{'name': 'bob', 'age': 25, 'city': '上海', 'email': None}
{'name': 'cathy', 'age': 28, 'city': '北京', 'email': 'cathy@example.com'}返回值与多返回值
Python 函数没有返回值时隐式返回 None;返回多个值实际是返回一个元组,调用方可以解包(Unpacking):
python
def divide(a: float, b: float) -> float:
"""两数相除"""
return a / b
def stat(numbers: list[float]) -> tuple[float, float, float]:
"""返回 (最小值, 最大值, 平均值)"""
return min(numbers), max(numbers), sum(numbers) / len(numbers)
print(divide(10, 4))
# 解包多返回值
lowest, highest, average = stat([88.5, 92.0, 79.0, 95.5])
print(f"最低 {lowest},最高 {highest},平均 {average:.2f}")
# 不需要的值用下划线占位
_, _, avg = stat([1.0, 2.0, 3.0])
print(f"平均值:{avg:.2f}")
# 无返回值函数
def log(message: str) -> None:
"""打印日志(无返回值)"""
print(f"[LOG] {message}")
result = log("服务启动")
print(f"返回值:{result}")text
2.5
最低 79.0,最高 95.5,平均 88.75
平均值:2.00
[LOG] 服务启动
返回值:None默认参数的陷阱与可变参数
绝不要用可变对象作为默认值
默认值在函数定义时只求值一次并保存。用 [] / {} 作默认值,多次调用会共享同一个对象,导致数据「串味」。
python
# 错误!可变默认值会在多次调用间共享
def add_item_wrong(item: str, items: list[str] = []) -> list[str]:
items.append(item)
return items
print(add_item_wrong("a"))
print(add_item_wrong("b")) # 期望 ['b'],实际 ['a', 'b']
# 正确:默认值用 None,函数内新建
def add_item(item: str, items: list[str] | None = None) -> list[str]:
if items is None:
items = []
items.append(item)
return items
print(add_item("a"))
print(add_item("b"))text
['a']
['a', 'b']
['a']
['b']可变参数让函数接收任意数量的参数:
python
def sum_all(*numbers: float) -> float:
"""求和任意数量的数字,args 是一个元组"""
return sum(numbers)
print(sum_all(1, 2, 3))
print(sum_all(1.5, 2.5, 3.5, 4.5))
def build_query(table: str, **conditions: object) -> str:
"""根据关键字参数拼装查询条件,kwargs 是一个字典"""
if not conditions:
return f"SELECT * FROM {table}"
where = " AND ".join(f"{k} = {v!r}" for k, v in conditions.items())
return f"SELECT * FROM {table} WHERE {where}"
print(build_query("users"))
print(build_query("users", age=30, city="北京"))
# 调用时用 * 解包序列、用 ** 解包字典
numbers = [1, 2, 3, 4]
print(f"解包求和:{sum_all(*numbers)}")
params = {"table": "orders", "status": "paid"}
print(build_query(**params))text
6
12.0
SELECT * FROM users
SELECT * FROM users WHERE age = 30 AND city = '北京'
解包求和:10
SELECT * FROM orders WHERE status = 'paid'文档字符串与类型提示
python
def calc_bmi(weight_kg: float, height_m: float) -> float:
"""计算身体质量指数(BMI)。
参数:
weight_kg: 体重,单位千克
height_m: 身高,单位米
返回:
BMI 数值,保留两位小数
异常:
ValueError: 身高或体重非正数时抛出
"""
if weight_kg <= 0 or height_m <= 0:
raise ValueError("身高和体重必须是正数")
return round(weight_kg / (height_m ** 2), 2)
print(calc_bmi(70.0, 1.75))
print(calc_bmi.__doc__.splitlines()[0]) # 访问文档字符串
# 现代类型提示:联合类型、容器泛型
from collections.abc import Callable, Iterable
def apply_all(
func: Callable[[int], int],
values: Iterable[int],
) -> list[int]:
"""对每个元素应用函数并返回列表"""
return [func(v) for v in values]
print(apply_all(lambda x: x * 2, range(5)))text
22.86
计算身体质量指数(BMI)。
[0, 2, 4, 6, 8]作用域与 LEGB 规则
变量查找遵循 LEGB 规则:Local → Enclosing → Global → Built-in:
python
counter = 0 # 全局变量
def increment() -> None:
global counter # 声明要修改全局变量
counter += 1
def outer() -> int:
value = 10 # 外层(Enclosing)变量
def inner() -> int:
return value + 5 # 访问闭包变量
return inner()
increment()
increment()
print(f"全局计数:{counter}")
print(f"闭包结果:{outer()}")
# 不推荐:尽量少用 global,改用返回值传递状态
def increment_pure(current: int) -> int:
"""纯函数版本:通过参数与返回值传递状态"""
return current + 1
total = 0
total = increment_pure(total)
total = increment_pure(total)
print(f"纯函数计数:{total}")text
全局计数:2
闭包结果:15
纯函数计数:2实战示例
示例一:数据校验工具函数
python
from dataclasses import dataclass
@dataclass
class User:
"""用户信息"""
name: str
email: str
age: int
def validate_user(user: User) -> list[str]:
"""校验用户信息,返回所有错误信息列表(空列表代表通过)"""
errors: list[str] = []
if not user.name or len(user.name) > 20:
errors.append("姓名不能为空且不超过 20 字符")
if "@" not in user.email or "." not in user.email.split("@")[-1]:
errors.append(f"邮箱格式非法:{user.email}")
if not 0 < user.age < 150:
errors.append(f"年龄不合法:{user.age}")
return errors
def format_errors(errors: list[str]) -> str:
"""把错误列表格式化为可读文本"""
if not errors:
return "校验通过"
return "校验失败:\n" + "\n".join(f" - {e}" for e in errors)
users = [
User("alice", "alice@example.com", 30),
User("", "bob_at_example", 200),
]
for user in users:
print(f"[{user.name or '匿名'}] {format_errors(validate_user(user))}")text
[alice] 校验通过
[匿名] 校验失败:
- 姓名不能为空且不超过 20 字符
- 邮箱格式非法:bob_at_example
- 年龄不合法:200示例二:函数组合与缓存
python
from functools import lru_cache, reduce
from time import perf_counter
# 用 lru_cache 缓存递归结果,把指数级复杂度降为线性
@lru_cache(maxsize=None)
def fib(n: int) -> int:
"""计算第 n 个斐波那契数(带缓存)"""
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
start = perf_counter()
print(f"fib(80) = {fib(80)}")
print(f"耗时:{perf_counter() - start:.6f} 秒")
# 用 reduce 组合函数实现管道
pipeline = [
str.strip,
str.lower,
lambda s: s.replace(" ", "-"),
lambda s: f"user/{s}",
]
slug = reduce(lambda acc, func: func(acc), pipeline, " Alice Wang ")
print(f"流水线结果:{slug}")
# 函数作为一等公民:传给高阶函数
def is_even(n: int) -> bool:
return n % 2 == 0
print(f"偶数:{list(filter(is_even, range(10)))}")text
fib(80) = 23416728348467685
耗时:0.000045 秒
流水线结果:user/alice-wang
偶数:[0, 2, 4, 6, 8]示例三:装饰器入门
装饰器本质是「接收函数、返回新函数」的高阶函数,用于横切关注点(日志、计时、权限):
python
from functools import wraps
from time import perf_counter
from typing import Any, Callable
def timer(func: Callable[..., Any]) -> Callable[..., Any]:
"""装饰器:统计函数执行耗时"""
@wraps(func) # 保留原函数的名称与文档
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = perf_counter()
result = func(*args, **kwargs)
elapsed = perf_counter() - start
print(f"{func.__name__} 耗时 {elapsed * 1000:.2f} ms")
return result
return wrapper
@timer
def heavy_task(n: int) -> int:
"""模拟耗时的计算任务"""
return sum(i * i for i in range(n))
print(f"结果:{heavy_task(200_000)}")
print(f"函数名保留:{heavy_task.__name__}")
print(f"文档保留:{heavy_task.__doc__}")text
heavy_task 耗时 8.43 ms
结果:2666646666700000
函数名保留:heavy_task
文档保留:模拟耗时的计算任务注意事项
- 可变对象不能作默认值:用
None占位,函数内再创建。 - 默认参数必须放在非默认参数之后:
def f(a=1, b):是语法错误。 - 函数返回
None不要写return:直接省略或写裸return均可。 *之后是仅关键字参数:调用时必须写参数名,可读性更好。- 修改全局变量需
global:但优先用返回值传递状态,减少副作用。 - 函数名会被后定义的覆盖:同名函数只保留最后一个定义。
- 参数是引用传递:函数内修改可变参数(如
list.append)会影响调用方。 - 递归必须有终止条件:Python 默认递归深度约 1000,超出抛
RecursionError。
python
# 参数按引用传递:修改可变参数会影响外部
def append_note(notes: list[str]) -> None:
notes.append("已处理") # 就地修改,外部可见
data = ["任务一"]
append_note(data)
print(f"外部数据:{data}")
# 正确做法:需要不修改原数据时,先复制
def append_note_safe(notes: list[str]) -> list[str]:
return [*notes, "已处理"] # 返回新列表,不改原对象
data2 = ["任务一"]
result = append_note_safe(data2)
print(f"原数据:{data2},新数据:{result}")text
外部数据:['任务一', '已处理']
原数据:['任务一'],新数据:['任务一', '已处理']最佳实践
- 函数只做一件事:「函数名里出现
and」通常意味着该拆分。 - 显式标注类型提示:
def f(x: int) -> str:让 IDE 与mypy提供帮助。 - 写文档字符串:用
"""一句话说明"""开头,必要时补充参数与返回说明。 - 参数不宜过多:超过 5 个考虑用数据类(dataclass)打包。
- 避免副作用:优先「纯函数」——相同输入永远得到相同输出。
- 用
*分隔仅关键字参数:布尔开关类参数强制使用关键字,避免误传。 - 提前返回代替深层嵌套:卫语句让主流程保持在最外层缩进。
- 装饰器记得加
@wraps:否则原函数的名称与文档会丢失。
python
# 推荐:用关键字参数表达意图,避免布尔陷阱
def fetch_data(url: str, *, use_cache: bool = True, timeout: float = 5.0) -> str:
"""获取数据(use_cache 必须用关键字传递)"""
return f"GET {url}(cache={use_cache}, timeout={timeout})"
print(fetch_data("https://example.com")) # 清晰的调用
print(fetch_data("https://example.com", use_cache=False))
# 错误!位置参数会让 use_cache 与 timeout 含义混淆
# fetch_data("https://example.com", False, 3.0) # 不推荐text
GET https://example.com(cache=True, timeout=5.0)
GET https://example.com(cache=False, timeout=5.0)下一节
继续学习:编写完整脚本