Pythonで浮動小数点数floatと16進数表現の文字列を相互に変換

Posted: | Tags: Python, 数値

Pythonで浮動小数点数floatと16進数表現の文字列を相互に変換する方法を説明する。

  1. 標準ライブラリstruct, binasciiモジュールを使う方法
  2. float型のメソッドhex()fromhex()を使う方法

の2つの方法があり、それぞれ変換できる16進数表現の形式が異なる。

Pythonのfloatは他のプログラミング言語ではdoubleなどと呼ばれることが多い倍精度浮動小数点数(64bit)だが、(1)の方法では32bitの16進数表現に変換する方法についても述べる。

ここでは、以下の内容について説明する。

  • 2つの16進数表現
    • シンプルな16進数表現
    • pを含む16進数表現
  • 浮動小数点数floatとシンプルな16進数表現文字列との相互変換
    • floatから16進数表現文字列
      • 2進数、8進数への変換
    • 16進数表現文字列からfloat
    • 単精度浮動小数点数(32bit)の場合
  • 浮動小数点数floatとpを含む16進数表現文字列との相互変換
    • floatから16進数表現文字列: hex()
    • 16進数表現文字列からfloat: fromhex()

Pythonにおけるfloatの範囲(最大値・最小値)については以下の記事を参照。

浮動小数点数floatではなく整数intを2進数、8進数、16進数の文字列にそれぞれ変換する方法は以下の記事を参照。

2つの16進数表現

シンプルな16進数表現

浮動小数点数は以下の3つのブロックに分かれる。

  • 符号ビット (sign)
  • 指数部 (exponent)
  • 仮数部 (fraction)

Pythonの浮動小数点型floatは倍精度浮動小数点数(64bit)。それぞれのビット数は標準規格であるIEEE 754で以下のように定められている。

IEEE 754 double

例えば10進数の42.195をこの形式の16進数に変換すると0x4045 18f5 c28f 5c29となる。

16進数から10進数への変換は、指数部が0x0000または非正規化数)、0x7ff(無限大またはNaN)の特殊な場合を除き、以下の数式で計算される。exponent bias1023(0x3ff)

$$ (-1)^{sign} \times 2^{exponent - exponent \, bias} \times 1.fraction $$

詳細は以下のページを参照。

Pythonでの変換方法は後述する。

とりあえず確認したい場合は以下のWebページが便利。各ビットの割当などの詳細も表示されるので理解しやすい。ボタンが4つあるが、上の2つが単精度(32bit)で下の2つが倍精度(64bit)の16進数表現との変換に対応している。

pを含む16進数表現

以下のような形式もある。

[sign] ['0x'] integer ['.' fraction] ['p' exponent]

sign は必須ではなく、 + と - のどちらかです。 integer と fraction は 16 進数の文字列で、 exponent は 10 進数で符号もつけられます。大文字・小文字は区別されず、最低でも 1 つの 16 進数文字を整数部もしくは小数部に含む必要があります。この制限は C99 規格のセクション 6.4.4.2 で規定されていて、 Java 1.5 以降でも使われています。
組み込み型 — Python 3.7.1rc1 ドキュメント

ドキュメントにもあるように、Cの%a書式や、JavaのDouble.toHexStringで取得できる形式と同じ。

例えば10進数の42.195をこの形式の16進数表現に変換すると0x1.518f5c28f5c29p+5となる。

Pythonでの変換方法は後述。

浮動小数点数floatとシンプルな16進数表現文字列との相互変換

floatから16進数表現文字列

floatをシンプルな16進数表現文字列に変換するには標準ライブラリのstructモジュールを使う。

floatの最大値を例とする。

import struct
import sys

f_max = sys.float_info.max

print(f_max)
# 1.7976931348623157e+308

struct.pack()floatをバイト列bytesに変換する。

print(struct.pack('>d', f_max))
# b'\x7f\xef\xff\xff\xff\xff\xff\xff'

print(type(struct.pack('>d', f_max)))
# <class 'bytes'>

第一引数の>dは書式文字列。

一文字目はバイトオーダーの指定で>はビッグエンディアンを表す。二文字目のddouble(8byte = 64bitの浮動小数点数)を表す。第二引数の値を64bitビッグエンディアンの浮動小数点数としてバイト列に変換するという意味になる。

リトルエンディアンは<<dとすると並びが逆になる。

print(struct.pack('<d', f_max))
# b'\xff\xff\xff\xff\xff\xff\xef\x7f'

書式文字列の詳細は公式ドキュメント参照。

これを一旦整数に戻す。バイト列から他のデータ型への変換はstruct.unpack()を使う。64bitの整数であるQを書式文字列とする。struct.pack()で指定した書式文字列とバイトオーダーを合わせる。

