函数是组织代码的基本单元。它让你把一段逻辑封装起来,给它一个名字,然后在需要的地方反复调用。Python 的函数设计灵活而强大——从最简单的位置参数到复杂的 *args**kwargs,从类型注解到嵌套函数,掌握函数就等于掌握了代码复用的钥匙。

def 语句与函数定义

Python 使用 def 关键字定义函数,基本语法如下:

def greet(name):
    """向指定的人打招呼"""
    return f"Hello, {name}!"

print(greet("Alice"))  # Hello, Alice!

函数的组成部分:

部分 说明
def 关键字 声明这是一个函数
函数名 greet 遵循变量命名规则,建议用蛇形命名法
参数列表 (name) 输入,可以为空 ()
冒号 : 标志函数头结束
函数体(缩进块) 实际执行的代码
return 语句 返回值,没有 return 则返回 None

函数定义只是创建了一个可调用对象,函数体在调用之前不会执行:

def lazy():
    print("这行代码被推迟执行了!")

print("函数已定义,但尚未执行")  # 先执行
lazy()  # 现在才执行函数体

参数 vs 参数

初学者经常混淆这两个概念:

  • 参数(Parameter):函数定义时写在括号里的变量名,是"形参"
  • 参数(Argument):调用函数时传入的具体值,是"实参"
def add(x, y):      # x 和 y 是参数(Parameter)
    return x + y

result = add(3, 5)  # 3 和 5 是参数(Argument)

用中文记忆:“参数"是定义时的占位符,“参数"是调用时塞进去的值。

参数类型

Python 的参数系统非常灵活,支持多种传递方式。

位置参数(Positional Arguments)

按顺序一一对应传入,这是最简单的形式:

def profile(name, age, city):
    print(f"{name}{age}岁,来自{city}")

profile("张三", 28, "北京")  # 必须按顺序
# 输出:张三,28岁,来自北京

关键字参数(Keyword Arguments)

调用时通过 参数名=值 的方式传参,顺序可以自由调整:

profile(city="上海", name="李四", age=35)
# 输出:李四,35岁,来自上海

关键字参数让代码的可读性大幅提升——调用者一眼就能看出每个值的含义。

混合使用

位置参数和关键字参数可以混合使用,但位置参数必须在关键字参数之前

profile("王五", city="广州", age=22)  # 正确
# profile(city="广州", "王五", 22)    # 错误!位置参数不能在关键字之后

默认参数(Default Parameters)

定义时为参数指定默认值,调用时可以省略:

def greet(name, greeting="你好"):
    print(f"{greeting}{name}!")

greet("小明")              # 你好,小明!
greet("John", "Hello")    # Hello,John!

⚠️ 重要警告:默认参数的求值时机!

默认参数只在函数定义时求值一次,而不是每次调用时都重新求值。这意味着不要使用可变对象作为默认值

# 错误示范——暴露了默认参数的陷阱
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['a', 'b']  —— 同一个列表!
print(add_item("c"))  # ['a', 'b', 'c']

正确的做法是使用 None 作为默认值,在函数体内部创建可变对象:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['b']  —— 每次都是新列表
print(add_item("c"))  # ['c']

*args —— 可变位置参数

当你不知道会传入多少个位置参数时,使用 *args。它会将多余的参数打包成元组:

def sum_all(*numbers):
    """接收任意多个数字,返回它们的和"""
    total = 0
    for n in numbers:
        total += n
    return total

print(sum_all(1, 2))           # 3
print(sum_all(1, 2, 3, 4, 5))  # 15

约定俗成的名称是 *args(arguments 的缩写),但星号 * 才是关键:

def log(*items):
    for item in items:
        print(f"[日志] {item}")

log("启动", "连接数据库", "查询完成")

拆包(Unpacking)与 *

* 也能在调用函数时使用,将一个可迭代对象拆成独立的位置参数:

def point(x, y, z):
    print(f"({x}, {y}, {z})")

coords = [10, 20, 30]
point(*coords)  # 等价于 point(10, 20, 30)
# 输出:(10, 20, 30)

# 字符串也可以拆
greeting = "PY"
print(*greeting)  # P Y

**kwargs —— 可变关键字参数

使用 **kwargs 接收任意数量的关键字参数,打包成字典:

def build_profile(**info):
    """构建用户档案"""
    print("用户信息:")
    for key, value in info.items():
        print(f"  {key}: {value}")

build_profile(name="Alice", age=30, city="Tokyo", job="Engineer")
# 用户信息:
#   name: Alice
#   age: 30
#   city: Tokyo
#   job: Engineer

字典拆包

同样,调用时也可以用 ** 拆包字典:

