5.2 参数传递机制
任务管理器里添加任务的函数 add_task(tasks, title, priority=3) 用到了三种参数:tasks 和 title 必须按顺序传入,priority 有默认值可以省略。这种混合用法在 Python 中非常普遍,掌握参数的各种传递方式,才能写出调用方便、不易出错的函数。本节从最基础的位置参数讲起,逐步覆盖关键字参数、默认参数、可变参数 *args 和 **kwargs、关键字专属参数、位置专属参数、实参解包,最后讨论对象引用传递这一容易被误解的机制。
5.2.1 位置参数
位置参数是基本的参数形式。调用函数时,实参按顺序对应到形参,第一个实参给第一个形参,第二个实参给第二个形参,依此类推。参数的数量必须匹配,多了或少了都会报 TypeError。
def add_task(tasks, title, priority):
tasks.append({"title": title, "priority": priority})
tasks = []
add_task(tasks, "写报告", 2)
print(tasks) # [{'title': '写报告', 'priority': 2}]调用 add_task(tasks, "写报告", 2) 时,tasks 对应形参 tasks,"写报告" 对应 title,2 对应 priority,完全按位置匹配。位置参数的好处是简洁,缺点是调用方需要记住每个位置的含义,参数多了容易搞混顺序。
5.2.2 关键字参数
调用函数时可以用 形参名=值 的形式指定实参,这种实参称为关键字参数。使用关键字参数后,实参的顺序不再重要,只要名字对应即可。当函数参数较多时,关键字参数能显著提升代码可读性。
add_task(tasks, title="回复邮件", priority=1)
add_task(tasks, priority=4, title="整理桌面") # 顺序无关
print(tasks)
# [{'title': '写报告', 'priority': 2}, {'title': '回复邮件', 'priority': 1}, {'title': '整理桌面', 'priority': 4}]两次调用都用了关键字参数,第二次故意把 priority 写在 title 前面,依然能正确匹配。关键字参数让调用意图清晰,不必回去看函数定义就能知道每个值的含义。位置参数和关键字参数可以混用,但位置参数必须写在关键字参数前面。
add_task(tasks, "开会", priority=3) # 合法,位置参数在前
add_task(tasks, title="开会", 3) # 报错,关键字参数后不能跟位置参数5.2.3 默认参数
定义函数时可以给形参指定默认值,调用时若不传该参数,就使用默认值。默认值让函数在常见场景下调用更简洁,只传必要参数即可。带默认值的参数必须写在无默认值参数之后。
def add_task(tasks, title, priority=3):
tasks.append({"title": title, "priority": priority})
tasks = []
add_task(tasks, "写报告") # priority 取默认值 3
add_task(tasks, "开会", priority=1) # priority 显式传 1
print(tasks)
# [{'title': '写报告', 'priority': 3}, {'title': '开会', 'priority': 1}]第一次调用省略了 priority,函数内部使用默认值 3。第二次调用显式传入 1,覆盖了默认值。默认参数的值在函数定义时计算一次,这一点会引出一个重要陷阱,稍后讲解。
5.2.4 可变默认参数陷阱
当默认值是列表、字典等可变对象时,会出现一个经典陷阱。由于默认值在函数定义时只计算一次,所有调用共享同一个默认对象,对它的修改会累积下来。
def add_task(title, tasks=[]): # 错误写法
tasks.append(title)
return tasks
print(add_task("写报告")) # ['写报告']
print(add_task("开会")) # ['写报告', '开会'],第一次的任务还在第二次调用时没有传 tasks,使用的是默认值,而这个默认值是第一次调用后被修改过的那个列表。tasks=[] 在定义时创建了一个空列表对象,后续所有省略 tasks 的调用都共享这一个对象。这几乎总是与预期不符。
5.2.5 用 None 解决可变默认参数陷阱
正确的做法是把默认值设为 None,在函数内部判断并创建新对象。这样每次调用不传参时,都会得到一个全新的空列表。
def add_task(title, tasks=None):
if tasks is None:
tasks = []
tasks.append(title)
return tasks
print(add_task("写报告")) # ['写报告']
print(add_task("开会")) # ['开会'],互不影响tasks is None 判断是否未传参,是则创建新列表。这是 Python 中处理可变默认参数的标准模式,适用于列表、字典、集合等所有可变对象。不可变对象(如整数、字符串、None)作为默认值不会有这个问题,因为它们无法被原地修改。
5.2.6 可变位置参数 *args
在形参名前加 *,该参数会收集多余的位置实参,组装成一个元组。这种参数常用于函数需要接受任意数量的位置参数的场景。
def add_many_tasks(tasks, *titles):
for title in titles:
tasks.append({"title": title, "priority": 3})
return tasks
tasks = []
add_many_tasks(tasks, "写报告", "开会", "回邮件")
print(tasks)
# [{'title': '写报告', 'priority': 3}, {'title': '开会', 'priority': 3}, {'title': '回邮件', 'priority': 3}]*titles 收集了 "写报告"、"开会"、"回邮件" 三个实参,组成元组 ("写报告", "开会", "回邮件")。函数内部用 for 遍历这个元组逐个处理。tasks 作为第一个位置参数正常匹配,*titles 只收集它之后的多余实参。
5.2.7 可变关键字参数 **kwargs
在形参名前加 **,该参数会收集多余的关键字实参,组装成一个字典。kwargs 是约定俗成的名字,意为 keyword arguments。它常与 *args 配合,构建灵活的函数接口。
def add_task(title, **extra):
task = {"title": title}
task.update(extra)
return task
print(add_task("写报告", priority=2, deadline="周五", tag="紧急"))
# {'title': '写报告', 'priority': 2, 'deadline': '周五', 'tag': '紧急'}priority=2、deadline="周五"、tag="紧急" 这些关键字实参被 **extra 收集成字典 {"priority": 2, "deadline": "周五", "tag": "紧急"},再用 update 合并到任务字典里。这种写法让函数能接受任意附加字段,扩展性很强。
5.2.8 参数顺序规则
当各种参数混合使用时,定义顺序必须遵循固定规则:普通位置参数、默认参数、*args、关键字专属参数、**kwargs。违反这个顺序会报语法错误。
def func(a, b=1, *args, c, d=2, **kwargs):
print(f"a={a}, b={b}, args={args}, c={c}, d={d}, kwargs={kwargs}")
func(10, 20, 30, 40, c=100, e=200, f=300)
# a=10, b=20, args=(30, 40), c=100, d=2, kwargs={'e': 200, 'f': 300}a 是位置参数,b 有默认值,*args 收集多余位置实参 (30, 40),c 和 d 是关键字专属参数(必须用关键字传入),**kwargs 收集剩余关键字实参。实际开发中很少同时用全所有类型,但理解顺序规则有助于读懂复杂函数签名。
5.2.9 关键字专属参数
在 * 之后定义的形参称为关键字专属参数,调用时必须用关键字传入,不能用位置。* 可以单独使用,也可以是 *args,二者之后的所有形参都成为关键字专属。
def add_task(tasks, title, *, priority=3, tag=None):
tasks.append({"title": title, "priority": priority, "tag": tag})
tasks = []
add_task(tasks, "写报告", priority=1, tag="紧急") # 合法
add_task(tasks, "开会", 1, "紧急") # 报错,priority 和 tag 必须用关键字* 把 priority 和 tag 标记为关键字专属,调用时必须写 priority=1 这种形式。这种设计能避免参数顺序混淆,特别是当函数有多个有默认值的参数时,强制关键字能让调用更清晰、更不容易出错。
5.2.10 位置专属参数
Python 3.8 引入了位置专属参数,在 / 之前定义的形参只能按位置传入,不能用关键字。/ 之前的位置专属参数和 * 之后的关键字专属参数,把参数严格分成了三类。
def get_task(tasks, index, /):
return tasks[index]
tasks = ["写报告", "开会"]
print(get_task(tasks, 0)) # 写报告,合法
print(get_task(tasks, index=0)) # 报错,index 是位置专属,不能用关键字位置专属参数主要用于把参数名作为实现细节隐藏起来,允许将来改名而不影响调用方。标准库中一些内置函数大量使用了这一特性。日常开发中用得较少,了解其含义即可。
5.2.11 完整的参数混合形式
把位置专属参数、普通参数、*args、关键字专属参数、**kwargs 全部组合起来,就得到完整的函数签名形式。
def create_task(title, /, priority=3, *tags, urgent, **extra):
print(f"title={title}, priority={priority}, tags={tags}, urgent={urgent}, extra={extra}")
create_task("写报告", 2, "工作", "本周", urgent=True, deadline="周五")
# title=写报告, priority=2, tags=('工作', '本周'), urgent=True, extra={'deadline': '周五'}title 在 / 前是位置专属参数,priority 是带默认值的普通参数,*tags 收集多余位置实参,urgent 是关键字专属参数,**extra 收集剩余关键字实参。实际项目中这种完整签名很少见,多数函数只用其中两三种。
5.2.12 实参解包 *
调用函数时,可以在实参前加 *,把可迭代对象(列表、元组等)展开成多个位置实参。这称为实参解包,常用于把一个序列的元素分别传给函数的各个参数。
def add_task(tasks, title, priority):
tasks.append({"title": title, "priority": priority})
tasks = []
info = ["写报告", 2]
add_task(tasks, *info) # 等价于 add_task(tasks, "写报告", 2)
print(tasks) # [{'title': '写报告', 'priority': 2}]*info 把列表 ["写报告", 2] 展开成两个独立实参,分别对应 title 和 priority。当数据以列表或元组形式存在,又需要传给多个参数的函数时,解包写法比手动取索引更简洁。
5.2.13 实参解包 **
类似地,** 可以把字典展开成关键字实参。字典的键必须是字符串,与函数的形参名对应。
def add_task(tasks, title, priority, tag):
tasks.append({"title": title, "priority": priority, "tag": tag})
tasks = []
data = {"title": "写报告", "priority": 2, "tag": "工作"}
add_task(tasks, **data) # 等价于 add_task(tasks, title="写报告", priority=2, tag="工作")
print(tasks)**data 把字典展开成三个关键字实参。* 和 ** 解包可以同时使用,前者展开序列到位置参数,后者展开字典到关键字参数。解包让函数调用与数据结构之间转换非常方便。
5.2.14 对象引用传递机制
Python 的参数传递机制常被描述为对象引用传递,更准确的说法是传共享。函数接收到的就是实参所引用对象的引用,形参和实参指向同一个对象,函数内部对形参的操作会影响到这个共享对象。
理解这一机制的关键在于区分两类操作。对变量重新赋值,是让变量指向一个新对象,不影响原对象和其他引用。对可变对象进行原地修改,是改变对象本身,所有引用该对象的变量都能看到变化。
5.2.15 不可变对象作为参数
不可变对象(整数、字符串、元组等)作为参数传入时,函数内部无法修改原对象,因为不可变对象不支持原地修改。函数内对形参重新赋值只是让形参指向新对象,外部变量不受影响。
def try_double(value):
value = value * 2
return value
n = 5
result = try_double(n)
print(n) # 5,外部 n 不变
print(result) # 10value = value * 2 在函数内部创建了一个新整数 10,让形参 value 指向它,外部变量 n 仍指向原来的 5。整数是不可变对象,没有原地翻倍的操作,任何看似修改的赋值都产生新对象。字符串、元组同理。
5.2.16 可变对象作为参数
可变对象(列表、字典、集合等)作为参数传入时,函数内部可以原地修改对象,这种修改对调用方可见。这是函数产生副作用的主要来源,需要谨慎对待。
def clear_tasks(tasks):
tasks.clear()
tasks = ["写报告", "开会"]
clear_tasks(tasks)
print(tasks) # [],原列表被清空tasks.clear() 是原地操作,清空了形参 tasks 指向的列表对象,由于实参和形参指向同一对象,外部 tasks 也变空了。这有时是期望行为(如函数的职责就是修改传入对象),有时会引发意外。如果不想让函数修改外部对象,应在函数入口先做拷贝。
5.2.17 避免函数意外修改外部对象
当函数需要读取或处理可变参数但不应修改它时,可以在函数入口传入副本,或由调用方传入副本。传入副本后,函数内部的修改只作用于副本,原对象安全。
def count_high_priority(tasks):
tasks_copy = list(tasks) # 防御性拷贝
# 即使后续误改 tasks_copy,也不影响外部
return sum(1 for t in tasks_copy if t.get("priority", 5) <= 2)
tasks = [{"title": "写报告", "priority": 1}, {"title": "开会", "priority": 4}]
print(count_high_priority(tasks)) # 1调用方也可以主动传入副本:count_high_priority(tasks.copy())。在函数接口设计中,应明确约定函数是否会修改传入对象,约定不修改的函数应在内部做防御性拷贝或在文档中声明。这能减少共享状态带来的难以追踪的 bug。
可变默认参数务必用 None
列表、字典、集合作为默认参数值是最常见的 Python 陷阱之一。记住一个原则:可变默认值一律用 None 占位,在函数体内创建新对象。这条规则能避免绝大多数相关 bug。
练习题
练习 1 定义函数 search_task(tasks, keyword, case_sensitive=False),在任务列表中搜索包含关键词的任务。case_sensitive 为 True 时区分大小写,为 False 时不区分。调用时分别省略和传入 case_sensitive 测试两种情况。
参考答案
def search_task(tasks, keyword, case_sensitive=False):
results = []
for task in tasks:
title = task if isinstance(task, str) else task.get("title", "")
if case_sensitive:
if keyword in title:
results.append(task)
else:
if keyword.lower() in title.lower():
results.append(task)
return results
tasks = ["写报告", "回复邮件", "写周报"]
print(search_task(tasks, "写")) # ['写报告', '写周报'],不区分大小写
print(search_task(tasks, "写", case_sensitive=True)) # ['写报告', '写周报']
print(search_task(tasks, "WRITE", case_sensitive=False)) # [],不区分时小写匹配case_sensitive 有默认值 False,调用时省略它即使用默认值。函数内部根据该参数选择匹配方式。这里关键词为中文时大小写无区别,英文场景下区别明显。
练习 2 以下函数有可变默认参数陷阱,请说明问题并修正。
def append_note(note, notes=[]):
notes.append(note)
return notes参考答案
默认值 [] 在函数定义时创建一次,所有省略 notes 的调用共享同一个列表,导致前一次调用追加的内容残留到后一次调用。
修正方式是把默认值改为 None,函数内部创建新列表。
def append_note(note, notes=None):
if notes is None:
notes = []
notes.append(note)
return notes
print(append_note("第一条")) # ['第一条']
print(append_note("第二条")) # ['第二条'],互不影响练习 3 编写函数 add_task(tasks, title, *, priority=3, tag=None),要求 priority 和 tag 必须用关键字传入。然后准备一个字典 {"title": "开会", "priority": 1, "tag": "紧急"},用 ** 解包方式调用该函数。
参考答案
def add_task(tasks, title, *, priority=3, tag=None):
tasks.append({"title": title, "priority": priority, "tag": tag})
return tasks
tasks = []
data = {"title": "开会", "priority": 1, "tag": "紧急"}
add_task(tasks, **data)
print(tasks) # [{'title': '开会', 'priority': 1, 'tag': '紧急'}]* 让其后的 priority 和 tag 成为关键字专属参数。**data 把字典展开为关键字实参,分别传给对应形参。注意 title 在 * 之前,是位置参数,字典中的 "title" 键通过解包也能正确匹配。
练习 4 以下代码运行后 numbers 的值是什么?请用对象引用传递机制解释原因。
def modify(lst):
lst.append(99)
lst = [0, 0, 0]
numbers = [1, 2, 3]
modify(numbers)参考答案
numbers 的值是 [1, 2, 3, 99]。
函数内 lst.append(99) 是原地修改,作用于实参和形参共同指向的列表对象,所以列表变成了 [1, 2, 3, 99]。随后 lst = [0, 0, 0] 是重新赋值,让形参 lst 指向一个全新的列表对象,这一操作只影响函数内部的局部变量 lst,不影响外部的 numbers。函数结束后局部变量 lst 销毁,numbers 仍指向被 append 修改过的那个列表。
这体现了区分两类操作的关键:原地修改影响外部,重新赋值不影响外部。
常见错误
错误 1 · TypeError: add_task() got multiple values for argument 'tasks'
原因:调用函数时,同一个形参既被位置实参匹配,又被关键字实参指定,导致冲突。常见于 *args 解包后再用关键字传同名列。
解决:检查调用语句,确保每个形参只通过一种方式传值。位置实参对应的形参不要再用关键字重复传入。
错误 2 · SyntaxError: positional argument follows keyword argument
原因:调用函数时把位置参数写在了关键字参数之后。Python 语法要求位置参数必须在关键字参数之前。
解决:调整实参顺序,把所有位置参数放在前面,关键字参数放在后面。
错误 3 · 可变默认参数导致多次调用共享状态
现象:def add_task(title, tasks=[]) 连续调用多次,tasks 列表中累积了之前调用追加的内容,而非每次都是空列表。
原因:默认值 [] 在函数定义时只创建一次,所有省略 tasks 的调用共享同一个列表对象,修改会累积。
解决:可变默认值一律用 None 占位,在函数体内判断 if tasks is None: tasks = [] 创建新对象。
错误 4 · TypeError: add_task() got an unexpected keyword argument 'priorities'
原因:用 **dict 解包调用函数时,字典的键与形参名不匹配,或函数没有定义 **kwargs 来接收多余的关键字实参。
解决:检查字典键名是否与函数形参名完全一致,或在函数定义中加上 **kwargs 收集剩余关键字参数。