Python 函数默认参数不能使用可变对象,如List,dict

许多使用很长时间Python的人也被下面的问题困扰:

def foo(a=[]):
    a.append(5)
    return a

Python新手估计可能会想这个函数返回一个只有元素[5]的列表.但是结果却出人意料:

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

我的一个经理曾经碰到过这个特性并把它叫做语言的"动态设计缺陷".这个现象应当有更深层次的解释,如果你不懂它的内部它确实非常令人困惑.然而我不能回答下面的问题:是什么原因使默认参数在函数的定义时被绑定,而不是在执行时?我怀疑这个特性在现实中有没有实际的用途(就像在C语言中有谁去用静态变量?)


事实上这并不是设计缺陷,也不是什么内部或性能原因.

原因很简单,Python中的函数是最高等级的对象,而不仅仅是一小段代码.

试着这么来理解:一个函数是一个被它自己定义而执行的对象;默认参数是一种"成员数据",所以它们的状态和其他对象一样,会随着每一次调用而改变.

-----------------------------------

Python’s handling of default parameter values is one of a few things that tends to trip up most new Python programmers (but usually only once).

What causes the confusion is the behaviour you get when you use a “mutable” object as a default value; that is, a value that can be modified in place, like a list or a dictionary.

An example:

>>> def function(data=[]):
...     data.append(1)
...     return data
...
>>> function()
[1]
>>> function()
[1, 1]
>>> function()
[1, 1, 1]

As you can see, the list keeps getting longer and longer. If you look at the list identity, you’ll see that the function keeps returning the same object:

>>> id(function())
12516768
>>> id(function())
12516768
>>> id(function())
12516768

The reason is simple: the function keeps using the same object, in each call. The modifications we make are “sticky”.

Why does this happen? #

Default parameter values are always evaluated when, and only when, the “def” statement they belong to is executed; see:

http://docs.python.org/ref/function.html (dead link)

for the relevant section in the Language Reference.

Also note that “def” is an executable statement in Python, and that default arguments are evaluated in the “def” statement’s environment. If you execute “def” multiple times, it’ll create a new function object (with freshly calculated default values) each time. We’ll see examples of this below.

What to do instead? #

The workaround is, as others have mentioned, to use a placeholder value instead of modifying the default value. None is a common value:

def myfunc(value=None):
    if value is None:
        value = []
    # modify value here

If you need to handle arbitrary objects (including None), you can use a sentinel object:

sentinel = object()

def myfunc(value=sentinel):
    if value is sentinel:
        value = expression
    # use/modify value here

In older code, written before “object” was introduced, you sometimes see things like

sentinel = ['placeholder']

used to create a non-false object with a unique identity; [] creates a new list every time it is evaluated.

Valid uses for mutable defaults #

Finally, it should be noted that more advanced Python code often uses this mechanism to its advantage; for example, if you create a bunch of UI buttons in a loop, you might try something like:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

only to find that all callbacks print the same value (most likely 9, in this case). The reason for this is that Python’s nested scopes bind to variables, not object values, so all callback instances will see the current (=last) value of the “i” variable. To fix this, use explicit binding:

for i in range(10):
    def callback(i=i):
        print "clicked button", i
    UI.Button("button %s" % i, callback)

The “i=i” part binds the parameter “i” (a local variable) to the current value of the outer variable “i”.

Two other uses are local caches/memoization; e.g.

def calculate(a, b, c, memo={}):
    try:
        value = memo[a, b, c] # return already calculated value
    except KeyError:
        value = heavy_calculation(a, b, c)
        memo[a, b, c] = value # update the memo dictionary
    return value

(this is especially nice for certain kinds of recursive algorithms)

and, for highly optimized code, local rebinding of global names:

import math

def this_one_must_be_fast(x, sin=math.sin, cos=math.cos):
    ...

How does this work, in detail? #

When Python executes a “def” statement, it takes some ready-made pieces (including the compiled code for the function body and the current namespace), and creates a new function object. When it does this, it also evaluates the default values.

The various components are available as attributes on the function object; using the function we used above:

>>> function.func_name
'function'
>>> function.func_code
<code object function at 00BEC770, file "<stdin>", line 1>
>>> function.func_defaults
([1, 1, 1],)
>>> function.func_globals
{'function': <function function at 0x00BF1C30>,
'__builtins__': <module '__builtin__' (built-in)>,
'__name__': '__main__', '__doc__': None}

Since you can access the defaults, you can also modify them:

>>> function.func_defaults[0][:] = []
>>> function()
[1]
>>> function.func_defaults
([1],)

However, this is not exactly something I’d recommend for regular use…

Another way to reset the defaults is to simply re-execute the same “def” statement. Python will then create a new binding to the code object, evaluate the defaults, and assign the function object to the same variable as before. But again, only do that if you know exactly what you’re doing.

And yes, if you happen to have the pieces but not the function, you can use thefunction class in the new module to create your own function object.


