默认参数
默认参数让函数在调用时可以省略某些形参,使用预先定义好的默认值。这个机制看似简单,但涉及求值时机、可变对象共享等深层细节,是 Python 函数中最容易踩坑的地方之一。
默认值的基本语法
在形参名后加 =表达式 即可指定默认值:
def greet(name, greeting="你好"):
return f"{greeting},{name}!"
print(greet("小明")) # 你好,小明!
print(greet("小明", "早上好")) # 早上好,小明!
print(greet("小明", greeting="再见")) # 再见,小明!
有默认值的参数必须放在无默认值参数之后,否则解析器无法判断调用时省略了哪些参数:
# def bad(a=1, b): # SyntaxError: non-default argument follows default argument
# pass
def good(a, b=1): # 合法
pass
默认值的求值时机
默认参数表达式在函数定义时求值,而不是每次调用时。这意味着默认值在函数对象创建时就被固定下来:
i = 10
def f(arg=i):
print(arg)
i = 20
f() # 输出 10,不是 20
这个特性对不可变对象(数字、字符串、元组)通常没有负面影响,因为不可变对象无法原地修改。但对可变对象(列表、字典、集合),后果非常严重。
可变默认参数陷阱
如果默认值是可变对象,所有未提供该参数的调用会共享同一个对象:
def append_item(item, items=[]):
items.append(item)
return items
print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] —— 保留了上次的结果!
print(append_item(3)) # [1, 2, 3]
为什么会这样?因为 [] 在函数定义时求值一次,创建了一个列表对象。每次调用 append_item 不传入 items 时,都使用这个同一个列表对象。函数内部 append 是原地修改,所以副作用累积了下来。
这个陷阱也适用于字典和集合:
def add_key(key, value, mapping={}):
mapping[key] = value
return mapping
print(add_key("a", 1)) # {'a': 1}
print(add_key("b", 2)) # {'a': 1, 'b': 2} —— 同样共享了!
None 哨兵模式
避免可变默认参数陷阱的标准做法是使用 None 作为默认值,在函数体内创建新对象:
def append_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(append_item(1)) # [1]
print(append_item(2)) # [2] —— 每次独立
print(append_item(3, [0])) # [0, 3] —— 传入显式列表也正常工作
None 是不可变的单例对象,用它做哨兵值可以明确区分"调用者未传参"和"调用者传了空列表"两种情况。注意判断时必须用 is None,不要用 == None 或 if not items(后者会把空列表也误判为需要新建)。
# 错误的判断方式
def bad(item, items=None):
if not items: # 危险!空列表也会被当作 None 处理
items = []
items.append(item)
return items
print(bad(1, [])) # 预期 [1],实际也是 [1],但逻辑不严谨
默认值为函数调用的情况
有时默认值需要动态计算,但记住它只在定义时求值一次:
from datetime import datetime
def log(msg, timestamp=datetime.now()):
return f"{timestamp}: {msg}"
# timestamp 被固定为模块导入时的时间,不是每次调用时
print(log("启动"))
如果需要每次调用时重新计算,必须使用 None 哨兵:
def log(msg, timestamp=None):
if timestamp is None:
timestamp = datetime.now()
return f"{timestamp}: {msg}"
默认参数与关键字参数的配合
默认参数天然适合与关键字参数结合使用。设计函数时,把最常用的默认值放在后面,调用者可以按需覆盖:
def connect(host, port=3306, user="root", password=None, database=None):
pass
# 只覆盖需要的部分
connect("localhost", database="test")
connect("localhost", 5432, user="admin")
常见错误
忘记可变默认参数的陷阱:
def collect_tags(tag, tags=[]):
tags.append(tag)
return tags
用可变对象做默认值但意图是每次新建:
def create_user(name, roles=["user"]): # 危险!所有默认用户共享同一角色列表
return {"name": name, "roles": roles}
正确做法:
def create_user(name, roles=None):
if roles is None:
roles = ["user"]
return {"name": name, "roles": roles}
小结
默认参数在定义时求值,不可变默认值安全,可变默认值会导致共享状态。使用 None 哨兵模式是处理可变默认参数的标准做法。理解求值时机和对象引用机制,才能避免这个经典陷阱。