You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python随对象一同序列化类/函数定义的潜在缺陷

序列化前将类/函数重声明到__main__强制随对象存储的潜在负面影响

问题背景

pickle、dill等Python序列化库存在已知设计限制:非__main__模块下定义的类、函数,序列化时不会存储其定义代码,反序列化时要求对应定义必须存在于和序列化时完全一致的模块导入路径下。这一限制要求使用者必须将相关类/函数单独封装为做了版本管控的依赖包,预先部署到所有反序列化环境,提升了部署成本、限制了使用灵活性。

部分开发者会采用名为"mainify"的变通方案:序列化前在__main__模块中重新声明目标对象对应的类/函数定义,让序列化库可以捕获到定义并随对象一同存储,典型实现代码如下:

import inspect
import types

def mainify(obj):
   
    if obj.__module__ != '__main__':                                                
        
        import __main__       
        is_func = True if isinstance(obj, types.FunctionType) else False                                                            
                                
        # Get source code and compile
        source = inspect.getsource(obj if is_func else obj.__class__)
        compiled = compile(source, '<string>', 'exec')                    

        # "Declare" in __main__ and keep track which key
        # of __main__ dict is new 
        pre = list(__main__.__dict__.keys()) 
        exec(compiled, __main__.__dict__)
        post = list(__main__.__dict__.keys())                        
        new_in_main = list(set(post) - set(pre))[0]
        
        # for function return mainified version, else assign new
        # class to obj and return object
        if is_func:
            obj = __main__.__dict__[new_in_main]            
        else:            
            obj.__class__ = __main__.__dict__[new_in_main]
                
    return obj

该方案在个人机器学习工作流中通常可以正常运行,但属于非官方的hack实现,除已知的"无法在不重新序列化的前提下直接修改序列化包内的定义修复bug"之外,还存在以下可预见的风险:

具体风险

  • 源码获取环节存在硬失败可能:实现依赖inspect.getsource拉取类/函数的源码,该接口要求定义对应的源码文件在运行时可访问、且文件内容和内存中加载的定义完全一致。如果类/函数是在REPL、Jupyter Notebook等交互式环境动态生成、被PyInstaller等工具打包进冻结二进制、或者运行时源码文件被修改/删除/移动,调用mainify会直接抛出异常,序列化流程完全中断。
  • 类身份一致性被破坏:__main__下重声明生成的类和原模块下的类是完全独立的两个对象,手动将实例的__class__指向新类后,所有isinstance(实例, 原模块类)的判断都会返回False,依赖类身份做注册、匹配的逻辑(比如工厂类注册表、框架插件匹配、自定义序列化钩子)会全部失效。如果类使用了元类、类装饰器,重声明时这些逻辑依赖原模块上下文初始化,还可能出现不可预期的类属性异常。
  • 闭包与模块全局依赖丢失:inspect.getsource只会抓取类/函数本身的源码,不会捕获函数闭包引用的外部变量、类定义时引用的原模块全局对象。如果原函数/类依赖所在模块的其他工具函数、常量、初始化后的第三方客户端对象,重声明到__main__时这些依赖不存在,要么执行exec编译阶段直接报错,要么反序列化后调用方法时抛出NameError。
  • __main__命名空间污染与冲突:实现没有做任何去重、重名校验:同一个类多次调用mainify会在__main__中生成多个互不兼容的重声明版本;如果重声明的类/函数名和__main__中已有的变量名冲突,通过集合差集取第一个新增key的逻辑会直接混乱,可能错误覆盖__main__的已有变量,或者拿到错误的对象引用。
  • 跨Python版本兼容性彻底丧失:内嵌到序列化文件中的类/函数定义和序列化时使用的Python版本强绑定,不同Python小版本之间的字节码规则、标准库API、内置对象结构都可能存在差异。比如Python 3.8环境下序列化的内嵌类定义,拿到Python 3.11环境反序列化时,很可能因为依赖了已变更的内置API直接报错——相比原有"要求导入路径一致"的模式,这种内嵌定义的方式反而彻底失去了通过升级依赖包适配新版本的能力。
  • 继承链依赖未被解决:如果目标类继承自其他非__main__模块的类,mainify只会重声明子类本身,父类的引用依然指向原模块路径,反序列化时依然要求父类存在于原有导入路径下,并没有真正解决路径依赖问题,很容易出现"以为已经把所有定义打包进去,实际反序列化还是报导入错误"的隐蔽问题。
  • 安全边界被打破:pickle/dill反序列化本身存在任意代码执行风险,但原有模式下反序列化加载的是环境中预先部署的可信代码;采用mainify方案后,序列化文件中直接内嵌了可执行的类/函数逻辑,一旦序列化文件被篡改,反序列化时会无感知执行恶意代码,没有任何额外校验防护的空间。
  • 序列化体积不必要膨胀:如果同一个类的多个实例被分别mainify后序列化,每个序列化文件都会存储一份重复的类定义,批量存储大量对象时,冗余的定义代码会让序列化文件体积远大于正常模式,带来额外的存储和传输开销。

内容的提问来源于stack exchange,提问作者Jonas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 14:15:53