struct.unpack()は複数の値を変換する関数なので、一つだけ変換する場合もタプルで返される。変換した値は[0]で取り出せる。

print(struct.unpack('>Q', struct.pack('>d', f_max)))
# (9218868437227405311,)

print(type(struct.unpack('>Q', struct.pack('>d', f_max))))
# <class 'tuple'>

print(struct.unpack('>Q', struct.pack('>d', f_max))[0])
# 9218868437227405311

print(type(struct.unpack('>Q', struct.pack('>d', f_max))[0]))
# <class 'int'>

なお、書式文字列を64bitの浮動小数点数であるdとすると当然ながらもとの値に戻る。

print(struct.unpack('>d', struct.pack('>d', f_max))[0])
# 1.7976931348623157e+308

Qで整数に変換した値を16進数の文字列に変換するにはhex()を使う。

print(hex(struct.unpack('>Q', struct.pack('>d', f_max))[0]))
# 0x7fefffffffffffff

print(type(hex(struct.unpack('>Q', struct.pack('>d', f_max))[0])))
# <class 'str'>

まとめて関数化するとfloatから16進数の文字列への変換は以下のようになる。なお、Pythonのfloatは64bitの浮動小数点数でほかのプログラミング言語のdoubleに相当するので、Pythonにはdoubleという型はないが便宜上double_to_hexという関数名にしている。

def double_to_hex(f):
    return hex(struct.unpack('>Q', struct.pack('>d', f))[0])

floatの範囲を超える値を指定してもエラーにはならないので注意。

print(double_to_hex(f_max))
# 0x7fefffffffffffff

print(double_to_hex(42.195))
# 0x404518f5c28f5c29

print(double_to_hex(1e500))
# 0x7ff0000000000000

print(double_to_hex(1e-500))
# 0x0

2進数、8進数への変換

16進数ではなく2進数や8進数に変換したい場合は、上で求めた16進数の値をint()で整数に変換したあとでbin()またはoct()で2進数や8進数の文字列に変換すればいい。

print(int(double_to_hex(f_max), 16))
# 9218868437227405311

print(bin(int(double_to_hex(f_max), 16)))
# 0b111111111101111111111111111111111111111111111111111111111111111

print(oct(int(double_to_hex(f_max), 16)))
# 0o777577777777777777777

以下のような関数で直接変換することも可能。

def double_to_bin(f):
    return bin(struct.unpack('>Q', struct.pack('>d', f))[0])

def double_to_oct(f):
    return oct(struct.unpack('>Q', struct.pack('>d', f))[0])

print(double_to_bin(f_max))
# 0b111111111101111111111111111111111111111111111111111111111111111

print(double_to_oct(f_max))
# 0o777577777777777777777

bin(), oct()については以下の記事を参照。

16進数表現文字列からfloat

16進数表現文字列をfloatに変換するには、まず文字列をバイト列に変換する。binascii.unhexlify()を使う。

import struct
import binascii

f_max_s = '7fefffffffffffff'

print(binascii.unhexlify(f_max_s))
# b'\x7f\xef\xff\xff\xff\xff\xff\xff'

print(type(binascii.unhexlify(f_max_s)))
# <class 'bytes'>

上で説明したstruct.pack()の書式文字列を>dとしてバイト列からfloatに変換する(元の文字列のバイトオーダーがリトルエンディアンの場合は<d)。

print(struct.unpack('>d', binascii.unhexlify(f_max_s)))
# (1.7976931348623157e+308,)

print(struct.unpack('>d', binascii.unhexlify(f_max_s))[0])
# 1.7976931348623157e+308

print(type(struct.unpack('>d', binascii.unhexlify(f_max_s))[0]))
# <class 'float'>

これを関数化すると以下のようになる。ここでも、Pythonにはdoubleという型はないが便宜上hex_tou_doubleという関数名にしている。

def hex_to_double(s):
    if s.startswith('0x'):
        s = s[2:]
    s = s.replace(' ', '')
    return struct.unpack('>d', binascii.unhexlify(s))[0]

0xがついている場合や空白で区切られた場合にも対応するようにしている。

print(hex_to_double('7fefffffffffffff'))
# 1.7976931348623157e+308

print(hex_to_double('0x7fefffffffffffff'))
# 1.7976931348623157e+308

print(hex_to_double('0x7fef ffff ffff ffff'))
# 1.7976931348623157e+308

print(hex_to_double('0x4045 18f5 c28f 5c29'))
# 42.195

無限大やNaN、非正規化数にも対応する。

