可变对象与不可变对象 在面向对象中,对象是类的一个实例;而在Python中,对象就是变量。 Python中一切皆对象,分为 不可变对象 与 可变对象。
存储方式 在理解可变与不可变之前,先来看看Python对象在内存中是如何存储的: 对象有三个数据:id
(标识。在CPython中,对应内存地址)、type
(类型)、value
(值)。
1 2 3 4 5 >>> message = 'hello' >>> id(message)2836204005360 >>> type(message)<class 'str '>
其中,id([object])
获取对象的内存地址,type([object])
获取对象的类型。 变量 message
并不是真正的对象,它是对象的引用,相当于记录了对象在堆空间的地址,通过这个地址可以访问到对应的对象。
不可变(immutable)与可变(mutable) 上例中,存储在堆中的字符串对象是独立的(没有引用外部),所以它本身是不可变的。 不可变对象:对象在内存中的值不能被改变。当改变某个变量时,由于其所引用的对象不可变,所以创建了新对象,变量再指向这个新对象的地址。
1 2 3 >>> message += ' world' >>> id(message)2836203997232
内置的不可变对象有 int、float、str、tuple。
同理,可变对象:对象在内存中的值可以被改变。 实际上就是对象的值引用了外部对象,值变了(即,引用变了)但不影响对象,所以未生成新的对象,只是对象内部发生了改变。
1 2 3 4 5 6 >>> students = ['lilei' , 'hanmeimei' , 'jim' ]>>> id(students)2836201579200 >>> students[2 ] = 'lucy' >>> id(students)2836201579200
虽然 students
引用的还是原来那个列表对象,但对象的值发生了改变,即,students[3]
指向了不同的字符串对象。 内置的可变对象有 list、set、dict。
可哈希对象 官方文档上对 hashable 做了明确说明:
一个对象的哈希值如果在其生命周期内固定不变(这需要 __hash__()
方法)且可以与其他对象进行比较(这需要 __eq__()
方法),那么这个对象就是可哈希对象(hashable)。相等的可哈希对象必须具有相同的哈希值。 可哈希性使得对象能够作为字典的键或集合成员使用,因为这些数据结构要在内部使用哈希值。 大多数 Python 中的不可变内置对象都是可哈希的;可变容器(例如 list 和 dict)都不可哈希;不可变容器(例如 tuple 和 frozenset)仅当它们的元素均为可哈希时才是可哈希的。 自定义对象默认是可哈希的。它们在比较时一定不相同(除非是与自己比较),它们的哈希值的生成是基于它们的 id()
。https://docs.python.org/zh-cn/3.9/glossary.html#term-hashable
但若重写 __hash__()
方法、__eq__()
方法,则可能改变自定义对象的可哈希性:
如果一个类没有定义 __eq__()
方法,那么也不应该定义 __hash__()
操作;如果它定义了 __eq__()
但没有定义 __hash__()
,则其实例将不可被用作可哈希集的项(即 unhashable
)。如果一个类定义了可变对象并实现了 __eq__()
方法,则不应该实现 __hash__()
,因为可哈希集的实现要求键的哈希集是不可变的(如果对象的哈希值发生改变,它将处于错误的哈希桶中)。https://docs.python.org/zh-cn/3/reference/datamodel.html#object.__hash __
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import randomclass MyObject (object) : def __init__ (self, hash_=None) : self.hash_ = 222 if hash_ is None else hash_ def __hash__ (self) : """ 若在对象生命周期内哈希值发生改变,则会导致哈希集发生混乱。 比如当此对象作为字典的键时,每次通过此对象从字典中取值都不一定能返回正确的数据。 """ return self.hash_ def __eq__ (self, other) : return False my_dict = {} o1 = MyObject() print('o1\t__hash__: %s, hash: %s, id: %s' % (o1.__hash__, hash(o1), id(o1))) my_dict[o1] = 'world1' o2 = MyObject() print('o2\t__hash__: %s, hash: %s, id: %s' % (o2.__hash__, hash(o2), id(o2))) my_dict[o2] = 'world2' print(my_dict) print('my_dict[o1]: %s, my_dict[o2]: %s' % (my_dict.get(o1), my_dict.get(o2))) """ 执行结果: o1 __hash__: <bound method MyObject.__hash__ of <__main__.MyObject object at 0x000001CDC3B2AFD0>>, hash: 222, id: 1983263190992 o2 __hash__: <bound method MyObject.__hash__ of <__main__.MyObject object at 0x000001CDC3B2AF70>>, hash: 222, id: 1983263190896 {<__main__.MyObject object at 0x000001CDC3B2AFD0>: 'world1', <__main__.MyObject object at 0x000001CDC3B2AF70>: 'world2'} my_dict[o1]: world1, my_dict[o2]: world2 若 __eq__() 返回 True,则 o1 等于 o2,执行结果为: o1 __hash__: <bound method MyObject.__hash__ of <__main__.MyObject object at 0x00000132260AAFD0>>, hash: 222, id: 1314898227152 o2 __hash__: <bound method MyObject.__hash__ of <__main__.MyObject object at 0x00000132260AAF70>>, hash: 222, id: 1314898227056 {<__main__.MyObject object at 0x00000132260AAFD0>: 'world2'} my_dict[o1]: world2, my_dict[o2]: world2 """
所以,理论上,可哈希对象一定是不可变对象,但 unhashable
未必是可变对象,也有可能仅仅是因为没有实现 __hash__()
。
浅拷贝(shallow copy)与深拷贝(deep copy) 赋值 在讲拷贝之前,先来看看常见的赋值操作。 赋值并不是拷贝对象,而是把变量的值(对象的内存地址)拷贝给另一个变量,比如:
1 2 3 4 >>> str1 = 'hello world' >>> str2 = str1>>> id(str1), id(str2)(2195276276528 , 2195276276528 )
str1
和 str2
指向同一个字符串对象。
浅拷贝 拷贝分浅拷贝和深拷贝,copy
模块分别提供了两个API:copy.copy(x)
和 copy.deepcopy(x)
。 浅拷贝仅拷贝对象本身,不拷贝子对象,所以修改拷贝后对象的可变子对象也会影响到原对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import copylist_a = ['2020' ] list_b = copy.copy(list_a) list_a[0 ] = '2021' print('list_a: %s, list_b: %s' % (list_a, list_b)) list_a = ['2020' , ['aaa' ]] list_b = copy.copy(list_a) list_a[1 ][0 ] = 'bbb' print('id(list_a[1]): %s, id(list_b[1]): %s' % (id(list_a[1 ]), id(list_b[1 ]))) print('list_a: %s, list_b: %s' % (list_a, list_b))
示例1:list_a[0]
指向的子对象是一个不可变对象,拷贝后 list_b[0]
也指向同一个字符串对象,所以修改 list_a[0]
指向另一个字符串对象并不影响 list_b[0]
。 示例2:list_a[1]
指向的子对象是一个可变对象,浅拷贝只是拷贝 list_a[1]
的值(并不会拷贝变量指向的对象),即 list_b[1]
和 list_a[1]
指向同一个列表对象,所以修改 list_a[1][0]
也会影响到 list_b[1][0]
。
深拷贝 相对于浅拷贝,深拷贝就很彻底了,会递归拷贝每一层对象。
1 2 3 4 5 list_a = ['2020' , ['aaa' ]] list_b = copy.deepcopy(list_a) list_a[1 ][0 ] = 'bbb' print('id(list_a[1]): %s, id(list_b[1]): %s' % (id(list_a[1 ]), id(list_b[1 ]))) print('list_a: %s, list_b: %s' % (list_a, list_b))
list_a[1]
和 list_b[1]
指向了不同的列表对象。 虽然深拷贝可以完整复制对象,但占用空间大,速度慢,所以,多数情况下尽量使用浅拷贝(Python中多数操作也默认是浅拷贝,比如切片)。
最后,再次说明下,拷贝只是复制内存地址,并不会复制非容器类型(int、float、str),所以如下代码实际上是一样的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import copystr1 = 'hello world' str2 = str1 str3 = copy.copy(str1) str4 = copy.deepcopy(str1) print('id(str1): %s' % id(str1)) print('id(str2): %s' % id(str2)) print('id(str3): %s' % id(str3)) print('id(str4): %s' % id(str4)) """ 执行结果: id(str1): 2292725768816 id(str2): 2292725768816 id(str3): 2292725768816 id(str4): 2292725768816 """
对 str1
的浅拷贝 和 深拷贝,实际上都只是赋值而已。
自定义对象的拷贝 虽然Python默认提供了自定义对象的拷贝实现,但想要明白具体拷贝了什么并能按需实现自己的拷贝逻辑,还是得先弄懂拷贝的原理。
拷贝的原理 直接看源码:https://github.com/python/cpython/blob/3.9/Lib/copy.py 官方文档上也做了说明,https://docs.python.org/zh-cn/3/library/copy.html
浅拷贝和深拷贝之间的区别仅在于复合对象(包含其他对象的对象,eg. 列表、类实例):
浅拷贝将构造一个新的复合对象,然后将所包含的对象直接插入到新的复合对象中。
深拷贝将构造一个新的复合对象,然后递归地拷贝所包含的对象并插入到新的复合对象中。
不同于浅拷贝,深拷贝存在两个问题:
递归对象(直接或间接地包含对自己的引用)可能会导致循环拷贝。
拷贝所有导致拷贝太多,如共享的数据结构等。
针对这两个问题,有对应的解决方案:
保存一张已拷贝的对象表。1 2 3 4 5 6 7 8 9 10 11 12 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) y = memo.get(d, _nil) if y is not _nil: return y
允许在自定义类中重写拷贝实现。 当前不支持拷贝的类型有:module、class、function、method、stack trace、stack frame、file、socket、window、array以及上述类型的相似类型。
内置对象中,浅拷贝的实现相对简单,可以自己看源码理解;深拷贝主要用递归拷贝来实现,如:
1 2 3 4 5 6 7 def _deepcopy_dict (x, memo, deepcopy=deepcopy) : y = {} memo[id(x)] = y for key, value in x.items(): y[deepcopy(key, memo)] = deepcopy(value, memo) return y d[dict] = _deepcopy_dict
接下来,着重来说说自定义对象的拷贝实现:__copy__
/__deepcopy__
->dispatch_table
->__reduce_ex__
/__reduce__
->__getstate__
/__setstate__
__copy__
和 __deepcopy__
首先判断对象中是否有 __copy__
或 __deepcopy__
。__copy__
和 __deepcopy__
分别对应浅拷贝和深拷贝的自定义实现。 如何自定义 __copy__
和 __deepcopy__
?按照前面梳理的拷贝思路来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import copyclass MyObject (object) : def __init__ (self) : print('__init__' ) self.name = 'lilei' self.message = ['hi' , 'hanmeimei' ] def __copy__ (self) : print('__copy__' ) cls = self.__class__ obj = cls.__new__(cls) obj.__dict__.update(self.__dict__) return obj def __deepcopy__ (self, memo) : print('__deepcopy__' ) cls = self.__class__ obj = cls.__new__(cls) memo[id(self)] = obj for k, v in self.__dict__.items(): setattr(obj, k, copy.deepcopy(v, memo)) return obj print('----- 自定义对象浅拷贝 -----' ) o1 = MyObject() o2 = copy.copy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) print('----- 自定义对象深拷贝 -----' ) o1 = MyObject() o2 = copy.deepcopy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) """ 执行结果: ----- 自定义对象浅拷贝 ----- __init__ __copy__ ['hi', 'lucy'] ['hi', 'lucy'] ----- 自定义对象深拷贝 ----- __init__ __deepcopy__ ['hi', 'hanmeimei'] ['hi', 'lucy'] """
其中,__dict__
存储了对象的一些属性(不包含特殊属性等);类和对象分别拥有自己的 __dict__
。
dispatch_table 如果没有自定义的 __copy__
或 __deepcopy__
,会接着判断 dispatch_table 中是否有对应类型的 归约函数 。 (规约函数 为序列化指定了范围,即在序列化时指明对哪些数据进行序列化操作,至于具体的序列化操作,由序列化协议实现。) dispatch_table 是 copyreg
模块中用来保存 规约函数 的字典,以对象类型作为key;通过 copyreg.pickle
进行添加。https://docs.python.org/zh-cn/3/library/copyreg.html#copyreg.pickle 具体实现详见:https://github.com/python/cpython/blob/3.9/Lib/copyreg.py#L12 所以,通过注册 规约函数 来达成自定义的拷贝数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import copy, copyregclass MyObject (object) : def __init__ (self, message=None) : print('__init__' ) self.name = 'lilei' self.message = ['hi' , 'hanmeimei' ] if message is None else message def pickle_my_object (obj) : print('pickling a MyObject instance...' ) return MyObject, (obj.message,), obj.__dict__ copyreg.pickle(MyObject, pickle_my_object) print('----- 自定义对象浅拷贝 -----' ) o1 = MyObject() o2 = copy.copy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) print('----- 自定义对象深拷贝 -----' ) o1 = MyObject() o2 = copy.deepcopy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) print('----- 序列化测试 -----' ) o1 = MyObject() print('--- pickle ---' ) data = pickle.dumps(o1) print('--- unpickle ---' ) o2 = pickle.loads(data) o2.message[1 ] = 'lucy' print(o1.message, o2.message) """ 执行结果: ----- 自定义对象浅拷贝 ----- __init__ pickling a MyObject instance... __init__ ['hi', 'lucy'] ['hi', 'lucy'] ----- 自定义对象深拷贝 ----- __init__ pickling a MyObject instance... __init__ ['hi', 'hanmeimei'] ['hi', 'lucy'] ----- 序列化测试 ----- __init__ --- pickle --- pickling a MyObject instance... --- unpickle --- __init__ ['hi', 'hanmeimei'] ['hi', 'lucy'] """
对比之下可以看出,拷贝实际上就是通过序列化和反序列化来实现的 。 细心的你可能会发现,为什么拷贝的对象也执行了 __init__
。 这是因为 规约函数 返回的元组在处理后用于生成新的对象实例,具体实现详见:https://github.com/python/cpython/blob/3.9/Lib/copy.py#L264
__reduce_ex__
或 __reduce__
dispatch_table 可能不够灵活,特别是想要基于对象类型以外的其他规则来对序列化进行定制,或是想要对函数和类的序列化进行定制的时候。 这时,可能要基于 Pickler 类进行子类化并实现 reducer_override()
方法(Python3.8支持)。 此方法的返回内容参照 __reduce__()
,它也可以选择返回 NotImplemented
来回退到传统行为。https://docs.python.org/zh-cn/3/library/pickle.html#custom-reduction-for-types-functions-and-other-objects 从 pickle 的源码中可以看出,序列化时,调用的优先级顺序为:reducer_override
-> dispatch_table
-> __reduce_ex__
-> __reduce__
https://github.com/python/cpython/blob/3.9/Lib/pickle.py#L535
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import copy, io, pickleclass MyObject (object) : def __init__ (self, message=None) : print('__init__' ) self.name = 'lilei' self.message = ['hi' , 'hanmeimei' ] if message is None else message class MyPickler (pickle.Pickler) : def reducer_override (self, obj) : """为 MyObject 类创建的对象定义 reducer""" if getattr(obj, '__class__' , None ) is MyObject: print('--- reducer_override ---' ) return MyObject, (obj.message,), obj.__dict__ else : return NotImplemented print('----- 自定义对象浅拷贝 -----' ) o1 = MyObject() o2 = copy.copy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) print('----- 自定义对象深拷贝 -----' ) o1 = MyObject() o2 = copy.deepcopy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) print('----- 序列化测试 -----' ) o1 = MyObject() f = io.BytesIO() p = MyPickler(f) p.dump(o1) o2 = pickle.loads(f.getvalue()) o2.message[1 ] = 'lucy' print(o1.message, o2.message) """ 执行结果: ----- 自定义对象浅拷贝 ----- __init__ ['hi', 'lucy'] ['hi', 'lucy'] ----- 自定义对象深拷贝 ----- __init__ ['hi', 'hanmeimei'] ['hi', 'lucy'] ----- 序列化测试 ----- __init__ reducer_override __init__ ['hi', 'hanmeimei'] ['hi', 'lucy'] """
相比 __reduce__
,__reduce_ex__
支持指定序列化协议版本,主要用于为以前的Python版本提供向后兼容。__reduce__
的定义详见官方文档:https://docs.python.org/zh-cn/3/library/pickle.html#object.__reduce __
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import copyclass MyObject (object) : def __init__ (self, message=None) : print('__init__' ) self.name = 'lilei' self.message = ['hi' , 'hanmeimei' ] if message is None else message def __reduce__ (self) : print('__reduce__' ) return self.__class__, (self.message,), self.__dict__ print('----- 自定义对象浅拷贝 -----' ) o1 = MyObject() o2 = copy.copy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) print('----- 自定义对象深拷贝 -----' ) o1 = MyObject() o2 = copy.deepcopy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) """ 执行结果: ----- 自定义对象浅拷贝 ----- __init__ __reduce__ __init__ ['hi', 'lucy'] ['hi', 'lucy'] ----- 自定义对象深拷贝 ----- __init__ __reduce__ __init__ ['hi', 'hanmeimei'] ['hi', 'lucy'] """
可以看出,reducer_override()
和 __reduce__
的返回数据跟 dispatch_table
的处理方式是一样,都初始化创建了新的对象。
__getstate__
和 __setstate__
实际上,前面介绍的都不是Python默认的拷贝实现,而是通过几个特殊方法来实现 __reduce__
。https://docs.python.org/zh-cn/3/library/pickle.html#object.__getstate __
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import copy, pickleclass MyObject (object) : def __init__ (self, message=None) : print('__init__' ) self.name = 'lilei' self.message = ['hi' , 'hanmeimei' ] if message is None else message def __getstate__ (self) : print('__getstate__' ) state = self.__dict__ return state def __setstate__ (self, state) : print('__setstate__' ) self.__dict__.update(state) print('----- 自定义对象浅拷贝 -----' ) o1 = MyObject() o2 = copy.copy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) print('----- 自定义对象深拷贝 -----' ) o1 = MyObject() o2 = copy.deepcopy(o1) o2.message[1 ] = 'lucy' print(o1.message, o2.message) print('----- 序列化测试 -----' ) o1 = MyObject() print('--- pickle --- ' ) data = pickle.dumps(o1) print('--- unpickle ---' ) o2 = pickle.loads(data) o2.message[1 ] = 'lucy' print(o1.message, o2.message) """ 执行结果: ----- 自定义对象浅拷贝 ----- __init__ __getstate__ __setstate__ ['hi', 'lucy'] ['hi', 'lucy'] ----- 自定义对象深拷贝 ----- __init__ __getstate__ __setstate__ ['hi', 'hanmeimei'] ['hi', 'lucy'] ----- 序列化测试 ----- __init__ --- pickle --- __getstate__ --- unpickle --- __setstate__ ['hi', 'hanmeimei'] ['hi', 'lucy'] """
可以看出,实际上,__getstate__
对应序列化操作,__setstate__
对应反序列化操作。 细心的你可能也发现了,这边在反序列化时并没有初始化实例。对于这点,官方也给了说明:
当实例解封时,它的 __init__()
方法通常不会被调用。其默认动作是: 先创建一个未初始化的实例(obj = cls.__new__(cls)
),然后还原其属性(obj.__dict__.update(attributes)
)。https://docs.python.org/zh-cn/3/library/pickle.html#pickle-inst
几个问题 关于堆栈 堆和栈是两个不同的概念。 程序运行时需要占用内存空间来存储数据和代码,这些内存从逻辑上划分为五个部分,按照地址从高到低依次为:栈(Stack)、堆(Heap)、数据段(Data Segment)、只读数据段(Static Area)和代码段(Code Segment)。 其中,栈用来存储局部、临时变量,以及函数调用时保存现场和恢复现场需要用到的数据,这部分内存在代码块开始执行时自动分配,代码块执行结束时自动释放,通常由编译器自动管理。 堆的大小不固定,可以动态的分配和回收,因此如果程序中有大量的数据需要处理,这些数据通常都放在堆上,如果堆空间没有正确的被释放会引发内存泄露的问题,而像Python、Java等编程语言都使用了垃圾回收机制来实现自动化的内存管理(自动回收不再使用的堆空间)。
自定义对象和函数是否为可变对象? 直观上理解,我们会觉得自定义对象肯定是可变对象,因为可以随意增删改属性而不影响变量指向的对象。 但,真如我们理解的那样吗,带着疑惑,上网搜了一圈,终于在知乎上找到了相似问题:https://www.zhihu.com/question/359026281 可变与不可变一般针对内置类型来说,区分可变与不可变是为了在特定场合下使用而给出的概念;脱离实际使用场景,纠结可变与不可变反而显得无意义。 比如,dict
的 key 必须是不可变对象,这是因为 dict 根据 key 来计算 value 的存储位置(采用的是哈希算法),所以,要保证 Hash 的正确性,作为 key 的对象必须不可变。按照可变与不可变的定义,考虑的是对象的值而非属性 。
自定义对象默认是可哈希对象,同时也是不可变对象;只有重写 __eq__()
且未重写 __hash__()
时才是可变对象。 同理,函数虽然可以设置属性,但函数对象是有固定的 __hash__()
实现,因而函数对象不可变。
参考资料 Python函数是可变对象还是不可变对象,https://www.zhihu.com/question/359026281 那些年我们踩过的那些坑 - 嵌套列表 How to override the copy/deepcopy operations for a Python object 浅拷贝、深拷贝完全解读