默认参数
默认参数让函数更灵活:调用方可以省略某些参数,函数使用预设的默认值。但默认参数有一个著名的陷阱,理解它需要知道 Python 的求值时机。
基本用法
def greet(name, greeting="Hello"):
print "%s, %s!" % (greeting, name)
greet("Alice") # Hello, Alice!
greet("Bob", "Hi") # Hi, Bob!
greet("Charlie", greeting="Hey") # Hey, Charlie!
默认参数在函数定义时求值,而非调用时。这意味着默认值只创建一次,后续调用共享同一个默认值对象。
可变对象陷阱
这是 Python 中最著名的 bug 来源之一:
def add_item(item, items=[]):
items.append(item)
return items
print add_item(1) # [1]
print add_item(2) # [1, 2] —— 列表被共享了!
print add_item(3) # [1, 2, 3]
原因:items=[] 在函数定义时创建了一个空列表。每次调用 add_item 时,如果没有提供 items,就使用这个已经存在的列表。所以三次调用操作的是同一个列表。
正确做法:用 None 作为哨兵
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print add_item(1) # [1]
print add_item(2) # [2] —— 每次创建新列表
None 是不可变对象,每次调用时检查 items is None,如果是就创建新列表。这样每个调用都有独立的列表。
默认参数的求值时机
x = 5
def func(a=x):
print a
x = 10
func() # 5,不是 10!
默认参数 a=x 在函数定义时求值,当时 x 是 5。之后 x 变成 10,但默认值已经固定为 5。
默认参数的顺序
默认参数必须放在非默认参数之后:
# 正确
def func(a, b=2, c=3):
pass
# 错误
def func(a=1, b): # SyntaxError: non-default argument follows default argument
pass
调用时,位置参数按顺序匹配,关键字参数可以跳过前面的默认参数:
def func(a, b=2, c=3):
print a, b, c
func(1) # 1 2 3
func(1, 4) # 1 4 3
func(1, c=5) # 1 2 5,跳过 b
func(c=5, a=1) # 1 2 5,关键字参数顺序任意
实际应用
配置函数:
def connect(host, port=80, timeout=30):
print "Connecting to %s:%d (timeout=%d)" % (host, port, timeout)
connect("example.com") # 使用默认端口和超时
connect("example.com", 8080) # 自定义端口
connect("example.com", timeout=60) # 只改超时
累加器函数:
def make_accumulator(start=0):
total = [start] # 用列表包装,实现闭包效果
def accumulate(n):
total[0] += n
return total[0]
return accumulate
acc = make_accumulator(10)
print acc(5) # 15
print acc(3) # 18
默认参数与 None 检查
当默认参数是字符串或数字时,不需要用 None 技巧,因为不可变对象不会被共享:
# 安全,字符串不可变
def greet(name, greeting="Hello"):
pass
# 安全,数字不可变
def repeat(text, times=3):
pass
# 危险,列表可变
def collect(items=[]): # 不要这样做
pass
# 危险,字典可变
def build_config(config={}): # 不要这样做
pass
经验法则:默认参数使用不可变对象(数字、字符串、元组、None)是安全的;使用可变对象(列表、字典、集合)时,用 None 作为哨兵值,在函数体内创建新对象。