zoukankan      html  css  js  c++  java
  • 为什么默认参数最好不要是可变对象

    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

        

  • 相关阅读:
    spoj694 DISUBSTR
    poj3261 Milk Patterns
    poj1743 Musical Theme
    Hdu1403 Longest Common Substring
    bzoj3779 重组病毒
    bzoj3083 遥远的国度
    bzoj3514 Codechef MARCH14 GERALD07加强版
    PAT 乙级 1027 打印沙漏(20) C++版
    PAT 乙级 1060 爱丁顿数(25) C++版
    windows编程之窗口抖动
  • 原文地址:https://www.cnblogs.com/saolv/p/8674189.html
Copyright © 2011-2022 走看看