楔子
本次来聊一聊序列化和反序列化,像内置的 pickle, json 库都可以将对象序列化和反序列化,这里我们说的是 pickle。
pickle 和 json 不同,json 序列化之后的结果是人类可阅读的,但是能序列化的对象有限,因为序列化的结果可以在不同语言之间传递;而 pickle 序列化之后是二进制格式,只有 Python 才认识,因此它可以序列化 Python 的绝大部分对象。
import pickle class Girl: def __init__(self, name, age): self.name = name self.age = age girl = Girl("古明地觉", 16) # 这便是序列化的结果 dumps_obj = pickle.dumps(girl) # 显然这是什么东西我们不认识, 但解释器认识 print(dumps_obj[: 20]) """ b'\x80\x04\x95;\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main_' """ # 我们可以再进行反序列化 loads_obj = pickle.loads(dumps_obj) print(loads_obj.name) # 古明地觉 print(loads_obj.age) # 16
这里我们不探究 pickle 的实现原理,我们来说一下如何自定制序列化和反序列化的过程。如果想自定制的话,需要实现 __getstate__ 和 __setstate__ 两个魔法方法:
import pickle class Girl: def __init__(self, name, age): self.name = name self.age = age def __getstate__(self): """序列化的时候会调用""" # 对 Girl 的实例对象进行序列化的时候 # 默认会返回其属性字典 # 这里我们多添加一个属性 print("被序列化了") return {**self.__dict__, "gender": "female"} def __setstate__(self, state): """反序列化时会调用""" # 对 Girl 的实例对象进行反序列化的时候 # 会将 __getstate__ 返回的字典传递给这里的 state 参数 # 我们再设置到 self 当中 # 如果不设置,那么反序列化之后是无法获取属性的 print("被反序列化了") self.__dict__.update(**state) girl = Girl("古明地觉", 16) dumps_obj = pickle.dumps(girl) """ 被序列化了 """ loads_obj = pickle.loads(dumps_obj) print(loads_obj.name) print(loads_obj.age) print(loads_obj.gender) """ 被反序列化了 古明地觉 16 female """
虽然反序列化的时候会调用 __setstate__,但实际上会先调用 __reduce__,__reduce__ 必须返回一个字符串或元组。
我们先来看看返回字符串是什么结果。
import pickle class Girl: def __init__(self, name, age): self.name = name self.age = age def __reduce__(self): print("__recude__") # 当返回字符串时,这里是 "girl" # 那么在反序列化之后就会返回 eval("girl") return "girl" girl = Girl("古明地觉", 16) dumps_obj = pickle.dumps(girl) # 反序列化 loads_obj = pickle.loads(dumps_obj) print(loads_obj.name) print(loads_obj.age) """ __recude__ 古明地觉 16 """
如果我们返回一个别的字符串是会报错的,假设返回的是 "xxx",那么反序列化的时候会提示找不到变量 xxx。那如果我们在外面再定义一个变量 xxx 呢?比如 xxx = 123,这样做也是不可以的,因为 pickle 要求序列化的对象和反序列化得到的对象必须是同一个对象。
因此 __reduce__ 很少会返回一个字符串,更常用的是返回一个元组,并且元组里面的元素个数为 2 到 6 个,每个含义都不同,我们分别举例说明。
返回的元组包含两个元素
当只有两个元素时,第一个元素必须是可调用对象,第二个元素表示可调用对象的参数(必须也是一个元组),相信你已经猜到会返回什么了:
import pickle class Girl: def __init__(self, name, age): self.name = name self.age = age def __reduce__(self): # 反序列化时会返回 range(*(1, 10, 2)) return range, (1, 10, 2) # 如果是 return int, ("123",) # 那么反序列化时会返回 int("123") # 所以此时返回的可以是任意的对象 girl = Girl("古明地觉", 16) dumps_obj = pickle.dumps(girl) loads_obj = pickle.loads(dumps_obj) print(loads_obj) # range(1, 10, 2) print(list(loads_obj)) # [1, 3, 5, 7, 9]
返回的元组包含三个元素
包含三个元素时,那么第三个元素是一个字典,会将该字典设置到返回对象的属性字典中。
import pickle class A: pass class Girl: def __init__(self, name, age): self.name = name self.age = age def __reduce__(self): # 当然返回 Girl 的实例也是可以的 # 只要保证对象有属性字典即可 return A, (), {"a": 1, "b": 2} girl = Girl("古明地觉", 16) dumps_obj = pickle.dumps(girl) loads_obj = pickle.loads(dumps_obj) print(loads_obj.__class__) print(loads_obj.__dict__) """ <class '__main__.A'> {'a': 1, 'b': 2} """
如果定义了 __reduce__ 的同时还定义了 __setstate__,那么第三个元素就不会设置到返回对象的属性字典中了,而是会作为参数传递到 __setstate__ 中进行调用:
import pickle class Girl: def __init__(self, name, age): self.name = name self.age = age def __setstate__(self, state): # state 就是 __reduce__ 返回的元组里的第三个元素 # 注意这个 self 也是 __reduce__ 的返回对象 print(state) def __reduce__(self): # 此时的第三个元素可以任意 return Girl, ("古明地恋", 15), ("ping", "pong") girl = Girl("古明地觉", 16) dumps_obj = pickle.dumps(girl) loads_obj = pickle.loads(dumps_obj) """ ('ping', 'pong') """ print(loads_obj.__dict__) """ {'name': '古明地恋', 'age': 15} """
所以当定义了 __reduce__ 的同时还定义了 __setstate__,那么第三个元素就可以不是字典了。如果没有 __setstate__,那么第三个元素必须是一个字典(或者指定为 None 相当于没指定)。
返回的元组包含四个元素
当包含四个元素时,那么第四个元素必须是一个迭代器,然后返回的对象内部必须有 append 方法。会遍历迭代器的每一个元素,并作为参数传递到 append 中进行调用。
import pickle class Girl: def __init__(self, name, age): self.name = name self.age = age self.where = [] def append(self, item): self.where.append(item) def __reduce__(self): """ 从第三个元素开始,如果指定为 None,那么相当于什么也不做 比如这里第三个元素我们指定为 None 那么是不会有 "往属性字典添加属性" 这一步的 即使定义了 __setstate__,该方法也不会调用 但是前两个元素必须指定、且不可以为 None """ return Girl, ("雾雨魔理沙", 17), None, \ iter(["雾雨魔理沙", "雾雨魔法店", "魔法森林"]) girl = Girl("古明地觉", 16) dumps_obj = pickle.dumps(girl) loads_obj = pickle.loads(dumps_obj) print( loads_obj.where ) # ['雾雨魔理沙', '雾雨魔法店', '魔法森林']
注意 append 方法里面的 self,这个 self 指的是 __reduce__ 的返回对象。因此这种方式非常适合列表,因为列表本身就有 append 方法。
import pickle class Girl: def __init__(self, name, age): self.name = name self.age = age def __reduce__(self): return list, (), None, \ iter(["雾雨魔理沙", "雾雨魔法店", "魔法森林"]) girl = Girl("古明地觉", 16) dumps_obj = pickle.dumps(girl) loads_obj = pickle.loads(dumps_obj) print( loads_obj ) # ['雾雨魔理沙', '雾雨魔法店', '魔法森林']
所以还是有点神奇的,我们明明是对 Girl 的实例序列化之后的结果进行反序列化,理论上也应该得到 Girl 的实例才对,现在却得到了一个列表,原因就是里面指定了 __reduce__。
并且此时第三个元素就不能指定了,如果指定为字典,那么会加入到返回对象的属性字典中。但我们的返回对象是一个列表,列表没有自己的属性字典,并且它也没有 __setstate__。
返回的元组包含五个元素
当包含五个元素时,那么第五个元素必须也是一个迭代器,并且内部的每个元素都是一个 2-tuple。同时要求返回的对象必须有 __setitem__ 方法,举个栗子:
import pickle class Girl: def __init__(self, name, age): self.name = name self.age = age def __reduce__(self): # 依旧会遍历可迭代对象, 得到的是一个 2-tuple # 然后传递到 __setitem__ 中 return Girl, ("古明地觉", 16), None, None, \ iter([("name", "雾雨魔理沙"), ("age", "17")]) def __setitem__(self, key, value): print(f"key = {key!r}, value = {value!r}") self.__dict__[key] = value girl = Girl("古明地觉", 16) dumps_obj = pickle.dumps(girl) loads_obj = pickle.loads(dumps_obj) """ key = 'name', value = '雾雨魔理沙' key = 'age', value = '17' """ # 在 __setitem__ 中我们将 name 和 age 属性给换掉了 print( loads_obj.__dict__ ) # {'name': '雾雨魔理沙', 'age': '17'}
返回的元组包含六个元素
当包含六个元素时,那么第六个元素必须是一个可调用对象,但是在测试的时候发现这个可调用对象始终没被调用。
因为 pickle 底层实际上是 C 写的,位于 Modules/_pickle.c 中,所以试着查看了一下,没想到发现了玄机。
我们说在没有定义 __setstate__ 的时候,__reduce__ 返回的元组的第三个元素应该是一个字典(或者 None),会将字典加入到返回对象的属性字典中;但如果定义了,那么就不会加入到返回对象的属性字典中了,而是会作为参数传递给 __setstate__(此时第三个元素就可以不是字典了)。而第六个元素和 __setstate__ 的作用是相同的,举个栗子:
import pickle class Girl: def __init__(self, name, age): self.name = name self.age = age def __setstate__(self, state): print("__setstate__ 被调用了") def sixth_element(self, ins, val): print(f"sixth_element 被调用了") print(ins.__dict__, val) self.__dict__["name"] = val def __reduce__(self): # 我们指定的第六个元素需要是一个可调用对象 # 如果指定了,那么此时 __setstate__ 会无效化 return Girl, ("古明地觉", 16), "古明地恋", \ None, None, self.sixth_element girl = Girl("古明地觉", 16) dumps_obj = pickle.dumps(girl) # 反序列化的时候,会将返回对象和第三个元素作为参数 # 传递给 self.sixth_element 进行调用 loads_obj = pickle.loads(dumps_obj) """ sixth_element 被调用了 {'name': '古明地觉', 'age': 16} 古明地恋 """ # 这里我们将 name 属性的值给换掉了 print( loads_obj.__dict__ ) # {'name': '古明地恋', 'age': 16}
我们看到当指定了第六个元素的时候,__setstate__ 就不会被调用了,但是需要注意的是:self.sixth_element 里面的 self 指的是元组的前两个元素组成的返回对象。
假设返回的不是 Girl 实例,而是一个列表,那么就会报错,因为列表没有 sixth_element 方法。当然第六个元素比较特殊,我们也可以不指定为方法,指定为普通的函数也是可以的,只要它是一个接收两个参数的可调用对象即可。
以上就是 __reduce__ 的相关内容,除了 __reduce__ 之外还有一个 __reduce_ex__,用法类似,只不过在调用的时候会传递协议的版本。
关于 pickle 底层的原理其实也是蛮有意思的,这里就不展开了,总之 pickle 不是安全的,它在反序列化的时候不会对数据进行检测。这个特点可以被坏蛋们用来攻击别人,因此建议在反序列化的时候,只对那些受信任的数据进行反序列化。
扩展类实例的序列化和反序列化
最后是扩展类实例的序列化和反序列化,终于到我们的主角了。默认情况下 Cython 编译器也会为扩展类生成 __reduce__ 方法,和动态类一样,扩展类实例在反序列化之后和序列化之前的表现也是一致的,但是仅当所有成员都可以转成 Python 对象并且没有 __cinit__ 方法时才可以序列化。
cdef class Girl: cdef int *p
如果是这样一个扩展类,那么在对它的实例序列化时就会报错:self.p cannot be converted to a Python object for pickling。
如果我们想禁止扩展类的实例被 pickle 的话,可以通过装饰器 @cython.auto_pickle(False) 来实现,此时 Cython 编译器不会再为该扩展类生成 __reduce__ 方法。
cimport cython @cython.auto_pickle(False) cdef class Girl1: cdef readonly str name cdef int age def __init__(self): self.name = "古明地觉" self.age = 16 cdef class Girl2: cdef readonly str name cdef int age def __init__(self): self.name = "古明地觉" self.age = 16
文件名为 cython_test.pyx,下面编译测试一下:
import pyximport pyximport.install(language_level=3) import pickle import cython_test girl1 = cython_test.Girl1() try: pickle.dumps(girl1) except Exception as e: print(e) """ cannot pickle 'cython_test.Girl1' object """ girl2 = cython_test.Girl2() loads_obj = pickle.loads(pickle.dumps(girl2)) print(loads_obj.name) # 古明地觉 try: loads_obj.age except AttributeError as e: print(e) """ 'cython_test.Girl2' object has no attribute 'age' """ # 因为 age 没有对外暴露,所以访问不到 # 因此序列化之前的 girl2 和反序列化之后的 loads_obj 是一致的
以上就是自定义序列化和反序列化操作,说实话一般用 __getstate__ 和 __setstate__ 就足够了。
E N D