Python JSON 模組 - 走到跑,跑到飛的 orjson

Posted on  May 10, 2023  in  Python 模組/套件推薦  by  Amo Chen  ‐ 4 min read

orjson 是一套由 Rust 實作的 Python 套件,專門用以處理 JSON 相關的 encode 與 decode 的工作,效率不僅快(根據官方測試最快可以達到 json 模組的 40 到 50 倍效率)更天生能直接處理 Python 內建 json 模組所無法序列化(serialize)的 datetime, UUID, dataclass 等資料,不需額外編寫序列化的處理程式。

如果你想改善 Python API server 處理 JSON 的速度,以降低系統回應時間,又或者你有大量 JSON 相關的資料要處理,想有效減少處理時間的話,不妨試試 orjson 吧!

前言

Python 雖然有內建 json 模組供開發者處理 JSON 格式的資料,例如將資料輸出為 JSON 格式,或將 JSON 字串載入並轉為 Python 資料。

不過 Python 內建的 json 模組效率卻不夠好之外,使用上也容易因為遇到 dataclass, datetime, UUID 等無法序列化(serialize)的資料,而造成 TypeError , 因此在輸出 JSON 格式的字串時,都需要針對 set, datetime 或 UUID 等型別的資料先轉換成 int, float 或 str 等型態,才能正確輸出 JSON 格式的字串,詳細問題與做法可以參考 製作 JSON serializable 的類別 一文。

也許小型的應用(application)感覺不出差異,一旦應用長大到每秒要處理相當多的 JSON 資料時,勢必要使用更快、更方便的 JSON 模組以加速應用處理 JSON 的速度。

本文所介紹的 orjson 就是解決上述執行效率與 JSON 序列化問題的最佳解決方案——快又方便有效

本文環境

$ pip install orjson

內建 json 模組 vs. orjson

首先下列一段簡單的程式碼,就能讓內建的 json 模組出錯:

import json

from datetime import datetime


d = {
    'time': datetime.now(),
}


j = json.dumps(d)

執行結果如下,可以看到內建 json 模組無法處理 datatime 型態的資料,因此拋出 TypeError: Object of type datetime is not JSON serializable 的例外錯誤(exception):

但是 orjson 天生就能夠處理 datetime, date 等型態的資料,因此不會發生問題,例如:

import orjson


from datetime import datetime


d = {
    'time': datetime.now(),
}


j = orjson.dumps(d)
print(j)

上述執行結果如下,可以看到 orjson 能夠正常處理 datetime 型態的資料:

b'{"time":"2023-05-10T02:36:43.813289"}'

值得注意的是, orjson 所輸出的 JSON 格式是 bytes, 所以如果要轉為 Python string 需要呼叫 decode() 方法,例如:

orjson.dumps({'time': datetime.now()}).decode()

不過輸出 bytes 的好處是寫檔效率較好,可以將 open() 函式的 mode 參數設定為 binary mode, 例如 wb 直接將 bytes 寫入檔案(因為 Python 不需再額外呼叫 deocde 所以速度較快):