print(hex_to_double('7ff0000000000000'))
# inf

print(hex_to_double('7ff0000000000001'))
# nan

print(hex_to_double('0000000000000001'))
# 5e-324

文字数が多かったり少なかったりするとエラーとなる。

# print(hex_to_double('ffff ffff ffff ffff ff'))
# error: unpack requires a buffer of 8 bytes

# print(hex_to_double('ffff ffff ffff ff'))
# error: unpack requires a buffer of 8 bytes

単精度浮動小数点数(32bit)の場合

Pythonのfloatは倍精度浮動小数点数(64bit)であるが、これを単精度浮動小数点数(32bit)の文字列を相互に変換したい場合は、上述の関数の書式文字列を変更すればOK。単精度浮動小数点数(32bit)はf、32bitの整数はIとする。

floatから単精度の16進数文字列。

def float_to_hex(f):
    return hex(struct.unpack('>I', struct.pack('>f', f))[0])

print(float_to_hex(42.195))
# 0x4228c7ae

単精度の16進数文字列からfloat

def hex_to_float(s):
    if s.startswith('0x'):
        s = s[2:]
    s = s.replace(' ', '')
    return struct.unpack('>f', binascii.unhexlify(s))[0]

print(hex_to_float('0x4228c7ae'))
# 42.19499969482422

とりあえず確認したいだけであれば上で紹介したページが便利。

浮動小数点数floatとpを含む16進数表現文字列との相互変換

floatから16進数表現文字列: hex()

float型のメソッドhex()floatをpを含む16進数表現の文字列に変換できる。

f = 256.0

print(f.hex())
# 0x1.0000000000000p+8

print(type(f.hex()))
# <class 'str'>

.が続いて一見分かりにくいが、以下のようにも書ける。

print(256.0.hex())
# 0x1.0000000000000p+8

print(0.5.hex())
# 0x1.0000000000000p-1

print(42.195.hex())
# 0x1.518f5c28f5c29p+5

hex()floatのメソッドなので、整数型からは呼べない。例えば256ではなく256.0のようにfloat型として記述する必要がある。

i = 256

# print(i.hex())
# AttributeError: 'int' object has no attribute 'hex'

16進数表現文字列からfloat: fromhex()

float型のクラスメソッドfromhex()でpを含む16進数表現の文字列をfloatに変換できる。

s = '0x1.0000000000000p+8'

print(float.fromhex(s))
# 256.0

print(type(float.fromhex(s)))
# <class 'float'>

0x.<fraction>, p<exponent>の部分は省略可能。ただし、例えば0xが無いと10進数と16進数で混乱してしまうので、あまり省略しすぎないほうがいい。

print(float.fromhex('0x1p+8'))
# 256.0

print(float.fromhex('1p+8'))
# 256.0

print(float.fromhex('0x100'))
# 256.0

print(float.fromhex('100'))
# 256.0

上の例のように、同じfloatに変換される文字列は一意ではない。

参考までに各値と具体的な計算を示す。exponentは10進数表記なので注意。

print(float.fromhex('0xf2.f8p-10'))
# 0.237274169921875

print((15 * 16**1 + 2 * 16**0 + 15 * 16**-1 + 8 * 16**-2) * 2**-10)
# 0.237274169921875

floatの最大値を変換すると以下の通り。

import sys

f_max = sys.float_info.max

print(f_max)
# 1.7976931348623157e+308

print(f_max.hex())
# 0x1.fffffffffffffp+1023

print(float.fromhex('0x1.fffffffffffffp+1023'))
# 1.7976931348623157e+308

これを超える値を示す文字列を指定するとオーバーフローする。

# print(float.fromhex('0x1.0000000000000p+1024'))
# OverflowError: hexadecimal value too large to represent as a float

# print(float.fromhex('0x2.0000000000000p+1023'))
# OverflowError: hexadecimal value too large to represent as a float

floatの正の最小の正規化数は以下の通り。

f_min = sys.float_info.min

print(f_min)
# 2.2250738585072014e-308

print(f_min.hex())
# 0x1.0000000000000p-1022

print(float.fromhex('0x1.0000000000000p-1022'))
# 2.2250738585072014e-308

正の最小の非正規化数(指数部が0x000)は以下のように表せる。

print(float.fromhex('0x0.0000000000001p-1022'))
# 5e-324

print(format(float.fromhex('0x0.0000000000001p-1022'), '.17'))
# 4.9406564584124654e-324

更に小さい値を示す文字列を指定した場合はエラーにはならず0.0になる。

print(float.fromhex('0x0.0000000000001p-1023'))
# 0.0

関連カテゴリー

関連記事