def connect(host, port, user, password):
    print(f"连接 {user}@{host}:{port}")

config = {
    "host": "localhost",
    "port": 5432,
    "user": "admin",
    "password": "secret",
}

connect(**config)  # 等价于 connect(host="localhost", port=5432, ...)

参数组合顺序

当一个函数同时使用多种参数时,顺序必须严格遵守:

def 函数名(位置参数, *args, 默认参数, **kwargs):
    pass

具体来说:

def complex_func(a, b, *args, option=True, **kwargs):
    """展示参数组合顺序"""
    print(f"位置参数: a={a}, b={b}")
    print(f"可变位置: {args}")
    print(f"默认参数: option={option}")
    print(f"可变关键字: {kwargs}")

complex_func(1, 2, 3, 4, 5, option=False, name="test", verbose=True)
# 位置参数: a=1, b=2
# 可变位置: (3, 4, 5)
# 默认参数: option=False
# 可变关键字: {'name': 'test', 'verbose': True}

Python 3.8+ 还引入了仅限位置参数的语法,使用 / 分隔:

def divide(a, b, /):
    """a 和 b 只能用位置参数传入"""
    return a / b

divide(10, 3)       # 正确
# divide(a=10, b=3)  # 错误!TypeError

以及仅限关键字参数,使用 * 分隔:

def safe_divide(a, b, *, round_result=True):
    """round_result 只能用关键字参数传入"""
    result = a / b
    return round(result, 2) if round_result else result

safe_divide(10, 3, round_result=True)   # 正确
# safe_divide(10, 3, True)              # 错误!TypeError

组合使用:

def func(positional_only, /, positional_or_keyword, *, keyword_only):
    pass

返回值与多值返回

单返回值

函数默认返回 None。有 return 语句时返回指定值:

def nothing():
    pass

print(nothing())  # None

def square(x):
    return x ** 2

print(square(4))  # 16

多值返回——实际上是返回元组

def min_max(numbers):
    """返回序列的最小值和最大值"""
    return min(numbers), max(numbers)

result = min_max([3, 1, 7, 2, 9])
print(result)       # (1, 9)
print(type(result)) # <class 'tuple'>

# 直接用拆包接收
low, high = min_max([3, 1, 7, 2, 9])
print(f"最小值: {low}, 最大值: {high}")
# 最小值: 1, 最大值: 9

提前 return

def safe_divide(a, b):
    if b == 0:
        return None  # 提前返回,避免除零错误
    return a / b

print(safe_divide(10, 0))  # None
print(safe_divide(10, 2))  # 5.0

注意:一个函数中可以有多个 return 语句,但只会执行第一个被命中的。

文档字符串(Docstring)

PEP 257 定义了 Python 文档字符串的规范。文档字符串是紧跟在函数定义后的第一个字符串字面量:

def factorial(n):
    """计算 n 的阶乘。

    Args:
        n: 非负整数

    Returns:
        n 的阶乘

    Raises:
        ValueError: 如果 n 为负数
    """
    if n < 0:
        raise ValueError("阶乘要求非负数")
    if n == 0:
        return 1
    return n * factorial(n - 1)

通过 .__doc__help() 可以查看文档字符串:

print(factorial.__doc__)  # 打印文档字符串
help(factorial)           # 更友好的格式输出

常用的文档字符串风格:

  • Google Style(上例所示):章节清晰,被广泛使用
  • NumPy/SciPy Style:适合科学计算项目
  • Sphinx/RST Style:适合需要自动生成文档的项目

类型提示(Type Hints)

PEP 484 引入了类型注解,让 Python 函数签名更加清晰。类型提示不影响运行时行为,但能帮助工具(IDE、mypy)做静态检查:

def add(x: int, y: int) -> int:
    return x + y

def greet(name: str, age: int) -> str:
    return f"{name} 今年 {age} 岁"

def lookup(key: str, data: dict[str, int]) -> int | None:
    return data.get(key)

常用类型注解

from typing import Optional, List, Tuple, Dict, Any, Union

# 简单类型
def encode(text: str, encoding: str = "utf-8") -> bytes: ...

# 集合类型(Python 3.9+ 直接用内置类型)
def process(items: list[int]) -> dict[str, int]: ...

# 可选类型
def find_user(uid: int) -> Optional[str]: ...

# 联合类型(Python 3.10+ 用 | 语法)
def parse(value: int | str) -> float | None: ...

# 任意类型
def log(message: Any) -> None: ...

使用 typing 模块

from typing import Callable, Iterator

# 函数作为参数
def apply(func: Callable[[int], str], value: int) -> str:
    return func(value)

# 生成器类型
def count_up_to(n: int) -> Iterator[int]:
    for i in range(n):
        yield i

