本文主要内容翻译自:点击打开链接
Python处理默认参数值的方式是少数几个难住Python初学者的问题之一(当然通常也就难住第一次)。
造成这种困惑的主要原因是当你使用一个可变的对象作为默认参数值时,也就是说,这个默认值会在某些时候被修改,比如一个list对象或者一个dictionary对象。
举一个例子:
>>> def function(data=[]):
... data.append(1)
... return data
...
>>> function()
[1]
>>> function()
[1, 1]
>>> function()
[1, 1, 1]
可以看到,打印出来的list会变得越来越长。如果你注意一下这个list的Identity,你会发现这个函数一直在返回同样的值:
>>> id(function())
12516768
>>> id(function())
12516768
>>> id(function())
12516768
原因很简单,这个函数在每次调用中,一直在使用同样的对象,我们做的修改是具有“粘行的”。
为什么会发生这样的事儿?
当且仅当默认参数值属于的“def”语句被执行的时候,它们就会被求值。
同时也要注意,在Python中,“def”定义的语句是可执行的,它的默认参数会在相对应的“def”语句环境中被求值。如果你多次运行“def”语句,每次它都会创建新的函数对象(用新计算的默认值)。我们将在下面看到例子。
正确的处理方式是怎样的?
作为一种变通方案,正如其他人提到的那样,使用一个占位符而不是修改这个默认值。None是一个常见的占位符:
def myfunc(value=None):
if value is None:
value = []
# modify value here
如果你需要处理任意对象,包括None,你可以使用一个哨兵对象:
sentinel = object()
def myfunc(value=sentinel):
if value is sentinel:
value = expression
# use/modify value here
在老的代码中,也就是在写“object”之前被介绍到的,你有时候会看到如下这样的写法:
sentinel = ['placeholder']
通常用一个唯一的Identity来创建一个non-false对象;当被求值的时候,[] 每次都会创建一个新的list。
使用可变默认参数的有效方式
最后,应该注意到的是在更高级的Python代码中经常使用这个机制来体现它的优势;例如:如果你想在一个循环中创建一串UI Button,你可能会这样尝试:
for i in range(10):
def callback():
print "clicked button", i
UI.Button("button %s" % i, callback)
不料却发现所有的callback都打印相同的值,在这个例子中最可能就是9. 原因是Python的嵌套域绑定在variable上,而不是object value,所以所有的callback实例都将看到当前 i 这个变量的值(=最后一个值)。要修正这个例子,我们只需使用明确的绑定:
for i in range(10):
def callback(i=i):
print "clicked button", i
UI.Button("button %s" % i, callback)
在“i = i”这部分,绑定参数“i”(一个本地变量)到外部变量“i”的当前值中。
(这里我想提一下,Python中“def”定义的函数其实就是一个对象,我们可以把函数具有的默认参数值理解成这个对象的“类属性”,而当这个属性是可变对象的时候,那么我们所做的操作就会被保留下来。再看下面这个例子:
def bar(a=[]):
print id(a)
a = a + [1] #赋值后创建了一个新的对象a
print id(a)
return a
>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # "类属性"一直都没有改变
4484523720 # 总是一个新的对象
[1]
>>> id(bar.func_defaults[0])
4484370232
这样应该就好理解多了,在下面的例子中还会提到这点。)
还有两个其他的使用场景是本地缓存和记忆化,例如:
def calculate(a, b, c, memo={}):
try:
value = memo[a, b, c] # 返回已经计算的值
except KeyError:
value = heavy_calculation(a, b, c)
memo[a, b, c] = value # 更新memo字典
return value
(在确定类型的递归算法中这是相当好用的)
还有就是,在高度优化的代码中,local name重新绑定到global name上:
import math
def this_one_must_be_fast(x, sin=math.sin, cos=math.cos):
...
这种特性是如何工作的?
当Python运行一个”def“语句时,它带来了一些已经准备好的部件(包括为这个函数体编译好的代码和当前的命名空间),而且创建了一些新的函数对象。当它做这些的时候,它也会计算默认参数值。
各种各样的组成元件是这个函数对象的有效属性,我们使用上面提到的函数来演示:
>>> function.func_name
'function'
>>> function.func_code
function at 00BEC770, file "" , line 1>
>>> function.func_defaults
([1, 1, 1],)
>>> function.func_globals
{'function': <function function at 0x00BF1C30>,
'__builtins__': '__builtin__' (built-in)>,
'__name__': '__main__', '__doc__': None}
既然你可以访问这些默认值,那么你也可以修改他们:
>>> function.func_defaults[0][:] = []
>>> function()
[1]
>>> function.func_defaults
([1],)
然而,这不是直接的处理方式,更推荐常规的使用方法。。。
(注意这里”[:]”的用法,举个例子:
>>> a = [x for x in range(8)]
>>> print(id(a))
4320183880
>>> a = a + [11, 12]
>>> print(id(a))
4320210632 #赋了新值后地址变了
>>> a[:] = a + [13, 14]
>>> print(id(a))
4320210632 #同样是赋值但是地址没变
>>> print(a)
[0, 1, 2, 3, 4, 5, 6, 7, 11, 12, 13, 14]
当同一个list在很多地方都在引用的时候,我们对某个做的赋值,应该考虑是否让其他地方也适用)
另外一种重置默认参数的简单方法是重新运行相同的”def“语句。然后Python将为代码对象创建一个新的绑定,计算默认参数的值,赋值这个函数对象到前面同样的变量上。但是赋值的时候,你得清楚的知道你在做什么。
当然,如果你碰巧有这个pieces而不是function,你可以使用 new 模块中的 function 类,来创建你自己的函数对象。