跳到主要内容

参数详解

函数参数是函数与外部交互的接口。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 位置只参数的用途

  1. 参数名不固定时:调用者依赖位置而非参数名,未来可以重命名参数而不破坏调用方。
  2. 性能优化:某些场景下位置匹配更快。
  3. 与 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 匿名函数,一种简洁地定义简单函数的方式。