1. Python 默认参数介绍
默认参数在 Python 函数中常用到,它为函数提供了缺省值,使得调用函数时可以不传递该参数。声明一个默认参数的语法如下:
def foo(a, b=2, c=3):
pass
foo(10) # 等价于 foo(10, 2, 3)
foo(10, c=30) # 等价于 foo(10, b=2, c=30)
在函数定义中,函数可以有任意数目的默认参数。默认参数的位置顺序必须与函数定义相同,即先是非默认参数,紧接着是默认参数。
1.1 使用默认参数的好处
默认参数的使用可以缩短函数的定义,并且简化函数参数的调用。
假如有一个取平均数的函数,有一个参数决定是否对输入的数字进行四舍五入。如果默认情况下进行四舍五入,那么函数定义如下:
def mean(numbers, round_result=True):
"""
Compute the mean of a list of numbers.
:param numbers: A list of numbers
:param round_result: Round the result to the nearest integer (default=True)
"""
n = sum(numbers)
result = n/len(numbers)
if round_result:
return round(result)
else:
return result
这样,当用户不关心是否对结果进行四舍五入时,即 round_result 参数传递 False,简化调用:
>>> mean([1.0, 2.0, 3.0])
2
>>> mean([1.0, 2.0, 3.0], False)
2.0
1.2 注意点
需要注意的是,默认参数的值只会在 Python 程序中调用了函数让其诞生后,“评估”一次。这和函数对象本身不同,函数对象是在解释 Python 模块时被创建的。
为了更好地理解这个问题,看看下面的代码:
i = 5
def f(arg=i):
print(arg)
i = 6
f() # 输出 5
2. Python 默认参数负责问题
当默认参数使用可变对象时,可能会导致一些不易发现的问题。
下面举一个典型的例子。假如有一个函数,要将接收到的字符串插入到列表中,然后返回该列表。
def func(item, items=[]):
items.append(item)
return items
如果连续多次调用该函数,如下所示:
print(func(1))
print(func(2))
print(func(3))
输出结果是:
[1]
[1, 2]
[1, 2, 3]
这并不是我们所期待的结果,因为我们每次调用 func 时都创建了一个新的空列表,然后将传递给它的参数添加到该列表中。但实际情况是,列表 items 被指定为默认参数,这意味着它实际上是一个全局变量。
默认参数 items 在函数定义时评估一次,因此在第一次调用函数时,items 将被创建并初始化为 []。因后续调用共享同一列表,所以会导致多次调用导致列表被逐渐扩充。
很多初学 Python 的程序员都会陷入类似这样的状况,所以在做函数设计时,应格外注意参数的传递方式。
2.1 怎样避免出现负责问题
为了解决这个问题,我们可以遵循一些相对比较好的实践经验。
1) 将不可变类型作为默认参数传入函数
不可变类型包括整数、浮点数和字符串。这样的变量不能被修改,并且在多次调用函数时保持不变。例如,下面的函数将数字添加到一个列表中,但默认参数是一个空元组,这样就保证了列表 items 只是一个新列表。
def func(item, items=()):
items += (item,)
return items
2) 在函数内创建默认参数的副本,而非对该变量进行就地修改
当我们有可变参数作为默认参数时,我们应该在函数内部新建一个副本,从而避免全局的可变对象被就地更改。例如,下面的函数接收一个字符串和一个列表作为参数,并将字符串添加到该列表的副本中:
def func(item, items=None):
if items is None:
items = []
items.append(item)
return items
这样,在每次调用该函数时,都会在函数内部新建一个空列表。
2.2 使用可变参数作为默认参数的示例
那么,如果我们确实需要使用可变参数作为默认参数,应该怎么做呢?下面是一种示例,它使用时尽可能地保护了可变对象:
def func(item, items=None):
if items is None:
items = []
items += [item]
return items
这里使用了默认参数,但将列表对象替换为 None。当函数被调用时,如果没有传递列表 items,则会在函数内部创建一个新的空列表,并将其添加到列表中。
这种方法可以更好地防止在调用函数时共享可变对象,并且它使用列表的加法运算符而不是 append 方法,这使得在函数内创建一个副本变得更为安全。
3. 使用默认参数注意事项
在使用默认参数时,需要注意以下几个问题:
3.1 默认参数的值在函数定义后评估一次
当 Python 解释器加载模块并定义函数时,函数的默认参数将被评估一次。这意味着所有的默认参数都将被创建并分配了值,而不是在函数被调用时动态创建。例如:
import datetime
def log(message, timestamp=datetime.datetime.now()):
print(message, timestamp)
log("Hello World!")
log("Hello again!") # 这里的时间是函数定义时的时间
由于默认参数是 datetime.datetime.now(),因此该函数将在加载模块时首先被评估。
3.2 默认参数应该使用不可变对象的值
默认参数的值在函数定义时仅被评估一次。如果默认参数是可变对象的话,那么实际上是多次使用了同一个对象。这样会导致许多问题,因为函数调用可能会修改这个默认参数。例如:
def add_item(name, quantity, unit, grocery_list=[]):
grocery_list.append(name)
grocery_list.append(quantity)
grocery_list.append(unit)
return grocery_list
print(add_item("apple", 5, "kg"))
print(add_item("bread", 2, "pieces"))
上述函数的默认参数为 []。这意味着该函数的所有调用都共享同一列表对象。由于列表是可变的,因此 append 调用将会直接修改共享的列表对象,从而导致不正确的行为。
修复这个问题有多种方法。其中一种方法是使用 None 作为默认值来避免使用可变对象:
def add_item_v2(name, quantity, unit, grocery_list=None):
grocery_list = [] if grocery_list is None else grocery_list
grocery_list.append(name)
grocery_list.append(quantity)
grocery_list.append(unit)
return grocery_list
print(add_item_v2("apple", 5, "kg"))
print(add_item_v2("bread", 2, "pieces"))
3.3 避免在调用函数时使用可变对象作为默认参数
任何时候都不应该在函数调用中共享可变对象。因为这样的代码可能会出现令人费解的错误。学习使用非可变对象作为默认参数是种好习惯。如果确实需要使用可变对象(例如列表或字典)作为默认参数,则必须小心处理。
在编写代码时,请务必考虑到这些注意事项,以避免出现隐藏的错误并使代码更易于阅读和调试。