MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python字典的深浅拷贝区分

2023-07-103.3k 阅读

Python字典的浅拷贝

在Python中,字典的浅拷贝是指创建一个新的字典对象,新字典的键值对与原字典相同,但新字典中的值(如果是可变对象,如列表、字典等)是原字典中值的引用。这意味着,如果原字典中的值是可变对象,修改新字典中的值会影响到原字典,反之亦然。

浅拷贝的实现方式

  1. 使用dict.copy()方法
    original_dict = {'a': [1, 2, 3], 'b': {'sub': 'value'}}
    shallow_copied_dict = original_dict.copy()
    print(original_dict)
    print(shallow_copied_dict)
    
    在上述代码中,original_dict.copy()创建了original_dict的浅拷贝shallow_copied_dict。这里,original_dict中的值[1, 2, 3]{'sub': 'value'}都是可变对象。
  2. 使用copy模块的copy函数
    import copy
    original_dict = {'a': [1, 2, 3], 'b': {'sub': 'value'}}
    shallow_copied_dict = copy.copy(original_dict)
    print(original_dict)
    print(shallow_copied_dict)
    
    同样,copy.copy函数也实现了浅拷贝的功能。对于字典而言,original_dict.copy()copy.copy(original_dict)的效果是一样的。

浅拷贝对可变值的影响

当原字典中的值是可变对象时,浅拷贝后对新字典中可变值的修改会反映到原字典中。

original_dict = {'a': [1, 2, 3], 'b': {'sub': 'value'}}
shallow_copied_dict = original_dict.copy()

# 修改浅拷贝字典中的可变值
shallow_copied_dict['a'].append(4)
shallow_copied_dict['b']['sub'] = 'new value'

print(original_dict)
print(shallow_copied_dict)

运行上述代码,你会发现original_dict中的[1, 2, 3]变成了[1, 2, 3, 4]{'sub': 'value'}变成了{'sub': 'new value'}。这是因为浅拷贝时,新字典中的[1, 2, 3]{'sub': 'value'}只是原字典中对应值的引用,它们指向内存中的同一个对象。所以当通过浅拷贝字典修改这些可变对象时,原字典中的对象也会受到影响。

浅拷贝对不可变值的影响

如果原字典中的值是不可变对象(如整数、字符串、元组等),浅拷贝后对新字典中不可变值的修改不会影响原字典。

original_dict = {'a': 1, 'b': 'hello', 'c': (1, 2)}
shallow_copied_dict = original_dict.copy()

# 修改浅拷贝字典中的不可变值
shallow_copied_dict['a'] = 2
shallow_copied_dict['b'] = 'world'
shallow_copied_dict['c'] = (3, 4)

print(original_dict)
print(shallow_copied_dict)

在这段代码中,original_dict中的值1'hello'(1, 2)都是不可变对象。当在浅拷贝字典shallow_copied_dict中修改这些值时,实际上是创建了新的不可变对象并重新赋值给相应的键,原字典中的值不受影响。

Python字典的深拷贝

字典的深拷贝与浅拷贝不同,它会递归地复制字典中的所有值,包括可变对象。这意味着新字典中的所有值(无论是可变还是不可变)都与原字典中的值是完全独立的对象,修改新字典中的值不会影响原字典,反之亦然。

深拷贝的实现方式

深拷贝需要使用copy模块的deepcopy函数。

import copy
original_dict = {'a': [1, 2, 3], 'b': {'sub': 'value'}}
deep_copied_dict = copy.deepcopy(original_dict)
print(original_dict)
print(deep_copied_dict)

在上述代码中,copy.deepcopy(original_dict)创建了original_dict的深拷贝deep_copied_dictdeep_copied_dict中的[1, 2, 3]{'sub': 'value'}虽然看起来与原字典中的值相同,但它们是完全独立的对象,在内存中有不同的地址。

深拷贝对可变值的影响

由于深拷贝会递归地复制所有值,所以当修改深拷贝字典中的可变值时,原字典不受影响。

import copy
original_dict = {'a': [1, 2, 3], 'b': {'sub': 'value'}}
deep_copied_dict = copy.deepcopy(original_dict)

# 修改深拷贝字典中的可变值
deep_copied_dict['a'].append(4)
deep_copied_dict['b']['sub'] = 'new value'

print(original_dict)
print(deep_copied_dict)

运行这段代码,你会看到original_dict中的值没有改变,而deep_copied_dict中的值发生了变化。这是因为深拷贝时,deep_copied_dict中的[1, 2, 3]{'sub': 'value'}是新创建的对象,与原字典中的对象没有引用关系。

深拷贝对不可变值的影响

