07 Python对象的比较、 拷贝

作者: leacoder | 来源:发表于2019-07-04 23:05 被阅读0次

    目录链接:https://www.jianshu.com/p/e1e201bea601

    '==' VS 'is'

    等于(==) 和 is 是 Python 中对象比较常用的两种方式。

    '==' 操作符比较对象之间的值是否相等:

    a == b  # 表示比较变量 a 和 b 所指向的值是否相等。
    

    'is' 操作符比较的是对象的身份标识是否相等, 即它们是否是同一个对象, 是否指向同一个内存地址。

    在 Python 中, 每个对象的身份标识, 都能通过函数 id(object) 获得。 因此, 'is' 操作符, 相当于比较对象之间的 ID 是否相等

    a1 = 10
    b1 = 10
    print('a1 == b1:',a1 == b1)
    print('id(a1):',id(a1))
    print('id(b1):',id(b1))
    print('a1 is b1:',a1 is b1)
    a2 = 257
    b2 = 257
    print('a2 == b2:',a2 == b2)
    print('id(a2):',id(a2))
    print('id(b2):',id(b2))
    print('a2 is b2:',a2 is b2)
    
    '==' VS 'is'.png

    对于整型数字来说, 以上 a1 is b1 为 True 的结论, 只适用于 -5 到 256 范围内的数字。

    事实上, 出于对性能优化的考虑, Python [CPython(Python 的 C 实现)]内部会对 -5 到 256 的整型维持一个数组, 起到一个缓存的作用。 这样, 每次你试图创建一个 -5 到 256 范围内的整型数字时, Python 都会从这个数组中返回相对应的引用, 而不是重新开辟一块新的内存空间,这样a1 is b1: True。如果整型数字超过了这个范围, 比如上述例子中的 257, Python 则会为两个 257 开辟两块内存区域, 因此 a 和 b 的 ID 不一样,这样a2 is b2: False。

    'is' 的速度效率 通常要优于 '==' ,因为 'is' 操作符不能被重载, Python 就不需要去寻找, 程序中是否有其他地方重载了比较操作符。执行比较操作符 'is' , 就仅仅是比较两个变量的 ID 而已。 '==' 操作符却不同,执行 a == b 相当于是去执行 a.__eq__(b) , Python大部分的数据类型都会去重载 __eq__ 这个函数。

    t1 = (1, 2, [3, 4])
    t2 = (1, 2, [3, 4])
    print('t1 == t2:',t1 == t2)
    
    t1[-1].append(5)
    print('t1 == t2:',t1 == t2)
    
    '=='.png

    我们知道元组是不可变的, 但元组可以嵌套, 它里面的元素可以是列表类型, 列表是可变的, 所以如果我们修改了元组中的某个可变元素, 那么元组本身也就改变了。

    浅拷贝和深度拷贝

    浅拷贝(shallow copy) 和深度拷贝(deep copy)

    浅拷贝

    浅拷贝指重新分配一块内存, 创建一个新的对象, 里面的元素是原对象中子对象的引用。
    Python 中可以用 copy.copy() 来实现对象的浅拷贝,适用于任何数据类型。

    如果原对象中的元素不可变, 那倒无问题;但如果元素可变, 浅拷贝通常会带来一些副作用:

    '''对 l1 执行浅拷贝, 赋予 l2。浅拷贝里的元素是对原对象元素的引用, 因此 l2 中的元素和 l1 指向同一个
    列表和元组对象,但l1和l2本身是不同对象。'''
    l1 = [[1, 2], (30, 40)]
    l2 = list(l1)    
    
    l1.append(100)    'l1和l2本身是不同对象, l1 的列表新增元素 100而l2不会'
    'l1 中的第一个列表新增元素 3,因为 l2 是 l1 的浅拷贝,l2 中的第一个元素和 l1 中的第一个元素, 共同指向同一个列表,l2 中的第一个列表也会相对应的新增元素 3'
    l1[0].append(3)   
    
    print('l1:',l1)
    print('l2:',l2)
    
     '元组是不可变的,这里对 l1 中的第二个元组拼接, 然后重新创建了一个新元组作为 l1 中的第二个元素,与l2的第二个元素不再是用一个对象的引用,操作后 l2 不变, l1 发生改变'
    l1[1] += (50, 60)
    
    print('l1:',l1)
    print('l2:',l2)
    
    浅拷贝副作用.png

    深度拷贝

    深度拷贝, 是指重新分配一块内存, 创建一个新的对象, 并且将原对象中的元素, 以递归的方式, 通过创建新的子对象拷贝到新对象中。 因此, 新对象和原对象没有任何关联。

    Python 中以 copy.deepcopy() 来实现对象的深度拷贝。

    import copy
    l1 = [[1, 2], (30, 40)]
    l2 = copy.deepcopy(l1)
    
    l1.append(100)
    l1[0].append(3)
    
    print('l1:',l1)
    print('l2:',l2)
    
    深度拷贝.png

    需要注意的是,如果被拷贝对象中存在指向自身的引用, 那么程序很容易陷入无限循环:

    import copy
    x = [1]
    x.append(x)
    
    print('x:',x)
    
    y = copy.deepcopy(x)
    print('y:',y)
    
    被拷贝对象中存在指向自身的引用.png

    为什么深度拷贝 x 到 y 后, 程序并没有出现 stack overflow?

    深度拷贝函数 deepcopy 中会维护⼀个字典, 记录已经拷⻉的对象与其 ID。 拷贝过程中, 如果字典里已经存储了将要拷贝的对象, 则会从字典直接返回。

    源码地址:https://github.com/python/cpython/blob/3.7/Lib/copy.py

    def deepcopy(x, memo=None, _nil=[]):
        """Deep copy operation on arbitrary Python objects.
            
        See the module's __doc__ string for more info.
        """
        
        if memo is None:
            memo = {}
        d = id(x) # 查询被拷贝对象 x 的 id
        y = memo.get(d, _nil) # 查询字典里是否已经存储了该对象
        if y is not _nil:
            return y # 如果字典里已经存储了将要拷贝的对象,则直接返回
            ...    
    

    如果使用 x == y 比较上述 y = copy.deepcopy(x) 的 y 与x

    RecursionError

    x==y内部执行是会递归遍历列表x和y中每一个元素的值,由于x和y是无限嵌套的,因此会stack overflow报错

    参考资料:

    极客时间 Python核心技术与实战学习

    Python核心技术与实战(极客时间)链接:
    http://gk.link/a/103Sv


    GitHub链接:
    https://github.com/lichangke/LeetCode

    知乎个人首页:
    https://www.zhihu.com/people/lichangke/

    简书个人首页:
    https://www.jianshu.com/u/3e95c7555dc7

    个人Blog:
    https://lichangke.github.io/

    欢迎大家来一起交流学习

    相关文章

      网友评论

        本文标题:07 Python对象的比较、 拷贝

        本文链接:https://www.haomeiwen.com/subject/qnvdhctx.html