UP | HOME

使用 pickle 序列化 Python 对象

目录

1 前言

很久以前就接触过和 pickle 相关的内容了,但是因为存在 json 的原因,就没有专门去了解学习相关的内容。

而现在,因为和 缓存 相关的原因,使得我来了解学习一下 pickle 相关的内容。

2 pickle 的简单使用

pickle 模块的使用很简单,接口和 json 差不多,如果有 json 模块的使用经验,那么 pickle 模块的上手无疑会简单很多。

import pickle

class Example(object):
    def __init__(self, value):
        self.value = value

with open('data.dat', 'wb') as f:
    pickle.dump(Example(10), f)

上面这段代码将一个 Python 对象序列化为一个字节流,并保存到文件 data.dat. 输出文件中的内容为:

b'\x80\x03c__main__\nExample\nq\x00)\x81q\x01}q\x02X\x05\x00\x00\x00valueq\x03K\nsb.'

与之对应的,可以通过 pickle.load 函数从文件中读取字节流并反序列化还原为 Python 对象:

with open('data.dat', 'rb') as f:
    obj = pickle.load(f)
    print('obj.value: ', obj.value)

输出结果为:

obj.value: 10

另外,可以通过函数 pickle.dumps 可以将对象序列化为一个 bytes 对象,通过 pickle.loads 将一个 bytes 对象反序列化还原为 Python 对象:

byte_data = pickle.dumps(Example(233))
print('byte: ', byte_data)

obj = pickle.loads(byte_data)
print('obj.value: ', obj.value)

输出结果如下:

byte:  b'\x80\x03c__main__\nExample\nq\x00)\x81q\x01}q\x02X\x05\x00\x00\x00valueq\x03K\xe9sb.'
obj.value:  233

3 可序列化的对象

Python 中,绝大多数对象都可以 直接pickle 完成序列化,包括自定义的类,具体的规则如下:

  • None, TrueFalse
  • int, floatcomplex
  • str, bytesbytearrays
  • 只包含可序列化对象的 list, tuple, setdict
  • 在模块的顶层定义的函数,不包括 lambda 表达式
  • 在模块的顶层定义的内置函数
  • 在模块的顶层定义的类

遵循以上规则的对象是可以直接序列化的,需要注意的是,对于 类实例, 还存在一些特殊的规则。

和前面的代码一样, 类实例 对象一般不需要额外的代码就可以完成序列化,其序列化行为类似如下代码:

def dump(obj):
    return (obj.__class__, obj.__dict__)

def load(cls, attributes):
    obj = cls.__new__(cls)
    obj.__dict__.update(attributes)
    return obj

可以看到,序列化 类实例 时会获取该实例的 属性字典, 反序列化时直接更新 实例属性 而不是调用 __init__ 方法。

这一行为对于部分对象来说是不合适的,比如说:

import time
import threading

class Countdown(object):
    def __init__(self, n):
        self.n = n
        self.thr = threading.Thread(target=self.run)
        self.thr.daemon = True
        self.thr.start()

    def run(self):
        while self.n > 0:
            print('T-minus', self.n)
            self.n -= 1
            time.sleep(5)

对于 Countdown 的实例,如果只是恢复自身的属性值是不够的,因为其状态还依赖于 外部系统状态. 对于这些对象,可以通过实现 __getstate____setstate__ 方法来定义它们的序列化行为。

class Countdown:
    def __init__(self, n):
        self.n = n
        self.thr = threading.Thread(target=self.run)
        self.thr.daemon = True
        self.thr.start()

    def run(self):
        while self.n > 0:
            print('T-minus', self.n)
            self.n -= 1
            time.sleep(5)

    def __getstate__(self):
        return self.n

    def __setstate__(self, n):
        self.__init__(n)

序列化时会调用 __getstate__ 方法,反序列化时会调用 __setstate__ 方法。

4 pickle 和 json

大多数时候,序列化一个 Python 对象我会选择 json 模块来完成,因为 json 格式更通用,更易读。

但是, pickle 也具有其优势:

  • 能够直接序列化大多数 Python 对象,方便
  • 能够以更快的速度序列化 Python 对象,快速

这两点中,更吸引我的是 pickle方便, 在不要求通用性和可读性的情况下, pickle 是一个很好的选择。

比如说,实现一个简单的内存缓存:

上面的链接是 werkzeugSimpleCache 的源码链接,它的实现就使用了 pickle, 部分代码如下:

class SimpleCache(BaseCache):
    def __init__(self, threshold=500, default_timeout=300):
        BaseCache.__init__(self, default_timeout)
        self._cache = {}
        self.clear = self._cache.clear
        self._threshold = threshold

    def get(self, key):
        try:
            expires, value = self._cache[key]
            if expires == 0 or expires > time():
                return pickle.loads(value)
        except (KeyError, pickle.PickleError):
            return None

    def set(self, key, value, timeout=None):
        expires = self._normalize_timeout(timeout)
        self._prune()
        self._cache[key] = (expires, pickle.dumps(value,
                                                  pickle.HIGHEST_PROTOCOL))
        return True

对于速度,可以用简单的代码来测试一下:

In [1]: import json, pickle

In [2]: obj = list({val: -val} for val in range(1000000))

In [3]: %timeit json_dump = json.dumps(obj)
976 ms ± 43 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [4]: %timeit pickle_dump = pickle.dumps(obj)
436 ms ± 7.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

很明显, pickle 的速度要快不少,所以,对于一些 Python 内部的使用场景, pickle 是一个不错的选择。

5 pickle 的使用技巧

  • pickle 可以很智能的处理多个对象:

    >>> import pickle
    >>> f = open('somedata', 'wb')
    >>> pickle.dump([1, 2, 3, 4], f)
    >>> pickle.dump('hello', f)
    >>> pickle.dump({'Apple', 'Pear', 'Banana'}, f)
    >>> f.close()
    >>> f = open('somedata', 'rb')
    >>> pickle.load(f)
    [1, 2, 3, 4]
    >>> pickle.load(f)
    'hello'
    >>> pickle.load(f)
    {'Apple', 'Pear', 'Banana'}
    
  • 函数 pickle.dumppickle.dumps 可以通过参数 protocol 来指定序列化方式,该参数的取值范围为 [0, pickle.HIGHEST_PROTOCOL], 默认值为 pickle.DEFAULT_PROTOCOL.

    效果如下:

    >>> import pickle
    >>> obj = list(range(1000000))
    >>> with open('dump_min.dat', 'wb') as f:
    ...     pickle.dump(obj, f, 0)
    ...
    >>> with open('dump_max.dat', 'wb') as f:
    ...     pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL)
    ...
    
    $  du -h dump_min.dat
    9.5M    dump_min.dat
    
    $  du -h dump_max.dat
    4.7M    dump_max.dat
    

6 结语

测试的时候想创建一个够大的列表,直接顺手按了一串零,结果,电脑当场死机……

重启后算了一下,至少需要 37G 的内存来保存数据才够用 @_@

垃圾电脑 QAQ

7 参考链接

版权声明:本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可