函数的参数
参数是函数与调用方之间的数据接口。合理设计参数,可以让函数既容易调用,又能清楚表达每个输入的用途。
本文使用 Python 3.10 及以上版本。读完后,你应该能够:
- 区分形参、实参、位置传递和关键字传递;
- 正确使用默认参数、
*args与**kwargs; - 使用
/和*约束调用方式; - 理解对象共享、可变默认值和参数解包的常见陷阱。
形参与实参
def calculate_total(price, quantity):
return price * quantity
total = calculate_total(20, 3)
- 定义函数时的
price、quantity是形参,相当于函数内部的局部变量; - 调用函数时的
20、3是实参,是本次调用提供的实际值。
每次调用都会为形参绑定新的值,不同调用互不影响。
位置实参
位置实参按照顺序与形参对应:
def describe_pet(name, species):
return f"{name} 是一只{species}"
print(describe_pet("团子", "猫"))
顺序错误通常不会自动报错,却可能产生错误结果:
describe_pet("猫", "团子") # 语法合法,但含义颠倒
位置实参适合数量少、含义直观的输入。
关键字实参
调用时写出形参名称,可以不依赖位置:
describe_pet(species="猫", name="团子")
关键字实参让调用含义更明确,尤其适合布尔开关、多个相同类型的值和可选配置:
def create_report(title, include_details, save_copy):
pass
create_report("月报", include_details=True, save_copy=False)
位置实参必须写在关键字实参之前:
describe_pet("团子", species="猫") # 合法
# describe_pet(name="团子", "猫") # SyntaxError
同一个形参不能同时接收位置值和关键字值:
# describe_pet("团子", name="豆包")
# TypeError: describe_pet() got multiple values for argument 'name'
默认参数
给形参提供默认值后,调用方可以省略该实参:
def greet(name, greeting="你好"):
return f"{greeting},{name}!"
print(greet("小林"))
print(greet("小林", "早上好"))
print(greet("小林", greeting="欢迎"))
没有默认值的形参必须放在有默认值的形参之前:
def connect(host, port=5432):
pass
下面的定义会产生语法错误:
# def connect(port=5432, host):
# pass
默认值在定义时计算
默认值只在执行 def 时计算一次,而不是每次调用时重新计算。可变对象作为默认值会被多次调用共享:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("A")) # ["A"]
print(add_item("B")) # ["A", "B"],通常不是预期结果
使用 None 作为哨兵,在函数内部创建新对象:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
同样应谨慎使用 set()、dict() 或自定义可变对象作为默认值。数字、字符串、元组和 None 等不可变值通常可以安全使用。
收集任意数量的位置实参
带星号的形参会把剩余位置实参收集成元组:
def calculate_sum(*numbers):
return sum(numbers)
print(calculate_sum()) # 0
print(calculate_sum(1, 2, 3)) # 6
名称 args 只是惯例,不是固定语法。更具体的名称通常更容易理解:
def join_words(separator, *words):
return separator.join(words)
print(join_words("-", "python", "is", "clear"))
*words 必须位于普通位置形参之后,它会收集尚未匹配的位置实参。
收集任意数量的关键字实参
两个星号的形参会把额外关键字实参收集成字典:
def build_profile(name, **details):
return {"name": name, **details}
profile = build_profile("小林", age=20, city="杭州")
print(profile)
名称 kwargs 也是惯例。**details 必须放在参数列表最后。关键字必须是字符串名称,同一个名称不能重复传入。
**kwargs 适合转发选项或构建灵活接口,但也会让可接受参数变得不明显。参数集合固定时,显式列出形参能获得更好的可读性、自动补全和错误提示。
仅限位置参数:/
/ 左侧的形参只能按位置传递:
def divide(dividend, divisor, /):
return dividend / divisor
divide(10, 2) # 5.0
# divide(dividend=10, divisor=2) # TypeError
仅限位置参数常见于内置函数和底层 API。它可以避免调用方依赖参数名称,也允许关键字字典使用相同名称:
def create_record(name, /, **fields):
return {"name": name, **fields}
create_record("主名称", name="别名")
普通业务函数不必为了展示语法而滥用 /;只有参数名称不应成为公开接口时才使用它。
仅限关键字参数:*
单独的 * 之后,形参只能通过关键字传递:
def resize(width, height, *, keep_ratio=True, quality=90):
return width, height, keep_ratio, quality
resize(800, 600, quality=80)
# resize(800, 600, False, 80) # TypeError
仅限关键字参数能让可选项更清楚,并防止调用者记错多个值的顺序。*args 后面的形参也天然是仅限关键字参数:
def log_all(*messages, level="INFO"):
for message in messages:
print(level, message)
五类参数的排列顺序
Python 参数可以按下面的顺序组合:
def example(position_only, /, normal, default=0, *args, keyword_only, **kwargs):
pass
从左到右分别是:
- 仅限位置参数;
- 可按位置或关键字传递的参数;
- 任意数量的位置参数;
- 仅限关键字参数;
- 任意数量的关键字参数。
多数函数只需要前两类,不应为了“灵活”而把全部形式都放进一个接口。
在调用处解包实参
调用函数时,* 可以把可迭代对象解包成位置实参:
def area(width, height):
return width * height
size = (8, 5)
print(area(*size)) # 等价于 area(8, 5)
** 可以把映射解包成关键字实参,键必须是字符串并与形参名称匹配:
options = {"width": 8, "height": 5}
print(area(**options)) # 等价于 area(width=8, height=5)
普通实参和解包可以组合,但不能让同一个参数重复获得值:
base = [1, 2]
extra = {"round_result": True}
def average(first, second, *, round_result=False):
result = (first + second) / 2
return round(result) if round_result else result
print(average(*base, **extra)) # 2
Python 如何传递参数
调用函数时,形参会绑定到实参所引用的对象。函数内对可变对象的修改,调用方通常可见:
def add_task(tasks, task):
tasks.append(task)
todo = ["阅读"]
add_task(todo, "练习")
print(todo) # ["阅读", "练习"]
但给形参重新赋值,只会改变函数内部的绑定:
def replace_tasks(tasks):
tasks = ["新任务"]
todo = ["阅读"]
replace_tasks(todo)
print(todo) # ["阅读"]
如果函数不应修改调用方的数据,可以复制后处理并返回新对象:
def with_task(tasks, task):
updated = tasks.copy()
updated.append(task)
return updated
是否原地修改不是语法对错问题,而是接口约定。函数名、文档和测试都应明确这种行为。
参数的类型注解
类型注解可以描述参数预期:
def format_name(first_name: str, last_name: str, uppercase: bool = False) -> str:
full_name = f"{first_name} {last_name}"
return full_name.upper() if uppercase else full_name
它有助于静态检查,但不会阻止调用方在运行时传入其他类型。处理外部输入时仍需校验,并在输入不合法时返回明确结果或抛出合适异常。
设计清晰的参数接口
参数不要过多
参数太多时,调用难以理解,也容易传错。可以考虑:
- 把有关联的数据封装为数据类或配置对象;
- 把一个承担多项职责的函数拆开;
- 将容易混淆的可选项设为仅限关键字参数。
布尔参数要表达清楚
下面的调用很难从现场看出 True 的含义:
export_data(records, True)
使用关键字会更清楚:
export_data(records, include_header=True)
如果一个布尔参数会让函数执行完全不同的任务,拆成两个函数可能更合适。
不要悄悄忽略无效输入
参数有明确范围时,应尽早检查:
def set_volume(level):
if not 0 <= level <= 100:
raise ValueError("level 必须在 0 到 100 之间")
错误信息应指出哪个参数无效以及有效范围,方便调用者修正。
常见错误
- 把关键字实参写在位置实参前面,会产生
SyntaxError。 - 同一个形参同时通过位置和关键字传值,会产生
TypeError。 - 使用列表或字典作为默认值,会让状态在多次调用之间意外共享。
- 盲目接收
*args、**kwargs,会隐藏函数真正支持的输入。 - 忘记
*values是调用处的解包,而定义中的*values是收集剩余实参。 - 修改传入的列表或字典前没有说明,会让调用方的数据意外变化。
练习
- 定义
create_user(name, *, active=True, role="member"),让可选项只能按关键字传递。 - 定义
multiply_all(*numbers),返回任意数量数字的乘积。 - 修复一个以空列表为默认值并在多次调用间共享数据的函数。
- 使用
*解包列表调用三边求周长函数,使用**解包字典调用同一函数。 - 为订单函数设计清晰的参数,包含商品、数量、折扣和是否需要发票。
小结
- 位置实参按顺序匹配,关键字实参按名称匹配。
- 默认值在函数定义时计算;可变默认值应改用
None哨兵。 *args收集额外位置实参,**kwargs收集额外关键字实参。/左侧是仅限位置参数,*后面是仅限关键字参数。- 调用处的
*和**分别解包位置实参和关键字实参。 - 形参绑定到实参对象;修改可变对象可能影响调用方,重新绑定形参则不会。
- 参数设计应优先清晰和可验证,而不是无限灵活。