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 | 函数的默认参数陷阱

posted @ 2020-11-06 16:13  听雨危楼  阅读(331)  评论(0编辑  收藏  举报