Python传参陷阱

Python 小记 2019-01-27 4732 字 1270 浏览 点赞

前言

简单的传参背后却藏匿着粗心就会完蛋的大坑。

默认传参

设计一个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']


本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论