打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
扩展类实例的序列化和反序列化

楔子


本次来聊一聊序列化和反序列化,像内置的 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, (1102)
        # 如果是 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), NoneNone, \
               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), "古明地恋", \
               NoneNone, 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

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
[Python]Python模块学习 ---- pickle, cPickle 对象序列化/反序列化
python中的json、pickle
Python数据持久化:JSON| 编程派 | Coding Python
Python语言学习:Python常用自带库(imageio、pickle)简介、使用方法之详细攻略
关于python的对象序列化介绍
序列化——廖雪峰
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服