为什么默认参数最好不要是可变对象
def add_end(L=[]):
L.append('END')
print (L)
这个函数我们运行多次,
>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>>add_end([7])
[7,'END']
>>> add_end()
['END', 'END', 'END]
事先声明,以下内容涉及到了一点python内部运行原理,可以大体理解为这样,或者说为了能正确的理解某些函数比如闭包,匿名函数等等,输出的结果为什么是这样而不是那样,以这样的目的为导向那么这样理解也无可厚非,但是真实的执行过程,要比以下论述的复杂,我们知道python解释器是那c语言写的,实际上对于加载进内存中的函数体,实际上是c语言里的结构体,这些结构体内部有很多变量,数组,指针数组,字符串数组等等,其中就有co_freevars(保存使用了的外层作用域中的变量名集合)和co_cellvars(保存嵌套作用域中使用的变量名集合)。举个例子:
x=2 def add(d): print (add.func_code.co_freevars) # ()为空,因为add是最外层函数 print (add.func_code.co_cellvars) # ('c','d')因为这两个都会被他内部的函数引用 print (x) c=3 def do_add(value): print (x,d,c) print (do_add.func_code.co_freevars) # ('c','d')内部函数使用了外层函数中的局部变量 print (do_add.func_code.co_cellvars) #为空因为这个函数内部没有再定义有函数 add(6)
再举一个对比的例子:
def a(): def b(): print (c) print (b.func_code.co_freevars) return b c=5 dd=a()#为空 dd()#打印5
def a(): def b(): print (c) print (b.func_code.co_freevars) c=3 return b dd=a()#('c',) dd()#3
下面进入正题:
解释器在看到这个函数定义的时候,会给函数分配内存空间,那么这个这个内存空间内存放着的就是这个函数的函数体,由于默认参数L是[],所以计算机首先在内存中创建[]对象,而后将引用L指向这个[]对象的内存地址。函数体只保存这个L。除此之外,函数体内保存的就是函数内所有的代码,最后,解释器再将变量add_end指向这个函数体的内存地址。以待以后调用该函数。
以上的过程就是解释器看到函数的定义,把函数加载到内存中的过程,接下来就可以调用了。需要知道的是,不管函数在以后运行了多少次,函数内容是不会发生改变的,或者说函数体是不会改变的。在本例中,就拿默认参数来说,函数体只保留引用L,并且引用L指向[]这个对象的内存地址,这点是永远都不会发生改变的,不可能出现我运行了三次以后,函数体内保存的内存地址发生改变了,这是不可能的。有人可能会说了,我运行一下这个函数,add_end([1,2,3]),函数体内的保存的内存地址不就发生改变了吗?由原先的[]的地址变为了[1,2,3]的地址?
这个问题问的很好,之所以你会有这个疑问,是因为你不知道,函数在运行的时候,系统会为这个函数再分配一个运行空间,这个空间用于保存函数运行过程中所产生的所有变量,就好比我们运行add_end([1,2,3]),那么计算机首先给函数分配运行空间,然后创建一个[1,2,3]的对象,然后将默认参数的引用L指向[1,2,3]的内存空间,注意,这个函数的运行空间是临时存在的,等到函数运行完成后,那么这个运行空间也就消失了。运行结束后,函数体内的引用L仍然指向的是[]对象的内存地址
以上似乎已经能完全解释过程了,但是还有一个重要的细节却漏掉了,就是默认参数确实是一个比较特殊的存在,特殊在,指定默认参数的代码,L=[],有别于函数内定义的代码,前者是在加载函数的时候就已经运行过了,并且只在这个时候运行一次,在调用的时候是不会运行的。但是函数内定义的代码却不一样,每次调用都会运行。为了验证这一点,可以看以下代码:
import time def a(): print ("haha") return 4 def b(s=a()): c=5 print (s) time.sleep(5)#注意在函数运行前,a函数已经执行了,可以看到在这里已经打印了haha b()#不在运行s=a(),因为没打印haha b(3)#不在运行s=a(),因为没打印haha
而像c=5这样的函数内定义的代码,只有在函数运行的时候,运行到这一句,系统先创建一个5的对象,然后引用c指向这个5,注意这个引用c是保存在当前函数的运行空间中的,等到运行结束后,运行空间消亡,引用c也就没了.题外话,python有个垃圾回收的策略,假如这个5只有这一个引用,那么这唯一的引用随着函数运行的结束消失,那么这个5的引用计数将变为0,将在适当的时候被垃圾回收。如果除了这个c还有其他引用指向这个5,那么c没了,这个5的引用计数就减少一个,直到引用计数变为0,就等着垃圾回收了
相信到这里你已经明白为什么默认参数不能是可变对象了。虽然我们不能改变函数体的内容(除非动态修改函数,python是支持的,这里先不说。),比如函数体记录着默认函数L指向了内存地址99999,我们不能把99999改写为99998,但是我们却能直接修改内存地址为99999的内容,前提是内存地址为99999所保存的是可变数据类型,在本例中,内存地址为99999的是[],正好是一个可变数据类型,那么我们append他,就是对他的直接修改,比如append('end'),那么这个他变为了['end'],不管是变之前的[]和变之后的['end'],他们俩的内存地址都是99999。并且函数每次运行的过程,是不会执行那个指定默认参数的语句的。那个只有在系统加载函数的时候运行一次。
有了函数运行空间的概念,就不难理解闭包的原理:
def count(): fs = [] for i in [1,3]: def f(): return i*i fs.append(f) return fs a=count() print (a[0]())#9 print (a[1]())#9
下面就把上面这个函数分布讲解,原来的函数等价于:
def count(): fs = [] i = 1 def f(): return i*i fs.append(f) i = 3 def f(): return i*i fs.append(f) return fs a=count() print (a[0]())#9 print (a[1]())#9
从第一行开始,首先看到有函数的定义,解释器将函数加载进内存,然后执行到a=count()这一句,count()函数运行,创建count函数运行空间,然后执行第一句,先在内存中创建一个空列表,把fs指向空列表,然后在创建1,i指向1,接着有了函数的定义,系统将这个f函数加载进内存,并且将f指向该函数,注意函数f里存在着外部变量i,接着将此函数追加到列表中,如果说没追加之前这个函数的引用计数为1,只有f指向他,那么现在他的引用计数变为了2,新增了列表第一个元素对他的引用,接着i变量指向了3,这时要注意列表中第一个函数的i由于跟这个外部变量i是一回事,所以列表中i的值也同样变为了3,然后又出现了函数的定义,解释器同样会将他加载静内存,然后将f指向该函数,这里就有了一个副作用,f原先指向的第一个函数,引用计数减为1,
然后同样的将此函数追加到列表中。最后把这个列表返回给a,也就是a指向该列表,到此,函数运行结束,函数运行空间也将消失,按理说所有的变量也将消失,包括fs,i,f。。这其中,由于变量i是被列表中的两个函数所引用的,所以你可以理解为他不能消失,除此之外,其他变量就都消失了。
所以,两个打印的都是9
如果我们把上述函数稍加修改,结果将截然不同,因为,f的参数i跟外部变量i完全是两回事,f这个内部函数不存在有外部局部变量的引用
def count(): fs = [] for i in [1,3]: def f(i): return i*i fs.append(f) return fs a=count() print (a[0](4))#16 print (a[1](5))#25
最后辨析一道这样的面试题,在说面试题之前,首先明确一下,假设代码如下
def a(c,d=[]):
d.append(c)
return d
a(3)#默认参数将变为[3]
a(4,[])#这点是新的疑惑点,注意这句话运行以后,从此以后(指的是以后运行这个函数的时候)默认参数并不会改变为[4],如过默认参数变为[4]成立的话,那么以下代码,在第二次运行b函数的时候应该打印ha,这显然是不对的,他任然会打印1,再次说明了给默认参数传参并不会改变默认参数的值
def b(c=1):
print (c)
b(2)
b()
面试题:
def a(c,d=[]):
d.append(c)
return d
a1=a(3)
a2=a(123,[])
a3=a(4)
print (a1)#[3,4]
print (a2)#[123]
print (a3)#[3,4]
面试题3:
print ([m(2) for m in [lambda x:i*x for i in range(6)]])
将打印[10, 10, 10, 10, 10, 10]
最后再说一个python易错点,就是,局部变量的作用范围仅仅是函数内部的嵌套函数,而不包括函数内调用的函数。举个例子:
def a():
print (c)
def b():
c=0
a()
b()#NameError: name 'c' is not defined