• python中的引用传递,可变对象,不可变对象,list注意点


    python中的引用传递
    首先必须理解的是,python中一切的传递都是引用(地址),无论是赋值还是函数调用,不存在值传递。

    可变对象和不可变对象
    python变量保存的是对象的引用,这个引用指向堆内存里的对象,在堆中分配的对象分为两类,一类是可变对象,一类是不可变对象。不可变对象的内容不可改变,保证了数据的不可修改(安全,防止出错),同时可以使得在多线程读取的时候不需要加锁。

    不可变对象(变量指向的内存的中的值不能够被改变)
    当更改该对象时,由于所指向的内存中的值不可改变,所以会把原来的值复制到新的空间,然后变量指向这个新的地址。
    python中数值类型(int和float),布尔型bool,字符串str,元组tuple都是不可变对象。

    a = 1
    print id(a)    # 40133000L,整数1放在了地址为40133000L的内存中,a变量指向这个地址。
    a += 1 
    print id(a)    # 40132976L,整数int不可改变,开辟新空间存放加1后的int,a指向这个新空间。
    

    可变对象(变量指向的内存的中的值能够被改变)
    当更改该对象时,所指向的内存中的值直接改变,没有发生复制行为。
    python中列表list,字典dict,集合set都是可变对象。包括自定义的类对象也是可变对象。

    a = [1,2,3]
    print id(a)    # 44186120L。
    
    a += [4,5]     # 相当于调用了a.extend([4,5])
    print id(a)    # 44186120L,列表list可改变,直接改变指向的内存中的值,没开辟新空间。
    
    a = a + [7,8]  # 直接+和+=并不等价,使用+来操作list时,得到的是新的list,不指向原空间。
    print id(a)    # 44210632L
    
    def f(default_arg=[]):
      default_arg.append('huihui')
    
    f() # ['huihui']
    f() # ['huihui', 'huihui'] # 函数默认的可变参数并不会每次重新初始化,而是使用上次的作为默认值。
    f([]) # ['huihui'] # 自行传入参数即可。
    
    def f(default_arg=None):  # 一个常见的做法是判断是否为空,为空则新建list,否则append
        if default_arg is None:
            default_arg = []
        default_arg.append("some_string")
        return default_arg
    

    可变对象和不可变对象

    () is ()    # 返回True,因为tuple是不可变对象(不可改变,怎么定义都一样)
    '' is ''    # 返回True,因为str是不可变对象
    None is None # 返回True,None也是不可变的
    
    [] is []    # 返回False,因为是可变对象(可能改变,定义出来的两个必然要不一样)
    {} is {}    # 返回False,因为是可变对象
    [] == []    # 返回True,注意==和is的不同,==只比较内容,is比较地址(id)
    
    class Student:
      pass
    Student() is Student()  # 返回False,自定义类型也是可变对象,两次定义的对象地址是不同的
    id(Student()) == id(Student())  # 返回True,这里比较神奇,是因为创建一个Student对象,id()后返回地址但是进行了对象销毁,第二次又重新创建,两次占用了同一个地址
    

    不可变对象的编译时驻留(类似java的常量池)
    int的驻留:-5到256之间的整数都会进行驻留,再次定义的变量地址不变,为什么是-5到256呢,这是解释器决定的,依赖于具体实现。
    str的驻留:只包含字母,数字,下划线的字符串会驻留;长度为0或1的会驻留;

    a = -5    
    b = -5   
    a is b     # True,-5到256之间的整数,驻留(直觉上这部分数据会频繁调用,驻留可以节省资源)
    
    a = 256    
    b = 256   
    a is b     # True,-5到256之间的整数,驻留
    
    a = -6    
    b = -6   
    a is b     # False,非-5到256之间的整数,不驻留
    
    a = 257    
    b = 257   
    a is b     # False,非-5到256之间的整数,不驻留
    
    a = 'hello_world'    
    b = 'hello'+'_'+'world'
    a is b     # True,只包含字母,数字,下划线的字符串会驻留
    
    a = 'hello_world!'    
    b = 'hello_world!'
    a is b     # False,包含了特殊字符!, 不驻留
    
    'hello_world' is '_'.join(['hello', 'world'])     # False,因为驻留是编译阶段发生的,join在解释阶段才产生结果,未进行驻留
    
    a, b = 'hello_world!', 'hello_world!'
    a is b    # True 编译器的优化,在同一行赋值字符串时,只创建一个对象,指给两个引用。(ps:不适用3.7.x版本,3.7.x中会返回False)
    

    关于驻留的陷阱
    跟驻留没有直接关系(雾?),是在命令行运行和py文件直接运行有一些差异。先看之前的小例子。

    a = 257    
    b = 257   
    a is b     # False,非-5到256之间的整数,不驻留。
    

    事实上,在命令行运行得到的才是False(我做的小实验一般都在交互式命令行上运行)
    如果把这三行放到py文件里,再直接运行,得到的是True,因为py文件是一次性编译的,而交互式命令行按一行为单位(严格说是命令结束时的全部,因为会有for while这种)编译
    或者在交互式中把这三行定义为函数,再调用函数,返回也是True

    def func():
      a = 257
      b = 257
      return a is b
    
    func()  # 返回True
    

    这是由python的代码块机制导致的,在同一代码块中相同值的赋值会指向同一个对象。函数体,类对象体,py文件,模块都可以看作一个代码块。

    在交互式命令行上,一行看作一个代码块(严格说是命令结束时的全部,因为会有for while这种),所以,这里所谓“代码块的优化”,就是前面提到的,同行赋值的优化,只在一行(代码块)上优化。

    到具体直接运行py文件,又有了更大范围的代码块的优化,所以连着两行相同赋值的对象,会指向同一个对象。

    引用传递后的改变

    a = [1,2,3]
    b = a
    b[0] = 2     # 由于list是可变对象,改变b时候会导致a的改变,a和b都是[2,2,3]
    
    s = 'abc'
    s2 = s
    s2 += 'd'   # 由于str是不可变对象,s2是新建的对象,s2的修改不会影响s。s为'abc',s2为'abcd'。
    

    list注意点

    a = [1,2,3]
    b = a
    a is b             # True,因为按引用传递,a和b存的地址(引用)是一样的,改变b相当于改变a。
    
    b = a[:]
    a is b            # False,想使用list的值却不想修改原list时可以使用切片[:]拷贝一份到新空间。
    
    a = [1,2,3]
    id(a)    # 140376329323528
    a = [1,2,3]
    id(a)    # 140376359286920,两次定义相同的list,但是其地址并不相同,会创造新对象
    
    a = [1,2,3]
    id(a)    # 140376329323528
    a[:] = [1,2,3]
    id(a)    # 140376329323528,因为a[:]切片创建的是新空间,对新空间赋值不影响旧空间a,所以a的地址跟原来一致。
    
    a =[ [0]*2 ]* 2   # 以这种方式创建一个二维list,此时a为[[0,0],[0,0]]。
    a[0] is a[1]      # True,这种创建方法的机制是复制list,所以2个list其实是同一个list。
    
    a[0][0] = 1       # 改变第一个list时第二个list也改变,此时a为[[1,0],[1,0]]。
    a[0] += [1]       # 改变第一个list时第二个list也改变,此时a为[[1,0,1],[1,0,1]]。+=相当于extend,对list进行原地修改。
    a[0] = a[0] + [1] # 改变第一个list时,第二个list不改变,此时a为[[1,0,1,1],[1,0,1]]。因为不是原地改变,而是创建了新list,然后给原来的引用赋了新值。
    a[0] = [1,2]      # a[0]指向创建的新list[1,2]。此时a[1]不变,a为[[1,2],[1,0,1]]。同样是给a[0]赋值了新的list[1,2],不会影响到a[1]。
    
    a = [[0]*2 for _ in range(2)] # 相对正确的创建方式,这样创建的二维list,改变a[0]并不会影响a[1]
    a[0] is a[1]                 # False
    
    a = [ []*1000 ]                  # 同理,这么定义返回的是[],并不能得到含有1000个空list的list(直觉误区)
    a = [ [] for _ in range(1000) ]  # 正确的定义方式
    
    x = float('nan')
    x == x, [x] == [x] # False, True 因为list之间比较的时候先比较元素的地址,如果相等则认为相等,当id不相等时才比较值
    
  • 相关阅读:
    学习&分享
    跳槽
    20121113:延期通知书
    2012.9.9 baocheng博客园正式与大家见面啦!
    数据库
    ASP.Net模板引擎
    javascript图片切换效果
    dockercompose环境下zookeeper单机搭建、集群搭建
    Linux服务器日常巡检脚本
    MMOS FFB伺服直驱方向盘主控板DIY
  • 原文地址:https://www.cnblogs.com/liaohuiqiang/p/9668303.html
Copyright © 2020-2023  润新知