Python 对象拷贝问题

在 Python 中涉及到对象拷贝主要有两个问题:

  1. 深拷贝和浅拷贝问题
  2. 自定义对象拷贝过程

1. 深浅拷贝

深拷贝和浅拷贝的主要区别在于它们如何处理对象中的可变子对象。对于不可变类型不涉及到深浅拷贝问题。在 Python 中,只有字典、集合、列表属于可变类型。

1.1 浅拷贝

创建一个新的对象,但只复制原对象的引用。这意味着原对象中的可变子对象(如列表、字典、集合等)不会被复制,而是共享相同的引用。修改任何子对象的内容都会影响到原对象和浅拷贝对象。

import copy


def ids(elemets):
    print(id(elemets), end=' ')
    for element in elemets:
        print(id(element), end=' ')
    print()


def test():
    my_list1 = [[10, 20], {30, 40}, {'k1': 50, 'k2': 60}]
    my_list2 = copy.copy(my_list1)

    # [[10, 20], {40, 30}, {'k1': 50, 'k2': 60}]
    # [[10, 20], {40, 30}, {'k1': 50, 'k2': 60}]
    print(my_list1)
    print(my_list2)

    # 2271041540416 2271041542272 2271043720064 2271044354496
    # 2271041874496 2271041542272 2271043720064 2271044354496
    ids(my_list1)
    ids(my_list2)

    # 问题:任何一个列表发生改变,其他的列表也会发生改变


if __name__ == '__main__':
    test()

程序的输出结果:

从输出结果来看,当我们使用 copy 函数或者切片对以一个包含可变类型的列表进行拷贝时,仅仅拷贝了子元素的引用,所以,当通过任意一个列表修改元素时,都会导致其他的元素发生变化。

1.2 深拷贝

创建一个新的对象,并递归地复制原对象及其所有子对象。这意味着修改任何子对象的内容都不会影响原对象或深拷贝对象。对于嵌套数据结构,深拷贝确保每个层级的子对象都是独立的。

使用 copy 模块的 deepcopy 可以实现对象的深拷贝。

import copy


def ids(elemets):
    print(id(elemets), end=' ')
    for element in elemets:
        print(id(element), end=' ')
    print()


def test():
    my_list1 = [[10, 20], {30, 40}, {'k1': 50, 'k2': 60}]
    my_list2 = copy.deepcopy(my_list1)

    # [[10, 20], {40, 30}, {'k1': 50, 'k2': 60}]
    # [[10, 20], {40, 30}, {'k1': 50, 'k2': 60}]
    print(my_list1)
    print(my_list2)

    # 21815361212736 1815361214592 1815363392384 1815364026816
    # 1815361546816  1815364626432 1815364488992 1815364632000 
    ids(my_list1)
    ids(my_list2)


if __name__ == '__main__':
    test()

程序的执行结果:

对列表进行深拷贝之后,会对列表中的可变类型子元素进行递归拷贝。需要注意的是,对于不可变类型子元素,不用刻意区分深浅拷贝,因为即使是浅拷贝,一个列表修改了不可变类型子元素,也不会影响到另外一个列表。

2. 拷贝协议

Python 通过拷贝协议支持自定义对象的拷贝过程,只需要在类内增加浅拷贝和深拷贝对应的魔术方法

__copy____deepcopy__ 两个函数。

假设:对象包含用户名和密码两个属性信息,该对象在拷贝时,不能进行密码拷贝(注意:默认会进行所有属性的拷贝),此时就可以使用自定义对象协议来实现。

import copy


class Demo:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def __repr__(self):
        return 'username: %s, password: %s' % (self.username, self.password)

    def __copy__(self):
        new_demo = Demo(self.username, '')
        return new_demo

    def __deepcopy__(self, memodict={}):
        new_demo = Demo(self.username, '')
        return new_demo


def test():
    demo1 = Demo('admin', '123456')
    print(demo1)

    # 这里会对所有属性的值进行拷贝(注意深浅拷贝问题)
    # 假设: 该类型对象拷贝时,默认不进行密码拷贝,应该如何实现?
    # 解决: 可以给 Demo 增加拷贝逻辑
    demo2 = copy.copy(demo1)
    print(demo2)

    demo3 = copy.deepcopy(demo1)
    print(demo3)


if __name__ == '__main__':
    test()

深拷贝时,有个额外的参数 memodict,它可以用来处理循环引用。如下例子,node1 引用 node2node2 引用 node1, 在自定义深拷贝函数中,就陷入了无限递归,致使程序报错:

RecursionError: maximum recursion depth exceeded while calling a Python object

import copy


class Node:
    def __init__(self, name):
        self.name = name
        self.next = None

    def __deepcopy__(self, memodict={}):
        new_node = Node(self.name)
        # 递归拷贝其他节点
        if self.next:
            new_node.next = copy.deepcopy(self.next, memodict)

        return new_node


def test():
    node1 = Node('node1')
    node2 = Node('node2')

    node1.next = node2
    node2.next = node1

    new_node = copy.deepcopy(node1)
    print(new_node)


if __name__ == '__main__':
    test()

解决方法:我们可以将拷贝过的对象存储到 memodict 中,避免重复拷贝。如下代码所示:

    def __deepcopy__(self, memodict={}):

        # 如果当前节点已被拷贝,直接返回
        if id(self) in memodict:
            return memodict[id(self)]

        new_node = Node(self.name)
        # 当前节点 self 已经被拷贝,并记录下来
        memodict[id(self)] = new_node
        # 递归拷贝其他节点
        if self.next:
            new_node.next = copy.deepcopy(self.next, memodict)

        return new_node

未经允许不得转载:一亩三分地 » Python 对象拷贝问题
评论 (0)

7 + 6 =