with open('output.jsonlines', 'wb') as file:
	file.write(orjson.dumps(d, option=orjson.OPT_APPEND_NEWLINE)

如果不想輸出 datetime 的 microseconds, 可以設定 orjson 的 option 參數為 orjson.OPT_OMIT_MICROSECONDS , 例如:

import orjson
from datetime import datetime


j = orjson.dumps(datetime.now(), option=orjson.OPT_OMIT_MICROSECONDS)
print(j)

上述執行結果如下,可以看到 orjson 只輸出到秒為止, microseconds 的部分被拿掉了:

b'"2023-05-10T05:51:31"'

orjson 也能正常處理含有時區(tzinfo)的 datetime 資料, 例如:

import orjson, datetime, zoneinfo


orjson.dumps(
    datetime.datetime(
        2023, 5, 1, 2, 3, 4, 5,
        tzinfo=zoneinfo.ZoneInfo('Asia/Taipei'),
    )
)

上述執行結果如下,可以看到結果中有 +08:00 時區的資訊:

b'"2022-12-01T02:03:04.000009+08:00"'

orjson 內建多種原本 Python json 模組無法處理的資料,例如:

此外,速度的部分我們也可以用 1 個簡單的 dict 型態的資料進行比較:

target = {
    "message": "Hello, JSON",
    "count": 10,
}

以下使用 %time 指令比較 json 與 orjson 的速度:

%time json.dumps(target)
%time orjson.dumps(target)

可以看到 orjson 至少快 json 模組至少 1 倍( 24 µs vs. 10 µs ):

不過這僅是我們所做的粗淺比較,詳細的 benchmark 可以參考 orjson 所提供的比較數據

dataclass

另一個 orjson 吸引人的功能特點是 orjson 支援處理 dataclass 的 JSON 序列化(serialization),這也是內建 json 模組所無法處理的,例如下列範例:

import json

from dataclasses import dataclass


@dataclass
class User(object):
  uid: int
  name: str
  role: str


user = User(1, "root", "admin")

json.dumps(user)

上述範例在試圖將 User dataclass 輸出為 JSON 時,出現 TypeError 的例外錯誤,如下圖:

不過 orjson 能夠正確處理 dataclass:

import orjson

from dataclasses import dataclass


@dataclass
class User(object):
  uid: int
  name: str
  role: str


user = User(1, "root", "admin")

orjson.dumps(user)

上述執行結果如下,可以看到 User dataclass 已經被正確序列化:

numpy

numpy 的 JSON 序列化也是 orjson 所主打的功能特點,不過 orjson 預設並沒有開啟 numpy 序列化的功能,如下列範例會出現 TypeError

import orjson, numpy


orjson.dumps(
  numpy.array([[1, 2, 3], [4, 5, 6]]),
)

如果要開啟 numpy 序列化功能,需要設定 orjson 的 option 參數 orjson.OPT_SERIALIZE_NUMPY , 例如:

import orjson, numpy


orjson.dumps(
  numpy.array([[1, 2, 3], [4, 5, 6]]),
  option=orjson.OPT_SERIALIZE_NUMPY,
)

orjson.loads() 載入 JSON 資料

如果想載入 JSON 字串,可以呼叫 orjson.loads(), 該函式支援傳入 string 與 bytes:

傳入 string:

orjson.loads('{"uid":1,"name":"root","role":"admin"}')

傳入 bytes:

orjson.loads(b'{"uid":1,"name":"root","role":"admin"}')

因此如果是從檔案中讀取出來,也不用額外轉成字串處理,可以直接用 mode='rb' 讀檔案:

with open('input.jsonlines', 'rb') as file:
    for line in file:
        orjson.loads(line)

default 參數

雖然 orjosn 已經支援多種資料型態的序列化,但也是有無法序列化的資料型態,例如 decimal 就無法被序列化為 JSON, 因此導致 TypeError 發生:

import orjson, decimal


orjson.dumps(decimal.Decimal("0.123"))

這種情況下,可以設定 default 參數,如果是 orjson 無法序列化的資料,將會傳給 default 函式進行處理,如下列範例中的 default 函式,該函式會判斷傳進來的資料是否為 decimal, 如果是則轉為字串,若不是則拋出 TypeError , 如果要處理更多不同的資料,擴充 default 函式即可:

import orjson, decimal


def default(obj):
    if isinstance(obj, decimal.Decimal):
        return str(obj)
    raise TypeError


orjson.dumps(decimal.Decimal("0.123"), default=default)

更多相關做法可以參閱 製作 JSON serializable 的類別 一文。

總結

orjson 憑藉著 Rust 實作的效能優勢以及支援各種常見的資料型態序列化,不僅能夠減少 Python 開發者的負擔,也能有效提升 JSON 相關應用執行速度。

值得一提的是 orjson 並不提供 load()dump() 2 個方法,如果你的 JSON 應用沒有用到 json.load()json.dump() 2 個函式(少個 s ),只要使用以下 import 方式就可以無痛轉為使用 orjson:

import orjson as json

如果你在尋找一種方法能夠簡單無痛提升 JSON 相關應用的效能,不仿考慮使用 orjson 吧,肯定能夠帶來一些幫助!

References

GitHub - ijl/orjson: Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!