単位付き数値を扱うPythonモジュールを調べてみた

そういう趣旨の話がTwitter(現・X)に流れていたので、ちょっと気になって調べてみた。

というのも、数値+単位という構造を持つクラスを作るというのはすぐ思い付くが、演算を実行するたびにクラスのメソッドを呼ぶのはスマートではないな、と。

結論を書くと、例えば、

a = 3KiB
b = 4MiB
print(a + b)

というような書き方が出来るモジュールは見つからなかった。ひょっとすると存在するのかもしれないけれど、PyObjectの拡張だけではダメで、コードをパースするところから手を入れないと無理なのは確実なので、Pythonでは難しいだろうな、と推測。

他の言語、例えばF#ではUnits of Measureとして利用できるらしい。

Pint

Pintでは以下のように書ける。

import pint
ureg = pint.UnitRegistry()
a = 3 * ureg.KiB
b = 4 * ureg.MiB
a + b

<Quantity(4099.0, 'kibibyte')>

”KB”は受け付けないが、”kB”はOK。

import pint
ureg = pint.UnitRegistry()
a = 3 * ureg.kB
b = 4 * ureg.MiB
a + b

<Quantity(4197.304, 'kilobyte')>

bitmath

bitmathなら、こう。

import bitmath
a = bitmath.KiB(3)
b = bitmath.MiB(4)
a + b

KiB(4099.0)

bitmathもPint同様、”KB”は受け付けないが”kB”は受け付ける。

astropy

astropyは天文関連の物理に関する単位や演算をカバーしていて、バイナリも扱える。

from astropy import units
a = 3 * units.KiB
b = 4 * units.MiB
a + b

<Quantity 4099. Kibyte>

“KB”は受け付けないが、”kB”なら受け付ける点は、これまでと同じ。

numericalunits

numericalunitsというのもあるが、バイナリは実装されていない。ここで”kB”がエラーにならないのは、ボルツマン定数として解釈されているから。

from numericalunits import *
a = 3 * KiB

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'KiB' is not defined

a = 3 * kB
b = 4 * mB

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'mB' is not defined. Did you mean: 'kB'?

b = 4 * MB

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'MB' is not defined. Did you mean: 'kB'?

そもそもコードを読む限り、numericalunitsは実装が良く無さそうで(精度を担保できない書き方が散見される)、科学技術計算には使えないように見えるし、単位をグローバル変数として定義していて、名前空間が汚染されまくる悪夢が簡単に予想できる。

si-units

si-unitsは実装途中でやめてしまった雰囲気がある。

from si_units import *
a = 3 * KiB

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'KiB' is not defined. Did you mean: 'KB'?

a = 3 * KB
b = 4 * MiB

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'MiB' is not defined. Did you mean: 'min'?

b = 4 * MB

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'MB' is not defined. Did you mean: 'KB'?

binary

binaryは単位変換だけを提供するので、このブログエントリーの趣旨からはちょっと外れる。convert_units()はTupleを返してるだけだし、”KiB” / “MiB”はエラーになる。

from binary import BinaryUnits, convert_units
a = convert_units(3, BinaryUnits.KB, BinaryUnits.KB)
b = convert_units(4, BinaryUnits.MB, BinaryUnits.KB)
a + b

(3.0, 'KiB', 4096.0, 'KiB')

units-of-measure

units-of-measureもあるようだが、macOS + Python 3.11ではpipでインストール出来なかったので、パス。

まとめ

Pintかastropyを使っておけ、といったところか。

計算式を文字列として受け取ってパースする関数を書くぐらいなら、Pythonではないがbcalを外部呼び出しするのもアリな気がしてきた。