嵌套函数

在函数内部再定义函数,这就是嵌套函数(Nested Function):

def outer(text):
    """外部函数"""
    def inner():
        """内部函数——只能在 outer 内部访问"""
        print(f"内部函数说: {text}")
    
    print("外部函数开始")
    inner()
    print("外部函数结束")

outer("Hello")
# 外部函数开始
# 内部函数说: Hello
# 外部函数结束

# inner()  # 错误!NameError,内部函数在外部不可见

嵌套函数的核心用途是封装——把只在某个函数内部需要的逻辑藏起来,不污染全局命名空间。

递归基础

函数调用自身就是递归。递归的核心是基线条件(停止条件)递归步骤

def factorial(n: int) -> int:
    """递归计算阶乘"""
    # 基线条件
    if n <= 1:
        return 1
    # 递归步骤
    return n * factorial(n - 1)

print(factorial(5))  # 120

# 用递归遍历嵌套结构
def flatten(items):
    """展开嵌套列表"""
    result = []
    for item in items:
        if isinstance(item, list):
            result.extend(flatten(item))  # 递归
        else:
            result.append(item)
    return result

nested = [1, [2, [3, 4], 5], 6]
print(flatten(nested))  # [1, 2, 3, 4, 5, 6]

Python 默认的递归深度限制为 1000,可以通过 sys.setrecursionlimit() 调整,但不建议做太深的递归——Python 没有尾递归优化。

纯函数的概念

纯函数(Pure Function)是函数式编程的核心概念:

  1. 相同输入永远得到相同输出
  2. 没有副作用(不修改外部状态)
# 纯函数
def add(a, b):
    return a + b

# 不纯的函数 —— 修改了外部变量
total = 0
def add_to_total(x):
    global total
    total += x
    return total

# 不纯的函数 —— 依赖外部状态
import random
def roll_dice():
    return random.randint(1, 6)  # 每次调用结果不同

纯函数的优点:易于测试、易于推理、可以缓存、线程安全。

函数是一等公民

在 Python 中,函数是一等公民(First-Class Citizen)——可以赋值给变量、作为参数传递、作为返回值:

def shout(text):
    return text.upper()

def whisper(text):
    return text.lower()

# 函数赋值给变量
my_func = shout
print(my_func("hello"))  # HELLO

# 函数作为参数
def apply(func, value):
    return func(value)

print(apply(shout, "python"))   # PYTHON
print(apply(whisper, "PYTHON")) # python

这个特性是下一章"高阶函数"的基础,我们在那里会深入探索。

实战练习

练习 1:灵活的日志函数

def log_message(message, level="INFO", *, timestamp=False):
    """格式化日志输出"""
    if timestamp:
        from datetime import datetime
        prefix = f"[{datetime.now():%H:%M:%S}][{level}]"
    else:
        prefix = f"[{level}]"
    print(f"{prefix} {message}")

# 测试
log_message("系统启动")                    # [INFO] 系统启动
log_message("磁盘空间不足", "WARN")        # [WARN] 磁盘空间不足
log_message("连接超时", "ERROR", timestamp=True)
# [14:30:25][ERROR] 连接超时

练习 2:通用的 CSV 行构建器

def build_csv_row(*values, delimiter=","):
    """将多个值拼接为 CSV 行"""
    escaped = []
    for v in values:
        s = str(v)
        if delimiter in s or '"' in s or '\n' in s:
            s = '"' + s.replace('"', '""') + '"'
        escaped.append(s)
    return delimiter.join(escaped)

print(build_csv_row("Alice", 28, "Engineer"))
# Alice,28,Engineer

print(build_csv_row('Bob "The Builder"', 35, 'Dev,Ops'))
# "Bob ""The Builder""",35,"Dev,Ops"

小结

本章我们学习了 Python 函数的核心知识:

  • def 定义函数,函数体缩进,return 返回值
  • 位置参数、关键字参数、默认参数的用法与组合顺序
  • *args 收集多余位置参数为元组,**kwargs 收集多余关键字参数为字典
  • 默认参数陷阱——可变对象作为默认值只求值一次
  • 函数支持多值返回(实际是返回元组,调用侧拆包)
  • 文档字符串(PEP 257)记录函数用途,类型注解(PEP 484)提供静态类型信息
  • 嵌套函数实现封装,递归处理自相似结构
  • 纯函数(无副作用、引用透明)是函数式编程的基础
  • 函数作为一等公民——可赋值、传参、返回

下一步: 深入了解函数的作用域规则和闭包——你将看到嵌套函数如何"记住"其创建时的环境。

Summary: 函数定义、参数类型、作用域与递归基础。