70_Python数据序列化JSON与Pickle

Python数据序列化完全指南:JSON与Pickle深度对比

文章目录

  • Python数据序列化完全指南:JSON与Pickle深度对比
    • 前言
    • [一、JSON序列化:`json` 模块](#一、JSON序列化:json 模块)
      • [1.1 基本序列化与反序列化](#1.1 基本序列化与反序列化)
      • [1.2 美化输出](#1.2 美化输出)
      • [1.3 文件读写](#1.3 文件读写)
      • [1.4 自定义JSON编解码](#1.4 自定义JSON编解码)
    • [二、Pickle序列化:`pickle` 模块](#二、Pickle序列化:pickle 模块)
      • [2.1 基本用法](#2.1 基本用法)
      • [2.2 文件读写与协议版本](#2.2 文件读写与协议版本)
      • [2.3 多对象序列化](#2.3 多对象序列化)
    • [三、JSON vs Pickle 全面对比](#三、JSON vs Pickle 全面对比)
      • [3.1 功能对比](#3.1 功能对比)
      • [3.2 性能对比](#3.2 性能对比)
      • [3.3 对比总结表](#3.3 对比总结表)
    • 四、实战案例
      • [4.1 配置文件管理(JSON)](#4.1 配置文件管理(JSON))
      • [4.2 计算结果缓存(Pickle)](#4.2 计算结果缓存(Pickle))
      • [4.3 通用数据导出工具](#4.3 通用数据导出工具)
    • 五、安全注意事项
    • 总结
    • [✅ 亮点总结](#✅ 亮点总结)
    • 适用场景
    • 扩展方向

前言

数据序列化是将内存中的Python对象转换为可存储或可传输格式的过程。无论是保存程序配置、缓存计算结果、在不同系统间交换数据,还是保存机器学习模型,序列化都是不可或缺的技术。Python最常用的两种序列化方案是JSON (跨语言、人类可读)和Pickle(Python专有、支持任意对象)。本文将深入对比两者的用法、优劣和适用场景。

一、JSON序列化:json 模块

JSON(JavaScript Object Notation)是最通用的数据交换格式,所有主流编程语言都原生支持。它的设计哲学是"简单至上"------只支持六种基本数据类型(字符串、数字、布尔、null、数组、对象),这既是它的优势(跨语言无障碍),也是它的局限(无法直接表示日期、自定义类等复杂类型)。

在学习JSON序列化时,务必掌握两组四个核心函数:dumps/loads(字符串 ↔ 对象)和 dump/load(文件 ↔ 对象)。一个实用的记忆技巧:带 s 的就是操作字符串(string),不带的就操作文件。

1.1 基本序列化与反序列化

python 复制代码
import json

# Python对象 → JSON字符串
data = {
    "name": "张三",
    "age": 28,
    "skills": ["Python", "JavaScript", "SQL"],
    "active": True,
    "score": None,
}

# dumps: 转为JSON字符串
json_str = json.dumps(data)
print(json_str)
# {"name": "\u5f20\u4e09", "age": 28, "skills": ["Python", "JavaScript", "SQL"], "active": true, "score": null}

# loads: JSON字符串 → Python对象
restored = json.loads(json_str)
print(restored)
print(f"类型: {type(restored)}, 姓名: {restored['name']}")

1.2 美化输出

python 复制代码
data = {
    "users": [
        {"id": 1, "name": "Alice", "email": "alice@example.com"},
        {"id": 2, "name": "Bob", "email": "bob@example.com"},
        {"id": 3, "name": "Charlie", "email": "charlie@example.com"},
    ],
    "total": 3,
    "page": 1,
}

# indent: 缩进美化
print(json.dumps(data, indent=2))

# sort_keys: 按键排序
print(json.dumps(data, indent=2, sort_keys=True))

# ensure_ascii=False: 保留中文不转义
print(json.dumps(data, indent=2, ensure_ascii=False))

1.3 文件读写

python 复制代码
import json

config = {
    "app_name": "MyPythonApp",
    "version": "1.0.0",
    "database": {
        "host": "localhost",
        "port": 5432,
        "name": "mydb",
    },
    "debug": True,
    "log_level": "INFO",
    "allowed_hosts": ["localhost", "127.0.0.1"],
}

# 写入JSON文件
with open("config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, indent=2, ensure_ascii=False)

# 从JSON文件读取
with open("config.json", "r", encoding="utf-8") as f:
    loaded_config = json.load(f)

print(f"应用名: {loaded_config['app_name']}")
print(f"数据库: {loaded_config['database']['host']}:{loaded_config['database']['port']}")

1.4 自定义JSON编解码

JSON原生不支持 datetimeDecimal 等Python特有类型,需要自定义编码器。这是实际项目中的常见需求------你经常需要将包含日期、Decimal金额、集合等Python特有类型的对象序列化为JSON。自定义编码器的核心是继承 json.JSONEncoder 并重写 default() 方法,在该方法中对不支持的类型进行处理并返回JSON可接受的值。

python 复制代码
import json
from datetime import datetime, date
from decimal import Decimal


class CustomEncoder(json.JSONEncoder):
    """自定义JSON编码器"""

    def default(self, obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        if isinstance(obj, Decimal):
            return float(obj)
        if isinstance(obj, set):
            return list(obj)
        if isinstance(obj, bytes):
            return obj.decode("utf-8")
        # 调用父类处理不支持的类型(会抛出TypeError)
        return super().default(obj)


# 使用自定义编码器
data = {
    "created_at": datetime.now(),
    "birthday": date(1995, 8, 22),
    "price": Decimal("99.99"),
    "tags": {"python", "tutorial"},
    "binary": b"hello",
}

json_str = json.dumps(data, cls=CustomEncoder, ensure_ascii=False, indent=2)
print(json_str)

自定义解码器:

python 复制代码
def custom_decoder(dct):
    """自定义JSON解码器(object_hook)"""
    # 处理ISO格式日期字符串
    for key, value in dct.items():
        if isinstance(value, str):
            # 尝试解析为datetime
            try:
                dct[key] = datetime.fromisoformat(value)
            except (ValueError, TypeError):
                pass
    return dct


# 使用时传入 object_hook
# restored = json.loads(json_str, object_hook=custom_decoder)

二、Pickle序列化:pickle 模块

Pickle是Python独有的序列化协议,能将几乎任意Python对象序列化为二进制格式。它的强大之处在于"几乎支持一切"------自定义类、嵌套对象、lambda函数(有限支持),通通可以序列化为字节流。但这种"无所不能"也带来了最大的风险:反序列化不受信任的Pickle数据可以执行任意代码,这在安全敏感的场景中是不可接受的。在生产环境中使用Pickle时一定要问自己三个问题:"这个文件从哪来的?有没有被篡改的可能?有没有更安全的替代方案?"

2.1 基本用法

python 复制代码
import pickle

# 复杂Python对象
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        return f"我是{self.name}, {self.age}岁"

person = Person("李四", 30)

# 序列化为bytes
pickled_data = pickle.dumps(person)
print(f"Pickle大小: {len(pickled_data)} bytes")

# 反序列化
restored_person = pickle.loads(pickled_data)
print(restored_person.greet())  # 我是李四, 30岁
print(type(restored_person))    # <class '__main__.Person'>

2.2 文件读写与协议版本

python 复制代码
import pickle

# 一个带嵌套对象的训练数据
model_data = {
    "algorithm": "random_forest",
    "parameters": {
        "n_estimators": 100,
        "max_depth": 10,
    },
    "feature_names": ["age", "income", "score"],
    "trained_at": "2024-01-15",
}

# 写入pickle文件(指定协议版本)
with open("model.pkl", "wb") as f:
    pickle.dump(model_data, f, protocol=pickle.HIGHEST_PROTOCOL)

# 读取pickle文件
with open("model.pkl", "rb") as f:
    loaded_model = pickle.load(f)

print(loaded_model["algorithm"])  # random_forest

2.3 多对象序列化

python 复制代码
import pickle

data = [
    {"id": 1, "value": "first"},
    {"id": 2, "value": "second"},
    {"id": 3, "value": "third"},
]

# 在一个文件中保存多个对象
with open("multi.pkl", "wb") as f:
    for item in data:
        pickle.dump(item, f)

# 按顺序读取
with open("multi.pkl", "rb") as f:
    results = []
    while True:
        try:
            obj = pickle.load(f)
            results.append(obj)
        except EOFError:
            break

print(f"读取了{len(results)}个对象")
for r in results:
    print(f"  {r}")

三、JSON vs Pickle 全面对比

在实战中选择JSON还是Pickle,是一个需要权衡多方面因素的决策。下面从功能支持、性能、安全性三个维度进行对比分析,帮助你做出正确的选择。

3.1 功能对比

python 复制代码
import json
import pickle
from datetime import datetime
from decimal import Decimal


class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email


# 测试数据
test_data = {
    "string": "Hello",
    "int": 42,
    "float": 3.14,
    "bool": True,
    "none": None,
    "list": [1, 2, 3],
    "dict": {"a": 1, "b": 2},
    "datetime": datetime.now(),
    "decimal": Decimal("10.5"),
    "custom_obj": User("张三", "zhang@test.com"),
}

print("=" * 50)
print("JSON vs Pickle 对比测试")
print("=" * 50)

for key, value in test_data.items():
    print(f"\n类型: {key} ({type(value).__name__})")

    # JSON 测试
    try:
        json_data = json.dumps(value, default=str)  # 用str兜底
        json_restored = json.loads(json_data)
        print(f"  JSON:   ✓ 序列化成功 (结果类型: {type(json_restored).__name__})")
    except (TypeError, ValueError) as e:
        print(f"  JSON:   ✗ {e}")

    # Pickle 测试
    try:
        pickled = pickle.dumps(value)
        pickle_restored = pickle.loads(pickled)
        print(f"  Pickle: ✓ 序列化成功 (结果类型: {type(pickle_restored).__name__})")
    except (TypeError, pickle.PicklingError) as e:
        print(f"  Pickle: ✗ {e}")

3.2 性能对比

python 复制代码
import json
import pickle
import time


def benchmark_serialization(data, iterations=10000):
    """对比JSON和Pickle的序列化性能"""
    # JSON
    start = time.perf_counter()
    for _ in range(iterations):
        json_str = json.dumps(data)
    json_dumps_time = time.perf_counter() - start

    json_bytes = json_str.encode()
    json_size = len(json_bytes)

    start = time.perf_counter()
    for _ in range(iterations):
        json.loads(json_str)
    json_loads_time = time.perf_counter() - start

    # Pickle
    start = time.perf_counter()
    for _ in range(iterations):
        pickled = pickle.dumps(data)
    pickle_dumps_time = time.perf_counter() - start

    pickle_size = len(pickled)

    start = time.perf_counter()
    for _ in range(iterations):
        pickle.loads(pickled)
    pickle_loads_time = time.perf_counter() - start

    print(f"{'指标':<15} {'JSON':<15} {'Pickle':<15}")
    print("-" * 45)
    print(f"{'序列化10000次':<15} {json_dumps_time:.3f}s{'':<8} {pickle_dumps_time:.3f}s")
    print(f"{'反序列化10000次':<15} {json_loads_time:.3f}s{'':<8} {pickle_loads_time:.3f}s")
    print(f"{'数据大小':<15} {json_size} bytes{'':<5} {pickle_size} bytes")


# 创建测试数据
test_data = {
    "users": [
        {"id": i, "name": f"user_{i}", "scores": list(range(100))}
        for i in range(100)
    ]
}

benchmark_serialization(test_data)

3.3 对比总结表

维度 JSON Pickle
可读性 人类可读 二进制,不可读
跨语言 几乎所有语言支持 仅Python
安全性 安全(只有数据) 不安全(可执行任意代码)
数据类型 基本类型(str/int/list/dict等) 几乎任何Python对象
自定义类 需要自定义编码器 原生支持
性能 较大数据时较慢 通常更快更小
适用场景 API、配置文件、数据交换 内部缓存、ML模型、临时存储

四、实战案例

以下实战案例展示了JSON和Pickle在实际项目中的典型应用模式------JSON用于需要人类可读和跨语言交换的场景(配置文件、API响应),Pickle用于Python内部的高效缓存和临时存储(计算结果缓存、机器学习模型)。理解这两种工具各自的最佳场景,是成为一个合格Python开发者的必修课。

4.1 配置文件管理(JSON)

python 复制代码
import json
import os
from pathlib import Path


class AppConfig:
    """应用程序配置管理器"""

    def __init__(self, config_path="app_config.json"):
        self.config_path = config_path
        self.data = self._load()

    def _load(self):
        if os.path.exists(self.config_path):
            with open(self.config_path, "r", encoding="utf-8") as f:
                return json.load(f)
        return self._default_config()

    def _default_config(self):
        return {
            "app_name": "MyApp",
            "version": "1.0",
            "window": {"width": 800, "height": 600},
            "theme": "light",
            "language": "zh-CN",
            "auto_save": True,
            "recent_files": [],
        }

    def get(self, key, default=None):
        return self.data.get(key, default)

    def set(self, key, value):
        self.data[key] = value

    def save(self):
        with open(self.config_path, "w", encoding="utf-8") as f:
            json.dump(self.data, f, indent=2, ensure_ascii=False)
        print(f"配置已保存到 {self.config_path}")


# 使用
config = AppConfig()
config.set("theme", "dark")
config.set("window", {"width": 1920, "height": 1080})
config.set("recent_files", ["project1.py", "project2.py"])
config.save()

4.2 计算结果缓存(Pickle)

python 复制代码
import pickle
import os
import time


def with_cache(cache_dir=".cache"):
    """缓存装饰器:将耗时计算的结果持久化"""

    def decorator(func):
        def wrapper(*args, **kwargs):
            os.makedirs(cache_dir, exist_ok=True)

            # 生成缓存key
            cache_key = f"{func.__name__}_{hash(str(args) + str(kwargs))}"
            cache_path = os.path.join(cache_dir, f"{cache_key}.pkl")

            # 命中缓存
            if os.path.exists(cache_path):
                with open(cache_path, "rb") as f:
                    print(f"[缓存命中] {func.__name__}")
                    return pickle.load(f)

            # 执行计算并缓存
            print(f"[计算中] {func.__name__}")
            result = func(*args, **kwargs)
            with open(cache_path, "wb") as f:
                pickle.dump(result, f)

            return result

        return wrapper

    return decorator


@with_cache(".cache")
def expensive_computation(n):
    """模拟耗时计算(如数据预处理)"""
    time.sleep(2)
    return [i ** 2 for i in range(n)]


# 第一次调用:计算并缓存
result1 = expensive_computation(100000)
print(f"结果长度: {len(result1)}")

# 第二次调用:命中缓存
result2 = expensive_computation(100000)
print(f"结果一致: {result1 == result2}")

4.3 通用数据导出工具

python 复制代码
import json
import pickle
from datetime import datetime


def export_data(data, filepath, format_type=None):
    """统一的序列化导出接口

    Args:
        data: 要导出的Python对象
        filepath: 输出文件路径
        format_type: 格式,根据扩展名自动判断(.json / .pkl)
    """
    if format_type is None:
        ext = filepath.rsplit(".", 1)[-1].lower()
        format_type = ext

    if format_type == "json":
        with open(filepath, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, ensure_ascii=False, default=str)
        print(f"JSON导出完成: {filepath}")

    elif format_type in ("pkl", "pickle"):
        with open(filepath, "wb") as f:
            pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)
        print(f"Pickle导出完成: {filepath}")

    else:
        raise ValueError(f"不支持的格式: {format_type}")


def import_data(filepath, format_type=None):
    """统一的序列化导入接口"""
    if format_type is None:
        ext = filepath.rsplit(".", 1)[-1].lower()
        format_type = ext

    if format_type == "json":
        with open(filepath, "r", encoding="utf-8") as f:
            return json.load(f)

    elif format_type in ("pkl", "pickle"):
        with open(filepath, "rb") as f:
            return pickle.load(f)

    else:
        raise ValueError(f"不支持的格式: {format_type}")


# 使用示例
data = {
    "timestamp": datetime.now().isoformat(),
    "users": [
        {"name": "Alice", "roles": ["admin", "editor"]},
        {"name": "Bob", "roles": ["viewer"]},
    ],
}

export_data(data, "export.json")
export_data(data, "export.pkl")

loaded_json = import_data("export.json")
loaded_pickle = import_data("export.pkl")

print(f"JSON加载: {loaded_json['users'][0]['name']}")
print(f"Pickle加载: {loaded_pickle['users'][0]['name']}")

五、安全注意事项

Pickle的安全警告:绝对不能从不受信任的来源加载Pickle数据! 这不是小事。Pickle在反序列化时会执行任意Python代码,如果攻击者构造一个恶意的 .pkl 文件,加载它的程序就可能被完全控制。这不像SQL注入还需要特定条件------Pickle的代码执行是其设计特性而非bug。

在实际开发中,遵循以下安全准则:

  1. 不要加载任何非你本人创建或来源不可信的Pickle数据------包括用户上传的文件、从网络下载的数据
  2. API接口中绝对不要使用Pickle------所有对外接口都应使用JSON或其他安全格式
  3. 临时缓存和内部通信场景下相对安全------因为这些数据在你的程序控制范围内
  4. 如果必须在不可信环境中使用序列化 ,考虑使用JSON + 自定义编码器,或者使用安全的序列化库如 h5py(用于机器学习模型)
python 复制代码
import pickle

# 危险示例:恶意Pickle可以在反序列化时执行任意代码
# 以下仅作演示,请勿在实际项目中加载不受信任的.pkl文件

# 安全的替代方案
def safe_load_json(filepath):
    """使用JSON作为安全的替代方案"""
    with open(filepath, "r", encoding="utf-8") as f:
        return json.load(f)

# 仅在以下情况使用Pickle:
# 1. 数据来自完全受信任的本地文件
# 2. 临时缓存/中间结果(不会有第三方注入风险)
# 3. 同一Python进程内通信(如multiprocessing)

总结

JSON和Pickle的选择策略不是"哪个更好",而是"哪个更适合当前场景"。JSON是通用交换格式的代表------安全、人类可读、跨语言支持,适合所有对外接口和配置文件;Pickle是Python专属的高效工具------灵活、性能好、支持任意对象,适合内部缓存和临时存储。

场景 推荐方案
Web API 数据交换 JSON
配置文件 JSON
跨语言/跨平台 JSON
数据需要人工审查/编辑 JSON
Python内部缓存/临时存储 Pickle
复杂Python对象(含自定义类) Pickle
机器学习模型保存 Pickle 或 joblib
不受信任来源的数据 JSON(仅使用loads不用于pickle)
  • JSON 是通用、安全、可读的数据交换格式,是绝大多数场景的首选
  • Pickle 是Python专属的强大序列化工具,方便但需谨慎使用
  • 选择规则:能用JSON就用JSON,有特殊需求再用Pickle

从面向对象到文件操作,从正则表达式到异步编程,再到数据序列化------这些核心技能构成了Python工程师的完整工具箱。持续实践、善用文档,你将能在实际项目中游刃有余地运用这些知识。

✅ 亮点总结

  • 详细对比 JSON 与 Pickle 的特性、安全性、跨语言支持,提供清晰的选择决策表
  • JSON 部分覆盖 dump/dumps/load/loads 四函数 + default/object_hook 自定义编解码
  • Pickle 部分深入 __getstate__/__setstate__ 魔术方法,实现自定义序列化行为
  • 安全警示:明确指出 Pickle 的反序列化风险,强调"不受信数据绝不 pickle.loads"

适用场景

  • Web 开发:前后端通过 JSON 格式交换数据,RESTful API 的请求体与响应体
  • 机器学习:用 Pickle 或 joblib 保存训练好的模型(sklearn、PyTorch 模型文件)
  • 配置管理:用 JSON 文件存储应用配置,支持人工编辑和版本控制

扩展方向

  • 学习 marshmallow / Pydantic 库,实现 JSON 数据的类型校验与序列化
  • 探索 msgpack / protobuf 等二进制序列化格式,在网络传输中获得更高效率
  • 了解 shelve 模块(基于 Pickle 的键值存储),适合小规模数据持久化