前言
简单的传参背后却藏匿着粗心就会完蛋的大坑。
默认传参
设计一个Python函数时,免不了用到默认传参;默认传参时,又免不了用到列表类型。如下面这样:
def add_elem_to_list(something, list_=[]):
for i in something:
list_.append(i)
乍看之下,好似没什么问题,我们像下边这样调用,同时试着打印list_:
add_elem_to_list(["z", "t", "y"])
# 输出:
['z', 't', 'y']
毫无意外,输出内容是我们想要的结果。我们再多调用这个函数几次,局面马上就要失控了:
def add_elem_to_list(something, list_=[]):
for i in something:
list_.append(i)
print(list_)
if __name__ == "__main__":
print("第一次调用", end=":")
add_elem_to_list(["z", "t", "y"])
print("第二次调用", end=":")
add_elem_to_list([5, 1, 2])
print("第三次调用", end=":")
add_elem_to_list(["有关心情"])
# 输出:
第一次调用:['z', 't', 'y']
第二次调用:['z', 't', 'y', 5, 1, 2]
第三次调用:['z', 't', 'y', 5, 1, 2, '有关心情']
结果让人不解,可事实的确如此。看起来第二次、第三次调用函数add_elem_to_list()时,使用的变量list_是第一次调用该函数所创建的。我们应该通过打印list_的id来确认这个猜想:
def add_elem_to_list(something, list_=[]):
# ....
print("list_的id是:{id_}\n"
"list_的内容是:{list_}\n".format(id_=id(list_), list_=list_))
if __name__ == "__main__":
# ...
# 输出:
第一次调用
list_的id是:23545256
list_的内容是:['z', 't', 'y']
第二次调用
list_的id是:23545256
list_的内容是:['z', 't', 'y', 5, 1, 2]
第三次调用
list_的id是:23545256
list_的内容是:['z', 't', 'y', 5, 1, 2, '有关心情']
猜想得到证实。那是不是所有的默认传参都只在函数第一次被调用时创建呢?写几个demo试试咯:
def test1(string="zty"):
print(id(string))
def test2(num=512):
print(id(num))
def test3(tuple_=(1, 2)):
print(id(tuple_))
if __name__ == "__main__":
test1()
test1()
print()
test2()
test2()
print()
test3()
test3()
# 输出:
56420992
56420992
26926384
26926384
27505168
27505168
因此,Python的默认传参中,形参只在函数第一次调用时被创建。第一次之后的调用,都是对其直接使用。
019.1.29补充
尽管现象看起来默认参数是在函数第一次调用时被创建,但事实并非如此。默认参数在Python解释器预览.py文件时就已经创建好了(十分抱歉,我真不知道用术语该怎么说)。这里举个例子:
def print_a():
do_print_a()
def do_print_a():
print("a")
if __name__ == "__main__":
print_a()
很显然,上边的代码不会报错,打印结果为“a”。有C语言经验的同学会知道,在C语言中是不允许这样调用的。因为函数do_print_a()在print_a()后面定义,除非有前置声明,不然编译器会报错,认为并不存在do_print_a()函数。但在Python中可以正常运行,是由于解释器“预览”了这个文件,知道do_print_a()函数定义过了。而就在这个“预览”过程中,默认参数被创建。无论是函数还是类方法,默认参数被藏在了__defaults__
中,只要调用时没有给默认参数传递形参,解释器就会自动取__defaults__
里的数据。
def test1(string="zty"):
pass
class Test1(object):
def __init__(self, lyst=["z", "t", "y"]):
pass
if __name__ == "__main__":
print(test1.__defaults__)
print(Test1.__init__.__defaults__)
# 输出:
('zty',)
(['z', 't', 'y'],)
参考慕课网Bobby老师的《Python高级编程和异步IO并发编程》。
这里有两种解决方案:
第一种:通过传递实参来改变默认传参的局限性。就像下面这样(喂喂喂这样哪里还像个默认传参呀)。
add_elem_to_list(["z", "t", "y"], []) add_elem_to_list([5, 1, 2], []) add_elem_to_list(["有关心情"], [])
第二种:在函数中赋予默认值。给list_一个默认值None,保留了默认传参的可传参也可不传参的优点。
def add_elem_to_list(something, list_=None): if list_ is None: list_ = [] # ...
递归中的参数
我们先看一个现象吧:
def recursion_func(string, list_, dict_, num=3):
if num <= 0:
return
print("string的id:{strid}".format(strid=id(string)))
print("list_的id:{listid}".format(listid=id(list_)))
print("dict_的id:{dictid}".format(dictid=id(dict_)))
print()
recursion_func(string, list_, dict_, num-1)
if __name__ == "__main__":
recursion_func("", [], {})
# 输出:
string的id:56543648
list_的id:56837544
dict_的id:57169744
string的id:56543648
list_的id:56837544
dict_的id:57169744
string的id:56543648
list_的id:56837544
dict_的id:57169744
看起来递归中的参数同Python的默认传参一样。这是由于递归过程中,系统自动保留过程中产生的数据,当函数里边的某个表达式需要用到一个变量,先去堆里看看有没有,有,那就不创建了,直接拿来用。
所以当递归+迭代的组合出现时,会有这样的尴尬:
def recursion_func(something, list_, num=2):
if num <= 0:
print(list_)
return
for __ in range(2):
list_.append(something[num-1])
recursion_func(something, list_, num-1)
if __name__ == "__main__":
recursion_func(["zty", 512], [])
# 输出:
[512, 'zty']
[512, 'zty', 'zty']
[512, 'zty', 'zty', 512, 'zty']
[512, 'zty', 'zty', 512, 'zty', 'zty']
解决方案:利用浅拷贝更新变量的id,也就是创建新的变量的意思。
import copy
def recursion_func(something, list_, num=2):
if num <= 0:
print(list_)
return
for __ in range(2):
newList = copy.copy(list_) # 浅拷贝
newList.append(something[num-1])
recursion_func(something, newList, num-1)
if __name__ == "__main__":
recursion_func(["zty", 512], [])
# 输出:
[512, 'zty']
[512, 'zty']
[512, 'zty']
[512, 'zty']
还不快抢沙发