CLOVER🍀

That was when it all began.

PythonでUUID バージョン7、ULIDを扱う

これはなにをしたくて書いたもの?

前にJavaでUUIDが扱えるライブラリーを調べてみました。

JavaでUUIDを扱えるライブラリーを調べる - CLOVER🍀

この時はUUID バージョン6〜8はドラフト段階だったのですが、2024年5月にRFC 9562として公開されました。

RFC 9562: Universally Unique IDentifiers (UUIDs)

今回はPythonでUUID v7を試してみたいと思います。またULIDも扱ってみましょう。

UUID バージョン6〜8

先にも書いたとおり、バージョン6〜8のUUIDはドラフト段階が続いていたのですが、RFC 9562として公開されています。

RFC 9562: Universally Unique IDentifiers (UUIDs)

これに伴いこちらのGitHubリポジトリーはアーカイブされ

GitHub - uuid6/uuid6-ietf-draft: Next Generation UUID Formats

こちらに移っています。

GitHub - ietf-wg-uuidrev/rfc4122bis: revision to RFC4122

この中で、比較的使うのではないかと思われるUUID バージョン7に焦点を当てます。

UUID バージョン7は、Unixエポックタイムスタンプから派生したフィールド持ち、バージョン1〜6よりエントロピーを改善したものです。
48ビットにミリ秒のタイムスタンプを割り当て、残りの74ビットを乱数で埋めています。

簡単に言うと、ソート可能なUUIDです。

ちなみにUUID バージョン6もソート可能なUUIDなのですが、構成自体はバージョン1と同じで元にするデータにMACアドレスが含まれています。

ULID

ULIDも挙げておきましょう。ULIDもソート可能でユニークなIDです。仕様は以下で公開されています。

GitHub - ulid/spec: The canonical spec for ulid

Javaでは以下のエントリーで扱ったことがあります。

JavaでULIDを使いたい(Sulky ULIDを使う) - CLOVER🍀

PythonでUUID バージョン7、ULIDを扱うには?

PythonでUUID バージョン7、ULIDを扱えるライブラリーを調べてみました。選択肢はいくつかあるようです。

ざっと見ただけでもこれくらいはありました。

UUID バージョン7。

GitHub - oittaa/uuid6-python: New time-based UUID formats which are suited for use as a database key

GitHub - stevesimmons/uuid7: UUID version 7, which are time-sortable (following the Peabody RFC4122 draft)

uuid6-pythonは名前こそUUID6ですが、UUID バージョン6〜8までを利用できます。

ちなみに、RFC 9562となったことでPythonの標準ライブラリーとしてのUUID バージョン7についてのPull Requestもあったりします。
そのうち標準で扱えるといいですね。

gh-89083: add support for UUID version 7 (RFC 9562) by picnixz · Pull Request #121119 · python/cpython · GitHub

Support UUIDv6, UUIDv7, and UUIDv8 from RFC 9562 · Issue #89083 · python/cpython · GitHub

ULID。

GitHub - mdomke/python-ulid: ULID implementation for Python

GitHub - bdraco/ulid-transform: Create and transform ULIDs

GitHub - ahawker/ulid: Universally Unique Lexicographically Sortable Identifier (ULID) in Python 3

python-ulidはWebサイトもあります。

Python ULID docs

さてどうしましょうということところですが、今回はUUID バージョン7はuuid6-python、ULIDはpython-ulidを使ってみることにしましょう。

環境

今回の環境はこちら。

$ python3 --version
Python 3.12.3


$ pip3 --version
pip 24.0 from /usr/lib/python3/dist-packages/pip (python 3.12)

uuid6-pythonでUUID バージョン7を扱う

まずはUUID バージョン7から。

$ pip3 install uuid6

確認はテストコードで行うことにします。型チェックもできるようにしておきます。

$ pip3 install pytest mypy

インストールしたライブラリーの一覧。

$ pip3 list
Package           Version
----------------- ---------
iniconfig         2.0.0
mypy              1.13.0
mypy-extensions   1.0.0
packaging         24.2
pip               24.0
pluggy            1.5.0
pytest            8.3.3
typing_extensions 4.12.2
uuid6             2024.7.10

比較のために、標準ライブラリーに含まれるUUID バージョン4も使っておきましょう。

uuid --- RFC 4122 に従った UUID オブジェクト — Python 3.12.7 ドキュメント

テストコードはこちら。

tests/test_uuid_v4.py

import re
import time
import uuid

def test_uuid_v4() -> None:
    generated_uuid = uuid.uuid4()

    assert re.match(r"^[^-]+-[^-]+-4[^-]+-[^-]+-[^-]+$", str(generated_uuid)) != None

    print(f"generated uuid v4 = {str(generated_uuid)}")

def test_generated_many_ids() -> None:
    generate_size = 10000000
    results = set()

    start = time.time()

    for i in range(generate_size):
        results.add(str(uuid.uuid4()))

    end  = time.time()

    assert len(results) == generate_size

    print(f"uuid v4 {generate_size} generated, elapsed time = {end - start} sec")