对于不可变值,深拷贝也会创建独立的对象,尽管在某些情况下Python可能会复用相同的不可变对象(如小整数池等优化机制)。但从逻辑上来说,深拷贝后的不可变值与原字典中的不可变值也是独立的。

import copy
original_dict = {'a': 1, 'b': 'hello', 'c': (1, 2)}
deep_copied_dict = copy.deepcopy(original_dict)

# 修改深拷贝字典中的不可变值
deep_copied_dict['a'] = 2
deep_copied_dict['b'] = 'world'
deep_copied_dict['c'] = (3, 4)

print(original_dict)
print(deep_copied_dict)

在这个例子中,修改深拷贝字典中的不可变值同样不会影响原字典,因为深拷贝创建了独立的对象。

浅拷贝和深拷贝的性能对比

一般来说,深拷贝的性能开销比浅拷贝大。因为深拷贝需要递归地复制所有的值,包括嵌套的可变对象,这涉及到更多的内存分配和对象复制操作。而浅拷贝只需要复制字典的顶层结构,对于可变值只是复制引用,相对来说开销较小。

简单性能测试示例

import copy
import timeit


original_dict = {'a': [1, 2, 3], 'b': {'sub': 'value'}}

# 测试浅拷贝性能
def shallow_copy_test():
    return original_dict.copy()


shallow_time = timeit.timeit(shallow_copy_test, number = 10000)

# 测试深拷贝性能
def deep_copy_test():
    return copy.deepcopy(original_dict)


deep_time = timeit.timeit(deep_copy_test, number = 10000)

print(f"浅拷贝10000次耗时: {shallow_time} 秒")
print(f"深拷贝10000次耗时: {deep_time} 秒")

运行上述代码,你会发现深拷贝10000次所花费的时间通常比浅拷贝要长。这是因为深拷贝的复杂性更高,需要更多的计算资源。

选择浅拷贝还是深拷贝

  1. 当不需要独立修改字典中的可变值时:如果在程序中不需要对拷贝后的字典中的可变值进行独立修改,即不希望修改新字典影响原字典,也不希望原字典的修改影响新字典,那么浅拷贝是一个不错的选择。浅拷贝的性能开销较小,适用于对性能要求较高且不需要独立值的场景。例如,在一些只读操作较多的场景中,浅拷贝可以节省内存和时间。
  2. 当需要独立修改字典中的可变值时:如果在程序中需要对拷贝后的字典中的可变值进行独立修改,即希望新字典和原字典完全独立,互不影响,那么深拷贝是必要的。虽然深拷贝性能开销较大,但能确保数据的独立性,避免因引用导致的意外数据修改。例如,在一些数据处理场景中,需要对数据进行隔离修改时,深拷贝能满足需求。

嵌套字典的浅拷贝和深拷贝

  1. 嵌套字典的浅拷贝:当字典中嵌套了字典时,浅拷贝只会复制顶层字典的键值对,内部嵌套的字典仍然是引用。
original_nested_dict = {'outer': {'inner': [1, 2, 3]}}
shallow_copied_nested_dict = original_nested_dict.copy()

# 修改浅拷贝嵌套字典中的内部字典
shallow_copied_nested_dict['outer']['inner'].append(4)

print(original_nested_dict)
print(shallow_copied_nested_dict)

在上述代码中,original_nested_dict是一个嵌套字典。浅拷贝后,shallow_copied_nested_dict中的{'inner': [1, 2, 3]}与原字典中的是同一个对象,所以修改shallow_copied_nested_dict中的inner列表会影响到original_nested_dict。 2. 嵌套字典的深拷贝:深拷贝会递归地复制嵌套字典中的所有层次。

import copy
original_nested_dict = {'outer': {'inner': [1, 2, 3]}}
deep_copied_nested_dict = copy.deepcopy(original_nested_dict)

# 修改深拷贝嵌套字典中的内部字典
deep_copied_nested_dict['outer']['inner'].append(4)

print(original_nested_dict)
print(deep_copied_nested_dict)

这里,深拷贝后deep_copied_nested_dict中的{'inner': [1, 2, 3]}是一个全新的对象,修改它不会影响到original_nested_dict

字典与其他数据结构结合时的拷贝

  1. 字典与列表结合的浅拷贝:当字典中的值是列表,并且对包含字典的列表进行浅拷贝时,字典本身是浅拷贝的。
original_list_with_dict = [{'a': [1, 2, 3]}, {'b': [4, 5, 6]}]
shallow_copied_list_with_dict = original_list_with_dict.copy()

# 修改浅拷贝列表中字典的值
shallow_copied_list_with_dict[0]['a'].append(4)

