跳到主要内容

函数的参数

参数是函数与调用方之间的数据接口。合理设计参数,可以让函数既容易调用,又能清楚表达每个输入的用途。

本文使用 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

从左到右分别是:

  1. 仅限位置参数;
  2. 可按位置或关键字传递的参数;
  3. 任意数量的位置参数;
  4. 仅限关键字参数;
  5. 任意数量的关键字参数。

多数函数只需要前两类,不应为了“灵活”而把全部形式都放进一个接口。

在调用处解包实参​

调用函数时,* 可以把可迭代对象解包成位置实参:

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 是收集剩余实参。
  • 修改传入的列表或字典前没有说明,会让调用方的数据意外变化。

练习​

  1. 定义 create_user(name, *, active=True, role="member"),让可选项只能按关键字传递。
  2. 定义 multiply_all(*numbers),返回任意数量数字的乘积。
  3. 修复一个以空列表为默认值并在多次调用间共享数据的函数。
  4. 使用 * 解包列表调用三边求周长函数,使用 ** 解包字典调用同一函数。
  5. 为订单函数设计清晰的参数,包含商品、数量、折扣和是否需要发票。

小结​

  • 位置实参按顺序匹配,关键字实参按名称匹配。
  • 默认值在函数定义时计算;可变默认值应改用 None 哨兵。
  • *args 收集额外位置实参,**kwargs 收集额外关键字实参。
  • / 左侧是仅限位置参数,* 后面是仅限关键字参数。
  • 调用处的 * 和 ** 分别解包位置实参和关键字实参。
  • 形参绑定到实参对象;修改可变对象可能影响调用方,重新绑定形参则不会。
  • 参数设计应优先清晰和可验证,而不是无限灵活。