pilot icon indicating copy to clipboard operation
pilot copied to clipboard

列表、函数参数以及缺省值的问题

Open imSeaton opened this issue 4 years ago • 5 comments

image image image 老师好! 如图中所示,append_list函数中将空列表lst作为函数参数。第一次使用缺省值调用append_list( lst=[ ]),执行完返回的结果为列表“[1]”。第二次再使用缺省值调用append_list( lst=[ ]),返回结果竟然再上一次调用的结果上添加元素,返回值为列表"[1,1]"。同上,第三次调用返回结果竟然为" [1, 1, 1]"。 这与我预计的结果不相符。我认为,在第一次调用结束后第二次调用时,append_list( )函数的参数lst应当重新被赋予缺省值——空列表[ ],在此基础上进行append操作,进而得到lst = [1],第三次重新调用得到的结果也是lst = [1]。 难道缺省值赋值时的操作lst=[ ]与函数体内的赋值操作lst=[ ]有什么不同之处吗?不清楚为什么lst在指向缺省值[ ]时, 还会指向上一次调用返回的结果?

imSeaton avatar May 12 '20 13:05 imSeaton

我在第九课讲 List 的结尾部分讲了作用域的进阶规则,最后总结出来 Python 中作用域的完整规则是两条:

  1. 全局(global)和函数内(local)这两个作用域是各自独立的,两个作用域中即使有同名的变量(var)也是不同的东西;全局作用域在程序运行期间一直有效,局部作用域在函数返回后失效。
  2. Python 中的变量(var)只是指向具体对象的名字引用(name reference),通过赋值语句我们可以让多个名字指向同一个对象;对类似 list 这样的内容可变的对象,如果有多个变量(var)指向同一个列表对象,当该对象的内容发生变化时,所有指向它的变量的内容都一起变化。

回到你的问题,你的代码中关键的是下面这个函数的写法很特殊:

def append_list(lst=[]):
    lst.append(1)
    return lst

这个函数接受一个列表作为输入参数,对这个列表执行了改变内容的操作(没有对 lst 做重新赋值),又返回了这个参数本身。然后你把这个返回值赋给了一个全局变量:

lst_updated = append_list()

于是全局变量 lst_updated 和局部变量(参数) lst 就指向了同一个对象,而因为这个对象有全局变量在引用,它获得了全局变量的生命周期,也就是说,不会随着函数调用结束而销毁——于是它的内容就得到了延续。

这显然是比较糟糕的情况,和我们的预期不会一样。

所以一般设计函数时,

  1. 最好的方式是不要改变任何输入参数的内容,而是在函数体中读取输入数据,创新新的数据用于返回,这样的函数比较“纯”,也比较安全;
  2. 如果不得不修改输入参数的内容,那么千万不要在这种情况下返回这个参数本身,应该返回 None 或者返回一个状态码,告诉调用端修改输入参数内容这个操作是不是成功之内的——反正这个函数的作用就是改变输入参数的内容,既然修改的是调用端传进来的东西,并不需要把修改后的再返回回去。

neolee avatar May 12 '20 19:05 neolee

谢谢老师,我知道了。 测试了一下,发现append_list()函数在首次被调用之后,再次调用时,无论调用多少次,lst和lst_updated都指向了同样的地址。 image image image

老师在设计函数时的建议,让我收获很多。我在采用老师的建议编写代码时,还遇到一个小问题:我在使用递归的思想设计“查找树的所有后代节点”这个函数(部分代码见下图)时,确实需要修改参数中的列表。而这样做必须每次都先创建一个空列表descendants=[ ],然后再传入函数中进行修改、获得所有后代节点。这样每次使用该函数时都需要先创建一个空列表。我觉得为了这样程序的安全性,这样设计的函数使用起来不那么直接,似乎有一点别扭。不知道有没有即直接又安全的方法设计这样的函数? image

imSeaton avatar May 14 '20 04:05 imSeaton

你看了我的回复之后自己动手去验证的操作很赞,既体现了不盲从的实证精神,又做到了“学以致用”。一定要坚持这样做下去。

针对你在设计的 get_descendants 方法,我觉得下面这个实现比较简单好懂:

class TreeNode:
    ......
    def get_descendants(self):
        descendants = []
        if self.children:
            for child in self.children:
                descendants.append(child)
                descendants.extend(child.get_descendants())

        return descendants

    ......

使用时大概是这样(root 是根节点,c 是树中某个节点):

print(root.get_descendants())
print(c.get_descendants())

事实上,必须要改变输入参数内容而且必须返回这个参数对象的情况真的很少见,如果你发现了一定要告诉我 😀

neolee avatar May 14 '20 04:05 neolee

之前写的时候没有考虑到extend函数,现在明白了,谢谢老师!

imSeaton avatar May 14 '20 07:05 imSeaton

李老师提供的解决办法太妙了,我原本想的也是提供一个空列表,然后把所有的子节点加进去, 第一次子节点的迭代很简单,把子节点的子节点加进去,后面就到了一个关键的点了,我只想添加后续子节点的子节点的子节点,但我就不知道怎么办了 来issue看了李老师的代码,太他妈妙了

Totie-O avatar Nov 04 '23 10:11 Totie-O