参数详解
函数参数是函数与外部交互的接口。Python 提供了极其丰富的参数形式:位置参数、关键字参数、默认值、可变参数 *args 和 **kwargs、以及通过 / 和 * 分隔的"位置只参数"和"关键字只参数"。理解这些机制能让你编写出更灵活、更易用的 API。
位置参数
最普通的参数形式:调用时按位置顺序匹配。
def describe_person(name, age, city):
print(f"{name},{age} 岁,来自 {city}")
describe_person("Alice", 30, "北京")
# 输出:Alice,30 岁,来自 北京
位置必须严格对应,顺序错了结果就错了:
describe_person(30, "Alice", "北京")
# 输出:30,Alice 岁,来自 北京 ← 语义错误
关键字参数
调用时通过 参数名=值 的形式传递参数,可以避免顺序问题:
def describe_person(name, age, city):
print(f"{name},{age} 岁,来自 {city}")
describe_person(age=30, name="Alice", city="北京")
# 输出:Alice,30 岁,来自 北京
位置参数和关键字参数可以混用,但位置参数必须在关键字参数之前:
describe_person("Alice", city="北京", age=30) # 合法
# describe_person(name="Alice", 30, city="北京") # 语法错误!
:::warning 混用规则 位置参数不能出现在关键字参数之后,下面的写法会报错:
describe_person(name="Alice", 30, city="北京")
# SyntaxError: positional argument follows keyword argument
:::
默认参数值
可以为参数设置默认值,调用时若未提供该参数,则使用默认值:
def greet(name, greeting="你好"):
print(f"{greeting},{name}!")
greet("Alice") # 输出:你好,Alice!
greet("Bob", "Hi") # 输出:Hi,Bob!
greet("Charlie", greeting="早上好") # 输出:早上好,Charlie!
:::warning 默认参数必须是不可变对象 默认参数值在函数定义时只计算一次,因此可变对象作为默认值会引发"共享状态"问题:
def add_item(item, lst=[]): # 危险!
lst.append(item)
return lst
print(add_item(1)) # 输出:[1]
print(add_item(2)) # 输出:[1, 2] ← 不是预期的 [2]!
print(add_item(3)) # 输出:[1, 2, 3]
正确做法是使用 None 作为哨兵:
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
print(add_item(1)) # 输出:[1]
print(add_item(2)) # 输出:[2]
:::
可变位置参数 *args
使用 *args 收集多余的位置参数为一个元组:
def sum_all(*args):
"""对所有参数求和"""
print(f"args 类型:{type(args)}, 内容:{args}")
return sum(args)
print(sum_all(1, 2, 3, 4, 5))
# 输出:args 类型:<class 'tuple'>, 内容:(1, 2, 3, 4, 5)
# 输出:15
可以和普通参数组合:
def introduce(name, *hobbies):
print(f"我是 {name},爱好:{', '.join(hobbies)}")
introduce("Alice", "读书", "跑步", "编程")
# 输出:我是 Alice,爱好:读书, 跑步, 编程
可变关键字参数 **kwargs
使用 **kwargs 收集多余的关键字参数为一个字典:
def create_user(name, age, **kwargs):
"""创建用户,extra 字段保存在 kwargs 中"""
user = {"name": name, "age": age}
user.update(kwargs)
return user
user = create_user("Alice", 30, city="北京", email="alice@example.com", active=True)
print(user)
# 输出:{'name': 'Alice', 'age': 30, 'city': '北京', 'email': 'alice@example.com', 'active': True}
参数的完整顺序
完整参数定义顺序为:
def func(pos_only, /, normal, *args, kw_only, **kwargs):
↑ ↑ ↑ ↑
位置只参数 普通参数 关键字只参数 可变关键字
完整示例:
def create_report(
title, # 普通参数
/, # 之前的参数是位置只参数
author, # 普通参数
*sections, # 可变位置参数
format="pdf", # 关键字只参数(带默认值)
debug=False, # 关键字只参数
**metadata, # 可变关键字参数
):
print(f"标题:{title}")
print(f"作者:{author}")
print(f"章节:{sections}")
print(f"格式:{format}")
print(f"调试:{debug}")
print(f"元数据:{metadata}")
create_report(
"月报", "Alice",
"销售", "运营", "财务",
format="html",
debug=True,
version="2.0",
)
输出:
标题:月报
作者:Alice
章节:('销售', '运营', '财务')
格式:html
调试:True
元数据:{'version': '2.0'}
关键字只参数(用 * 分隔)
* 之后的参数必须以关键字形式传递,这能提升 API 的可读性:
def connect(host, port, *, timeout=10, retry=3):
"""连接服务器,timeout 和 retry 必须用关键字指定"""
print(f"连接 {host}:{port},超时 {timeout}s,重试 {retry} 次")
connect("localhost", 8080) # 合法
connect("localhost", 8080, timeout=5) # 合法
connect("localhost", 8080, retry=5, timeout=20) # 合法
# connect("localhost", 8080, 5) # 错误!timeout 必须用关键字
:::tip 何时使用关键字只参数
当一个函数有多个布尔型或数值型参数时,调用 func(True, False, True) 几乎无法阅读。强制使用关键字能让代码意图清晰:
send_email(to="alice@x.com", subject="通知", html=True, attach=False)
:::
位置只参数(用 / 分隔)
/ 之前的参数只能通过位置传递,不能使用关键字。Python 3.8+ 引入:
def power(base, exp, /):
"""计算 base 的 exp 次方"""
return base ** exp
print(power(2, 10)) # 合法
# print(power(base=2, exp=10)) # 错误!位置只参数不能用关键字
:::info 位置只参数的用途
- 参数名不固定时:调用者依赖位置而非参数名,未来可以重命名参数而不破坏调用方。
- 性能优化:某些场景下位置匹配更快。
- 与 C 函数一致:内置函数(如
len(obj)、pow(x, y, z))大多用位置只参数,避免obj等通用名称被当作关键字。 :::
参数解包(* 和 **)
调用函数时,可以使用 * 和 ** 将列表/元组或字典解包为参数。
解包列表/元组
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
print(add(*numbers)) # 等价于 add(1, 2, 3),输出:6
t = (10, 20, 30)
print(add(*t)) # 输出:60
解包字典
def create_user(name, age, city):
return {"name": name, "age": age, "city": city}
info = {"name": "Alice", "age": 30, "city": "北京"}
print(create_user(**info))
# 等价于 create_user(name="Alice", age=30, city="北京")
# 输出:{'name': 'Alice', 'age': 30, 'city': '北京'}
混合解包
def greet(greeting, name, punctuation):
return f"{greeting},{name}{punctuation}"
args = ("Hello",)
kwargs = {"name": "Alice", "punctuation": "!"}
print(greet(*args, **kwargs))
# 输出:Hello,Alice!
:::tip 解包的实际用途 参数解包在动态调用函数时特别有用,例如把参数从配置文件或数据库读取后再调用函数:
config = {"host": "localhost", "port": 8080, "timeout": 5}
connect(**config)
:::
强制类型注解的参数
类型注解和参数形式可以结合使用,让函数签名更清晰:
from collections.abc import Callable
def process_data(
data: list[int],
/,
*,
transform: Callable[[int], int] | None = None,
filter_fn: Callable[[int], bool] | None = None,
) -> list[int]:
"""处理整数列表。
Args:
data: 输入数据(位置只参数)。
transform: 转换函数,对每个元素应用。
filter_fn: 过滤函数,返回 True 才保留。
Returns:
处理后的列表。
"""
result = data
if filter_fn is not None:
result = [x for x in result if filter_fn(x)]
if transform is not None:
result = [transform(x) for x in result]
return result
print(process_data([1, 2, 3, 4, 5], transform=lambda x: x * x, filter_fn=lambda x: x % 2 == 0))
# 输出:[4, 16]
实战:灵活的日志函数
下面实现一个灵活的日志函数,综合运用各种参数形式:
from datetime import datetime
def log(
message: str,
/,
*,
level: str = "INFO",
tag: str | None = None,
extra: dict | None = None,
**context,
) -> str:
"""格式化日志消息。
Args:
message: 日志内容(位置只参数,避免与 message 关键字冲突)。
level: 日志级别,如 INFO/WARN/ERROR。
tag: 自定义标签。
extra: 额外的结构化字段(与 context 互斥使用更清晰)。
**context: 任意键值对上下文信息。
Returns:
格式化后的日志字符串。
"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
parts = [f"[{timestamp}]", f"[{level}]"]
if tag:
parts.append(f"[{tag}]")
parts.append(message)
fields = {}
if extra:
fields.update(extra)
fields.update(context)
if fields:
kv = " ".join(f"{k}={v}" for k, v in fields.items())
parts.append(f"({kv})")
return " ".join(parts)
# 基本用法
print(log("服务启动完成"))
# 输出示例:[2025-01-15 10:30:00] [INFO] 服务启动完成
# 指定级别和标签
print(log("数据库连接失败", level="ERROR", tag="DB"))
# 输出示例:[2025-01-15 10:30:00] [ERROR] [DB] 数据库连接失败
# 使用 extra 字典
print(log("用户登录", extra={"user": "Alice", "ip": "192.168.1.1"}))
# 输出示例:[2025-01-15 10:30:00] [INFO] 用户登录 (user=Alice ip=192.168.1.1)
# 使用 **context 直接传关键字
print(log("请求处理完成", level="DEBUG", method="GET", path="/api/users", status=200))
# 输出示例:[2025-01-15 10:30:00] [DEBUG] 请求处理完成 (method=GET path=/api/users status=200)
# 参数解包
ctx = {"request_id": "abc-123", "user_id": 42}
print(log("开始处理", **ctx))
# 输出示例:[2025-01-15 10:30:00] [INFO] 开始处理 (request_id=abc-123 user_id=42)
:::tip 设计 API 的原则
- 默认参数放在末尾,让常见的简单调用只需少量参数
- 布尔/数值型参数尽量设为关键字只参数,避免
func(True, False)这种难读的调用 - 接收任意元数据时使用
**kwargs,比硬编码一堆参数更灵活 :::
小结
- 位置参数按顺序匹配;关键字参数通过
name=value传递,更可读 - 默认参数值让函数调用更灵活,但可变对象作默认值是个陷阱,应使用
None哨兵 *args收集多余位置参数为元组;**kwargs收集多余关键字参数为字典/之前的参数是位置只参数;*之后的参数是关键字只参数- 调用函数时可用
*和**解包列表/字典为参数 - 完整参数顺序:
pos_only, /, normal, *args, kw_only, **kwargs - 类型注解可以与所有参数形式结合,提升代码可读性
下一节将介绍 Lambda 匿名函数,一种简洁地定义简单函数的方式。