uuid#uuid4でUUID バージョン4を得ることができます。文字列にする時はstrを使います。

    generated_uuid = uuid.uuid4()

    print(f"generated uuid v4 = {str(generated_uuid)}")

printでの結果の例。

generated uuid v4 = a7ec59f6-0346-4b71-9443-e8d3813287ae

UUIDは、-で区切られた3ブロック目の先頭の文字でバージョンを見分けることができます。バージョン4なので4ですね。

    assert re.match(r"^[^-]+-[^-]+-4[^-]+-[^-]+-[^-]+$", str(generated_uuid)) != None

続いてuuid6-pythonを使ってみます。

tests/test_uuid_v7.py

import re
import time
import uuid6

def test_uuid_v7() -> None:
    generated_uuid = uuid6.uuid7()

    assert re.match(r"^[^-]+-[^-]+-7[^-]+-[^-]+-[^-]+$", str(generated_uuid)) != None

    print(f"generated uuid v7 = {str(generated_uuid)}")

def test_generated_many_ids() -> None:
    generate_size = 10000000
    results = set()

    start = time.time()

    for i in range(generate_size):
        results.add(str(uuid6.uuid7()))

    end  = time.time()

    assert len(results) == generate_size

    print(f"uuid v7 {generate_size} generated, elapsed time = {end - start} sec")

使い方はuuid6#uuid7を呼び出すだけです。あとは標準ライブラリーのUUID バージョン4と同じですね。

    generated_uuid = uuid6.uuid7()

バージョン7なので、3ブロック目の先頭の文字は7です。

    assert re.match(r"^[^-]+-[^-]+-7[^-]+-[^-]+-[^-]+$", str(generated_uuid)) != None

printでの結果の例。

generated uuid v7 = 0193810a-5b77-7fb6-9852-7313f39a37c1

簡単に行っているベンチマークの結果を含めたテスト結果の出力。

$ pytest --capture=no
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.12.3, pytest-8.3.3, pluggy-1.5.0
rootdir: /path/to/project
collected 4 items

tests/test_uuid_v4.py generated uuid v4 = 7a00b640-4ff0-4f61-9c60-1b855148519d
.uuid v4 10000000 generated, elapsed time = 40.72287321090698 sec
.
tests/test_uuid_v7.py generated uuid v7 = 01938111-039c-7bd5-ad00-4417204cd58a
.uuid v7 10000000 generated, elapsed time = 55.693116664886475 sec
.

======================================================================= 4 passed in 98.42s (0:01:38) =======================================================================

ちなみにuuid6-pythonでもベンチマークを載せていて、こちらでもUUID バージョン7はUUID バージョン4よりは遅くなっています。

uuid6 / Performance

1番速いのはUUID バージョン1ですね。

python-ulidでULIDを使う

次は、python-ulidでULIDを使ってみましょう。

$ pip3 install python-ulid


## Pydaticのサポートも追加したい場合
$ pip3 install python-ulid[pydantic]

確認はテストコードで行うことにします。型チェックもできるようにしておきます。

$ pip3 install pytest mypy

インストールしたライブラリーの一覧。

$ pip3 list
Package           Version
----------------- -------
iniconfig         2.0.0
mypy              1.13.0
mypy-extensions   1.0.0
packaging         24.2
pip               24.0
pluggy            1.5.0
pytest            8.3.3
python-ulid       3.0.0
typing_extensions 4.12.2

テストコードはこちら。

tests/test_ulid.py

import re
import time
from ulid import ULID

def test_ulid() -> None:
    generated_ulid = ULID()

    assert len(str(generated_ulid)) == 26

    print(f"generated ulid = {str(generated_ulid)}")

def test_generated_many_ids() -> None:
    generate_size = 10000000
    results = set()

    start = time.time()

    for i in range(generate_size):
        results.add(str(ULID()))

    end  = time.time()

    assert len(results) == generate_size

    print(f"ulid {generate_size} generated, elapsed time = {end - start} sec")

ULIDのコンストラクタでULIDを生成します。

    generated_ulid = ULID()

他にもタイムスタンプやdatetimeを元に生成する方法もありますが、省略。

ULIDは26文字で、文字列で扱うにはstrで変換するのはUUIDと同じですね。

    assert len(str(generated_ulid)) == 26

printでの出力例。

generated ulid = 01JE0J35QSK5JNA9ZH3VAQH12P

簡単なベンチマークを含めた実行例。

$ pytest --capture=no
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.12.3, pytest-8.3.3, pluggy-1.5.0
rootdir: /path/to/project
collected 2 items

tests/test_ulid.py generated ulid = 01JE0J765V9TS6ZHMP8ADECQAM
.ulid 10000000 generated, elapsed time = 69.83835077285767 sec
.

======================================================================= 2 passed in 70.93s (0:01:10) =======================================================================

こんなところでしょうか。

おわりに

PythonでUUID バージョン7、ULIDを扱えるライブラリーを調べてみました。

Pythonではどのようなライブラリーがあるのか知らなかったのと、RFC 9562になったことでPythonの標準ライブラリーでも追加された
UUIDバージョンの取り込みが進んでいそうなことを知れてよかったですね。