1-Python - 不可不知的默认参数陷阱
可变对象作为默认参数的陷阱
先来看这段代码:
def foo(value, l = []):
l.append(value)
return l
print(foo("a"))
print(foo("b", []))
print(foo("c"))
"""
我们认为的打印:
['a']
['b']
['b', 'c']
实际的打印:
['a']
['b']
['a', 'c']
"""
想要知道原因,就必须要知道一些事情。
Python中一切皆对象:
- 定义一个变量,这个变量是对象。
- 定义一个函数,函数时对象。
- 定义一个类,类是对象。
所以,Python中既然万物皆对象,所以,Python中一切传值都是对于对象的引用,或者说对于对象地址的引用。
在Python中,对象又可以分为两类:
- 可变类型的对象,如:dict、list。
- 不可变类型的对象,如:tuple、int、str、set。
注意,tuple这家伙也是个坑货
t1 = (1, "a")
t2 = (1, ['a', 'b'])
print(hash(t1)) # hash值: 3696735525067939916
print(hash(t2)) # TypeError: unhashable type: 'list'
"""
结论,只有当tuple中,所有的元素都是可哈希类型,那这个tuple才算是不可变类型的对象
也才符合上面的分类。
"""
言归正传,在Python中,对于不可变类型的对象来说,传值相当于重新对原对象做了引用,而对于可变类型的对象来说,传值只是相当于多了个对原对象值的引用,如下示例演示了这一现象:
def foo(value, l = []):
print("value:{}, id(value):{}; l:{}, id(l):{}".format(value,id(value), l, id(l)))
foo(1, [1, 2])
foo(2, [2, 3])
foo(3, [3, 4])
"""
value:1, id(value):1374645280; l:[1, 2], id(l):2180236871176
value:2, id(value):1374645312; l:[2, 3], id(l):2180236871176
value:3, id(value):1374645344; l:[3, 4], id(l):2180236871176
"""
所以,当默认参数值是可变对象的时候,那么每次使用该默认参数的时候,其实更改的是同一个变量对象。
当Python声明了函数之后,那这个函数的相关信息都成为了该函数的对象的属性,我们可以通过dir
来查看都绑定了哪些属性:
def foo(value, l = []):
l.append(value)
return l
print(dir(foo)) # 通过 dir 查看函数的属性
print(foo("a"), foo.__defaults__)
print(foo("b", []), foo.__defaults__)
print(foo("c"), foo.__defaults__)
"""
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
['a'] (['a'],)
['b'] (['a'],)
['a', 'c'] (['a', 'c'],)
"""
所有默认参数值则存储在函数对象的func.__defaults__
属性中,它的值是个元组,元组中每一个元素均为一个默认参数的值。
上面说了函数中默认值的陷阱和产生的原因。
那么同样的,在类中也存在这种现象:
class Bar(object):
def __init__(self, l=[]):
self.l = l
def add(self, value):
self.l.append(value)
def foo1(i):
bar = Bar()
bar.add(i)
print(bar.l, id(bar.l))
for i in range(5):
foo1(i)
"""
[0] 2383616784200
[0, 1] 2383616784200
[0, 1, 2] 2383616784200
[0, 1, 2, 3] 2383616784200
[0, 1, 2, 3, 4] 2383616784200
产生这种情况的原因是,虽然在每次循环中都重新实例化,但 l 引用的列表还是同一个列表,即 l 对象没变,l 对象指向的值也没变
"""
def foo2(i):
bar = Bar(l=[]) # 虽然还是引用的同一个 l 对象,但l对象指向的值确是一个新的列表,即 l 对象没变,但 l 对象指向的值变了
bar.add(i)
print(bar.l, id(bar.l))
for i in range(5):
foo2(i)
"""
[0] 2383616785800
[1] 2383616785800
[2] 2383616785800
[3] 2383616785800
[4] 2383616785800
"""
避免陷阱问题
现在时候来解决可变类型的陷阱问题了。
默认值使用None代替
def foo(value, l = None):
if not l:
l = []
else:
l = l
l.append(value)
return l
print(foo("a"))
print(foo("b", []))
print(foo("c"))
"""
['a']
['b']
['c']
"""
采用装饰器来解决问题
import copy
def freshdefault(f):
fdefaults = f.__defaults__
def refresher(*args,**kwds):
f.__defaults__ = copy.deepcopy(fdefaults)
return f(*args,**kwds)
return refresher
@freshdefault
def foo(value, l = []):
l.append(value)
return l
print(foo("a"))
print(foo("b", []))
print(foo("c"))
"""
['a']
['b']
['c']
"""
that's all, see also:
Python函数默认参数陷阱,你知道吗? | Default Parameter Values in Python | 函数的默认参数陷阱