扩展类实例的序列化和反序列化

简介: 扩展类实例的序列化和反序列化


楔子




本次来聊一聊序列化和反序列化,像内置的 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


相关文章
|
1月前
|
JSON 数据格式 索引
Python中序列化/反序列化JSON格式的数据
【11月更文挑战第4天】本文介绍了 Python 中使用 `json` 模块进行序列化和反序列化的操作。序列化是指将 Python 对象(如字典、列表)转换为 JSON 字符串,主要使用 `json.dumps` 方法。示例包括基本的字典和列表序列化,以及自定义类的序列化。反序列化则是将 JSON 字符串转换回 Python 对象,使用 `json.loads` 方法。文中还提供了具体的代码示例,展示了如何处理不同类型的 Python 对象。
|
1月前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。
|
2月前
|
存储 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第9天】在Java的世界里,对象序列化是连接数据持久化与网络通信的桥梁。本文将深入探讨Java对象序列化的机制、实践方法及反序列化过程,通过代码示例揭示其背后的原理。从基础概念到高级应用,我们将一步步揭开序列化技术的神秘面纱,让读者能够掌握这一强大工具,以应对数据存储和传输的挑战。
|
2月前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第3天】在Java编程的世界里,对象序列化与反序列化是实现数据持久化和网络传输的关键技术。本文将深入探讨Java序列化的原理、应用场景以及如何通过代码示例实现对象的序列化与反序列化过程。从基础概念到实践操作,我们将一步步揭示这一技术的魅力所在。
|
1月前
|
存储 缓存 NoSQL
一篇搞懂!Java对象序列化与反序列化的底层逻辑
本文介绍了Java中的序列化与反序列化,包括基本概念、应用场景、实现方式及注意事项。序列化是将对象转换为字节流,便于存储和传输;反序列化则是将字节流还原为对象。文中详细讲解了实现序列化的步骤,以及常见的反序列化失败原因和最佳实践。通过实例和代码示例,帮助读者更好地理解和应用这一重要技术。
39 0
|
4月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
4月前
|
存储 开发框架 .NET
解锁SqlSugar新境界:利用Serialize.Linq实现Lambda表达式灵活序列化与反序列化,赋能动态数据查询新高度!
【8月更文挑战第3天】随着软件开发复杂度提升,数据查询的灵活性变得至关重要。SqlSugar作为一款轻量级、高性能的.NET ORM框架,简化了数据库操作。但在需要跨服务共享查询逻辑时,直接传递Lambda表达式不可行。这时,Serialize.Linq库大显身手,能将Linq表达式序列化为字符串,实现在不同服务间传输查询逻辑。结合使用SqlSugar和Serialize.Linq,不仅能够保持代码清晰,还能实现复杂的动态查询逻辑,极大地增强了应用程序的灵活性和可扩展性。
155 2
|
3月前
|
JSON fastjson Java
niubility!即使JavaBean没有默认无参构造器,fastjson也可以反序列化。- - - - 阿里Fastjson反序列化源码分析
本文详细分析了 Fastjson 反序列化对象的源码(版本 fastjson-1.2.60),揭示了即使 JavaBean 沲有默认无参构造器,Fastjson 仍能正常反序列化的技术内幕。文章通过案例展示了 Fastjson 在不同构造器情况下的行为,并深入探讨了 `ParserConfig#getDeserializer` 方法的核心逻辑。此外,还介绍了 ASM 字节码技术的应用及其在反序列化过程中的角色。
94 10
|
3月前
|
存储 XML JSON
用示例说明序列化和反序列化
用示例说明序列化和反序列化
23 1
|
3月前
|
存储 Java 开发者
Java编程中的对象序列化与反序列化
【9月更文挑战第20天】在本文中,我们将探索Java编程中的一个核心概念——对象序列化与反序列化。通过简单易懂的语言和直观的代码示例,你将学会如何将对象状态保存为字节流,以及如何从字节流恢复对象状态。这不仅有助于理解Java中的I/O机制,还能提升你的数据持久化能力。准备好让你的Java技能更上一层楼了吗?让我们开始吧!