9.6 使用pickle进行转储和加载

    pickle模块是Python内部的一种格式,用来完成对象的持久化。

    Python标准库中是这样描述pickle的:

    pickle模块可以将一个复杂的对象转换为一个字节数组并且使用相同的内部结构将字节流转换为一个对象。将这些字节流写入文件或许是最常见的场景,但也可能输出到网络进行传输或是数据库。

    pickle所关注的只有Python。它并不是一种用于数据转换的格式,比如JSON、YAML、CSV或者是XML,都可用于其他语言所编写的应用。

    pickle模块用很多方式完成了与Python的轻量集成。例如,一个类的reduce()reduce_ex()方法用于提供对pickle处理过程的支持。

    我们可以使用以下的方式对博客执行pickle处理。

    import pickle
    with open("travel_blog.p","wb") as target:
      pickle.dump( travel, target )

    以上代码完成了整个travel对象到指定文件的导出。文件的写入使用了纯字节,因此open()函数使用了"wb"模式。

    我们可以使用如下方式将字节反序列化为对象。

    with open("travel_blog.p","rb") as source:
      copy= pickle.load( source )

    由于pickle数据是使用字节写入的,文件必须以"rb"模式打开。pickle对象将被正确地绑定于适当的类定义。底层的字节流不是用来直接读的。必须经过适当调整才具备可读性,但设计它的初衷不是为了像YAML一样可读。

    9.6.1 针对可靠的pickle处理进行类设计

    实际上,一个类的init()方法并不是用来unpickle一个对象的。init()方法可通过使用new()来绕过执行并直接将Pickle的值写入对象的dict中。当类定义的init()中包含了一些处理逻辑时,这一点就很重要。比如,当init()打开了外部文件,创建了一个GUI接口中的几个部分,或对数据库执行了一些修改,那么在unpickling期间这些操作就不会被执行。

    当在init()处理过程中执行了一个新实例变量的计算逻辑时,不会有真正的问题。例如,在21点中,当Hand被创建时,Hand对象会计算Card实例的总数。传统的pickle处理过程会保存这个经过计算产生的实例变量。在对象被unpickle之前,它不会被重新计算,只是将之前计算的值unpickle。

    如果一个类依赖于init()的处理逻辑,为了确保初始化逻辑被正确执行,它必须使用特定的顺序来执行,需要做以下两件事。

    • 避免在init()中提前完成初始化。相反,使用一次性初始化过程。例如,如果需要操作多个文件,当被需要时才执行。
    • 定义getstate()和setstate()方法,它们可被pickle用于保存和还原状态。在传统的Python代码中,接下来会使用setstate()方法调用init()所调用的相同方法来执行一次性的初始化。

    在以下例子中,初始化的Card实例被Hand对象所加载,随后在init()方法中被写入日志文件用于审计。这里是一个Hand的实现版本,在执行unpickling时,它未能正常地工作。

    class Handx:
      def init( self, dealercard, *cards ):
        self.dealer_card= dealer_card
        self.cards= list(cards)
        
    for c in self.cards:
          audit_log.info( "Initial %s", c )
      def append( self, card ):
        self.cards.append( card )
        
    audit_log.info( "Hit %s", card )
      def __str
    ( self ):
        cards= ", ".join( map(str,self.cards) )
        return "{self.dealer_card} | {cards}".format( self=self,
    cards=cards )

    有两个记录日志的地方:init()append()。在对象的初始化和使用unpickling来重建构建对象的两个过程中,init()的行为是不一致的。如下的日志可以说明这一点。

    import logging,sys
    audit_log= logging.getLogger( "audit" )
    logging.basicConfig(stream=sys.stderr, level=logging.INFO)

    以上实现创建了日志并确保审计信息的日志级别是恰当的。以下脚本简单地实现了Hand对象的创建,pickle和unpickle。

    h = Hand_x( FaceCard('K',''), AceCard('A','♣'), Card('9','♥') )
    data = pickle.dumps( h )
    h2 = pickle.loads( data )

    当执行这段代码时,可以看到在unpickling Hand对象时,在init()的处理过程中,日志记录并没有被写入。为了适当地通过记录日志达到审计目的并用于unpickling,可以放一些类级别的日志。例如,可以通过扩展getattribute(),当类中的任何特性被访问时记录一条初始化日志。这会导致有状态的日志并且当一个hand对象每次被操作都会执行一次if语句。一种更好的方案是,对状态的保存进行追踪并使用pickle来完成状态的恢复。

    class Hand2:
      def init( self, dealercard, *cards ):
        self.dealercard= dealercard
        self.cards= list(cards)
        for c in self.cards:
          auditlog.info( "Initial %s", c )
      def append( self, card ):
        self.cards.append( card )
        auditlog.info( "Hit %s", card )
      def str( self ):
        cards= ", ".join( map(str,self.cards) )
        return "{self.dealercard} | {cards}".format( self=self,
    cards=cards )
      def getstate( self ):
        return self.__dict

      def __setstate
    ( self, state ):
        self.__dict
    .update(state)
        for c in self.cards:
          audit_log.info( "Initial (unpickle) %s", c )

    在picking时,会调用getstate()方法来获得对象的当前状态。这个方法可以返回任何信息。在对象包含内部缓存的情况下,例如,为了节省时间和空间,缓存可能没有被pickle。这种实现直接重用了内部dict的实现。

    当unpickling时,setstate()方法用于重置对象值。它会将状态合并保存到内部的dict中并适当地记录一些日志。

    9.6.2 安全性和全局性问题

    unpickling的过程中,在pickle流中的一个全局名称可能会导致一段自由代码的执行。大致上,全局名称是类名或函数名。然而,也可能在一个模块的函数中包含一个全局名称,例如os或者是subprocess。对于没有严格的SSL控制的网络环境,当传输pickled对象时,应用程序可能会遭到攻击,本地文件完全不用担心。

    为了阻止自由代码的执行,必须对pickle.Unpickler类进行扩展。我们会使用更安全的方式来重写find_class()方法。必须考虑到以下几点unpickling的问题。

    • 必须阻止内置的exec()和eval()函数的使用。
    • 必须阻止可能会导致不安全的模块和包的使用。例如,sys和os应当被禁用。
    • 允许应用程序模块的使用。

    以下是加入一些限制的一个示例。

    import builtins
    class RestrictedUnpickler(pickle.Unpickler):
      def findclass(self, module, name):
        if module == "builtins":
          if name not in ("exec", "eval"):
            return getattr(builtins, name)
        elif module == "_main
    ":
          return globals()[name]
        # elif module in any of our application modules…
        raise pickle.UnpicklingError(
        "global '{module}.{name}' is forbidden".format(module=module,
    name=name))

    这个版本的Unpickler类可以帮助我们避免大量潜在的问题,都是由于pickle流被篡改所导致的,它允许使用除exec()eval()外的任何内置函数。对于自定义类,只允许在main中使用。其他使用情况则会抛出异常。