函数默认值的陷阱:用None和Docstring来描述可变默认值的参数 (Effective Python 第24条)
在Python中,函数的默认参数值只在定义函数时计算一次。这一特性可能会导致一些意想不到的问题,尤其是在默认参数是可变对象(如字典、列表或日期时间对象)的情况下。本文将探讨这一问题,并提供解决方案,包括使用None
和docstring
来描述默认值会变化的参数,以及如何与类型注解结合使用。
问题:函数默认值的“坑”
假设我们有一个函数,其默认参数是一个字典或列表:
def add_item(items=[]):items.append("default item")return items
当我们调用这个函数时,第一次调用会返回['default item']
,但第二次调用会返回['default item', 'default item']
,第三次调用会返回['default item', 'default item', 'default item']
,依此类推。这是因为默认参数[]
在函数定义时被计算了一次,并且后续的调用都会共享同一个列表对象。
类似地,如果我们使用一个字典作为默认参数:
def update_config(config={}):config["key"] = "value"return config
每次调用这个函数时,config
字典都会被修改,导致所有调用共享同一个字典对象。
此外,如果我们希望默认参数是动态变化的值(如当前日期时间),也会遇到类似的问题:
import datetimedef log_time(timestamp=datetime.datetime.now()):print(f"Logged at {timestamp}")
无论我们调用这个函数多少次,timestamp
的值都会是函数定义时的日期时间,而不是每次调用时的当前时间。
解决方法:将默认值设为None
,并在函数体中初始化
为了避免上述问题,我们可以将默认参数设为None
,并在函数体中进行初始化。这样,每次调用函数时都会创建一个新的对象。
示例1:避免共享可变对象
def add_item(items=None):if items is None:items = []items.append("default item")return items
在这个示例中,items
的默认值是None
。每次调用函数时,如果items
是None
,我们都会创建一个新的空列表。这样,每次调用函数时都不会共享同一个列表对象。
示例2:动态默认值
import datetimedef log_time(timestamp=None):if timestamp is None:timestamp = datetime.datetime.now()print(f"Logged at {timestamp}")
在这个示例中,timestamp
的默认值是None
。每次调用函数时,如果timestamp
是None
,我们都会获取当前的日期时间。这样,每次调用函数时都会使用最新的时间。
在docstring
中描述默认行为
除了将默认参数设为None
,我们还应该在docstring
中明确描述函数的默认行为。这样,其他开发者或用户可以清楚地了解函数的默认参数是如何工作的。
示例:在docstring
中描述默认行为
def add_item(items=None):"""向列表中添加一个默认项。Args:items (list, optional): 要操作的列表。默认为None,此时会创建一个空列表。Returns:list: 添加默认项后的列表。"""if items is None:items = []items.append("default item")return items
在这个docstring
中,我们明确说明了items
的默认行为:如果items
为None
,则会创建一个空列表。
结合类型注解使用
Python 3.10及以上版本支持|
符号来表示联合类型注解。我们可以将None
和具体的类型结合起来,以进一步明确参数的类型。
示例:结合类型注解使用
def add_item(items: list | None = None):"""向列表中添加一个默认项。Args:items (list | None): 要操作的列表。默认为None,此时会创建一个空列表。Returns:list: 添加默认项后的列表。"""if items is None:items = []items.append("default item")return items
在这个示例中,我们使用类型注解list | None
来明确items
的类型:它可以是一个列表,也可以是None
。
总结
函数默认参数值只在定义函数时计算一次,这可能会导致一些意想不到的问题,尤其是在默认参数是可变对象或动态值的情况下。为了避免这些问题,我们可以采取以下措施:
- 将默认参数设为
None
,并在函数体中进行初始化。 - 在
docstring
中明确描述默认行为,以便其他开发者或用户清楚函数的行为。 - 结合类型注解,以进一步明确参数的类型。
通过这些方法,我们可以写出更健壮、更易维护的代码,避免默认参数带来的潜在问题。