<think>嗯,用户问的是为什么不建议在函数默认参数中传入可变对象。这个问题我之前学过一点,但需要仔细回忆一下。记得在Python中,函数默认参数是在定义时就被计算和存储的,而不是每次调用函数时重新创建。这可能和可变对象有关联。 比如,如果默认参数是像列表这样的可变对象,比如def func(a=[]),那么这个列表只会在函数定义的时候创建一次,之后每次调用函数如果没有提供参数的话,都会使用同一个列表。这样会导致多次调用函数时,默认参数的值会被保留下来,可能引发意外的结果。例如: def func(a=[]): a.append(1) print(a) 第一次调用func()会输出[1],第二次调用就会输出[1, 1],第三次就是[1, 1, 1],这显然不是我们通常期望的。用户可能希望每次调用时,如果没有传入参数默认参数都是一个新的空列表,但实际上不是这样的。这时候就需要避免将可变对象作为默认参数。 那为什么Python要这样设计呢?可能是因为性能考虑,避免每次调用函数时都创建新的对象,特别是对于大的对象来说。但是这也导致了一个常见的陷阱,特别是对于新手来说容易犯错。 那正确的做法应该是什么呢?通常建议使用可变对象作为默认参数,比如None,然后在函数内部检查参数是否为None,如果是的话,就创建一个新的可变对象。例如: def func(a=None): if a is None: a = [] a.append(1) print(a) 这样每次调用func()的时候,如果没有传入a,都会创建一个新的空列表,避免了之前的问题。这应该就是用户需要知道的解决方案。 用户可能的场景是他们在编写函数时,希望有一个默认的可变参数,但不知道这样会导致问题,结果在多次调用时出现意料之外的行为。他们可能需要了解这个陷阱,以及如何避免。 此外,用户可能已经遇到了这个问题,或者是在面试中被问到,所以需要明确解释原因和解决方法。需要确保用户理解默认参数Python中的求值时机,以及可变对象在其中的影响。 总结起来,关键点是:默认参数在定义时求值一次,可变对象会被保留和修改,导致意外结果;解决方法是用None作为默认值,并在函数内部分配新的可变对象。</think>在 Python 中,函数默认参数值是在**函数定义时**(而非调用时)被计算并保存的。如果将可变对象(如列表、字典等)作为默认参数,会导致一个常见的陷阱:多次调用函数时,默认参数可能保留前一次调用的修改,从而引发意外行为。 --- ### **问题示例** ```python def append_to_list(value, my_list=[]): # 默认参数是空列表可变对象) my_list.append(value) return my_list print(append_to_list(1)) # 输出 [1] print(append_to_list(2)) # 输出 [1, 2](预期是 [2]) ``` - **原因**:函数定义时创建了默认列表 `my_list=[]`,后续所有未显式传递 `my_list` 的调用都共享同一个列表对象。 - **结果**:多次调用后,默认列表会累积之前的值。 --- ### **背后的机制** 1. **默认参数的存储** Python 函数默认参数值存储在函数对象的 `__defaults__` 属性中,并在函数定义时初始化一次。 ```python print(append_to_list.__defaults__) # 输出 ([1, 2],)(调用两次后) ``` 2. **可变对象的引用语义** 列表、字典等可变对象是通过引用传递的,因此所有未显式传递默认参数的调用,共享同一个对象的引用。 --- ### **解决方案** 使用可变对象(如 `None`)作为默认参数,并在函数内部显式创建可变对象: ```python def safe_append_to_list(value, my_list=None): if my_list is None: my_list = [] # 每次调用时创建新列表 my_list.append(value) return my_list print(safe_append_to_list(1)) # 输出 [1] print(safe_append_to_list(2)) # 输出 [2] ``` --- ### **为什么 Python 这样设计?** - **性能优化**:避免每次调用函数时都重新创建默认对象(尤其是大对象)。 - **一致性**:函数参数的行为与其他语言(如 JavaScript)不同,需要开发者特别注意。 --- ### **其他注意事项** 1. **所有可变默认参数类型都会导致此问题** 包括列表、字典、集合、自定义类对象等。 ```python def add_key(key, value, my_dict={}): # 默认字典同样危险 my_dict[key] = value return my_dict print(add_key("a", 1)) # 输出 {'a': 1} print(add_key("b", 2)) # 输出 {'a': 1, 'b': 2} ``` 2. **显式传递参数不会触发问题** 只有不传递参数时才会共享默认对象: ```python print(append_to_list(3, [])) # 输出 [3](显式传递新列表) ``` --- ### **总结** | **场景** | **风险** | **解决方案** | |-------------------------|----------------------------------|---------------------------------------| | 默认参数可变对象 | 多次调用共享同一对象,数据意外累积 | 使用 `None` 作为默认值,内部创建新对象 | | 默认参数为不可变对象 | 无风险(如 `int`, `str`, `None`) | 无需处理 | --- ### **面试常见追问** #### **Q1:如何验证默认参数是同一个对象?** ```python def check_default(obj=[]): print(id(obj)) # 多次调用时打印的 id 相同 check_default() # 输出 140000000 (示例id) check_default() # 输出 140000000(相同id) ``` #### **Q2:为什么 `None` 是安全的?** `None` 是不可变对象函数内部每次判断 `my_list is None` 后,都会显式创建一个新列表,确保每次调用独立。 #### **Q3:是否所有情况下都要避免可变默认参数?** - **否**:如果明确需要共享状态(如缓存机制),可以故意使用此特性。但需充分注释,避免他人误用。 ```python def cache_data(data, cache=[]): cache.append(data) return cache ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值