print(original_list_with_dict)
print(shallow_copied_list_with_dict)

在这个例子中,original_list_with_dict是一个包含字典的列表。浅拷贝original_list_with_dict后,列表中的字典是浅拷贝的,所以修改浅拷贝列表中字典的值会影响原列表中的字典。 2. 字典与列表结合的深拷贝:如果要对包含字典的列表进行深拷贝,需要使用copy.deepcopy

import copy
original_list_with_dict = [{'a': [1, 2, 3]}, {'b': [4, 5, 6]}]
deep_copied_list_with_dict = copy.deepcopy(original_list_with_dict)

# 修改深拷贝列表中字典的值
deep_copied_list_with_dict[0]['a'].append(4)

print(original_list_with_dict)
print(deep_copied_list_with_dict)

此时,深拷贝后的列表中的字典及其内部的列表都是全新的对象,修改深拷贝列表中字典的值不会影响原列表。

字典浅拷贝和深拷贝在实际项目中的应用场景

  1. 浅拷贝在缓存场景中的应用:在一些缓存场景中,可能会使用浅拷贝。例如,有一个缓存字典,其中的值是一些配置信息,这些配置信息在大部分情况下是只读的。当需要获取缓存数据进行一些计算时,可以使用浅拷贝。这样可以节省内存和时间,同时由于配置信息一般不会被修改,所以不会出现数据一致性问题。
cache_dict = {'config': {'param1': 10, 'param2': 'value'}}
# 获取缓存数据进行计算
local_cache = cache_dict.copy()
# 这里对local_cache进行计算,不会影响cache_dict
  1. 深拷贝在数据隔离场景中的应用:在数据处理项目中,经常需要对数据进行隔离操作。例如,有一个包含用户数据的字典,每个用户数据又包含一些可变的配置信息。当需要对某个用户的数据进行独立处理时,就需要使用深拷贝。
import copy
user_data_dict = {'user1': {'settings': {'theme': 'dark', 'language': 'en'}},
                  'user2': {'settings': {'theme': 'light', 'language': 'zh'}}}

# 对user1的数据进行独立处理
user1_data = copy.deepcopy(user_data_dict['user1'])
user1_data['settings']['theme'] = 'custom'

print(user_data_dict)
print(user1_data)

在这个例子中,通过深拷贝user1的数据,可以在不影响其他用户数据的情况下对user1的数据进行修改。

注意事项

  1. 循环引用问题:在处理复杂数据结构时,可能会出现循环引用的情况。例如,一个字典中的值又引用回了这个字典本身。在进行深拷贝时,copy.deepcopy会检测并处理循环引用,避免无限递归。但浅拷贝不会处理循环引用,如果不小心使用浅拷贝处理包含循环引用的数据结构,可能会导致程序出现难以调试的错误。
import copy
# 创建一个包含循环引用的字典
a = {'self_ref': None}
a['self_ref'] = a

# 浅拷贝会导致问题(这里不详细展开错误情况)
# shallow_copied = a.copy()

# 深拷贝可以正确处理循环引用
deep_copied = copy.deepcopy(a)
print(deep_copied)
  1. 自定义对象的拷贝:如果字典中的值是自定义对象,浅拷贝和深拷贝的行为可能需要根据自定义对象的__copy____deepcopy__方法来确定。如果自定义对象没有实现这些方法,默认的浅拷贝和深拷贝行为可能不符合预期。在这种情况下,需要根据实际需求在自定义对象中正确实现__copy____deepcopy__方法。
import copy


class MyClass:
    def __init__(self, value):
        self.value = value


my_obj = MyClass(10)
original_dict = {'obj': my_obj}

# 浅拷贝
shallow_copied_dict = original_dict.copy()
# 深拷贝
deep_copied_dict = copy.deepcopy(original_dict)

print(shallow_copied_dict['obj'] is original_dict['obj'])
print(deep_copied_dict['obj'] is original_dict['obj'])

在上述代码中,由于MyClass没有实现__copy____deepcopy__方法,浅拷贝和深拷贝后的对象与原对象是同一个(这里通过is判断)。如果希望在拷贝时创建新的MyClass对象,需要在MyClass中实现相应的拷贝方法。

通过对Python字典浅拷贝和深拷贝的详细介绍,包括它们的实现方式、对可变和不可变值的影响、性能对比、在不同数据结构结合时的情况、实际应用场景以及注意事项等方面,相信你对字典的拷贝有了更深入的理解,在实际编程中能够根据具体需求正确选择使用浅拷贝还是深拷贝,避免因拷贝不当导致的错误和性能问题。