Skip to content

定义函数 ​

函数是组织代码的基本单元:把一段可复用的逻辑封装起来,给它取个名字,通过参数接收输入、通过返回值输出结果。当脚本超过几十行,就该用函数拆分逻辑。本节讲解 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)

下一节

继续学习:编写完整脚本

参考链接 ​