5 minutes
函数定义与参数传递
函数是组织代码的基本单元。它让你把一段逻辑封装起来,给它一个名字,然后在需要的地方反复调用。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)是函数式编程的核心概念:
- 相同输入永远得到相同输出
- 没有副作用(不修改外部状态)
# 纯函数
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: 函数定义、参数类型、作用域与递归基础。