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

覺得我們的內容實用嗎?MyApollo 電子報讀者募集中!歡迎訂閱電子報!

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):

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

但是 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 ):

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

不過這僅是我們所做的粗淺比較,詳細的 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 的例外錯誤,如下圖:

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

不過 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 已經被正確序列化:

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

numpy#

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

import orjson, numpy


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

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

如果要開啟 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"))

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

這種情況下,可以設定 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)

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

更多相關做法可以參閱 製作 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

覺得內容實用嗎?

MyApollo 電子報讀者募集中!新文章與技術新知會直接寄到你的信箱。

訂閱電子報

對抗久坐職業傷害

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

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

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

馬上試用 PomodoRoll

贊助我們的創作

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

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

Buy me a coffee

輸入關鍵字開始搜尋