Python: 小数点以下を含む数字を二進法で表示する

Python

前回のエントリ

を作成中に得た、Pythonで小数点以下を二進法に変換する方法をまとめておく。

Pythonはデフォルトでは整数の二進法への変換にのみ対応

Pythonはそのままでは小数点を含む数の二進数への変換は対応していない。

下記の通り、bin(16)で16を二進数で表記はできる(= 10000)が、bin(16.5)とするとエラーが出る。

>>> bin(16)
'0b10000'
>>> bin(16.5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'float' object cannot be interpreted as an integer

エラーメッセージにある通り、float(小数点以下を含む数)のオブジェクト(要は16.5)はinteger(整数)とは解釈できない、と言う事で、bin()を使用して二進法に変換するにはinter(整数)の必要があるとわかる。

これは、Pythonの公式チュートリアルの15. 浮動小数点演算、その問題と制限でも

「残念なことに、ほとんどの小数は 2 進法の分数として正確に表わすことができません。その結果、一般に、入力した 10 進の浮動小数点数は、 2 進法の浮動小数点数で近似された後、実際にマシンに記憶されます。」

とある通り、2進法で小数点以下がある数字を表示するには多くの場合割り切れず、10進法で1/3 = 0.33333….と循環小数になるように、計算が延々と続く循環小数になる事が関係しているのではないだろうか。

ではどうすれば変換できるのか?
Pythonライブラリのmpmathを使えば比較的に楽に行える。

mpmathとは

mpmathとは、実数および複素数の浮動小数点演算を任意の精度で行うための、フリー (BSD ライセンス) の Python ライブラリ。

mpmath is a free (BSD licensed) Python library for real and complex floating-point arithmetic with arbitrary precision. It has been developed by Fredrik Johansson since 2007, with help from many contributors. (HPより)

標準ライブラリには含まれていないので、使用するためにはまずインストールが必要。

% pip3 install mpmath
DEPRECATION: Configuring installation scheme with distutils config files is deprecated and will no longer work in the near future. If you are using a Homebrew or Linuxbrew Python, please see discussion at https://github.com/Homebrew/homebrew-core/issues/76621
Collecting mpmath
  Downloading mpmath-1.2.1-py3-none-any.whl (532 kB)
     |████████████████████████████████| 532 kB 3.4 MB/s            
Installing collected packages: mpmath

インストールしたら、Python3を立ち上げて下記の通りimportして使用。
from mpmath import * でmpmathの全てをインポート。mpmathの全てをその名前で使用が可能。

import * とせずに、例えば、import mpmathとした場合、mpmathが持つmp functionを使用する為にはmpmath.mpとして実行する必要がある。しかし、import * で全てをインポートしておけば”mp”で使用できる。

>>> from mpmath import *

まずはprint(mp)で現状の精度の設定を確認。

>>> print(mp)
Mpmath settings:
  mp.prec = 53                [default: 53]
  mp.dps = 15                 [default: 15]
  mp.trap_complex = False     [default: False]

mp.prec = 53 [default: 53]は二進法の計算で割り切れない場合、何桁まで計算をするか(精度)、の設定。
mp.dps = 15 [default: 15]十進法の精度の設定。

このmp.precとmp.dpsの関係はほぼmp.prec = mp.dps * 3.333…との事。
例えばmp.precを33にすると、mp.precは9に自動的に設定される。

>>> mp.prec = 33
>>> print(mp)
Mpmath settings:
  mp.prec = 33                [default: 53]
  mp.dps = 9                  [default: 15]
  mp.trap_complex = False     [default: False]

ちなみに、”mp.trap_complex = False [default: False]”の部分についてはmpmathのチュートリアルをパッと見て詳しい説明を見つける事が出来なかったのでとりあえずこういうものとしておく。

精度設定はdefaultに戻して以降続ける。

実際の計算

今回は前回の記事で使った4.45の二進法表記を求める。

まずはmpfメソッドで求めたい数の浮動小数点数での実数(real float)を求める。
mpmathのContextのページによると、”mpf creates a real number”とあるが、浮動小数点数での実数(real float)と言う理解。

浮動小数点とは固定長の仮数部と固定長の指数部の2つの部分の組み合わせによって、数値を表現したもの。
例えば十進法(基数が10)の場合、1.2345は仮数部の12345と、指数部の10の-4乗で表現できる。

ちなみに、mpcメソッドで複素数(実数と虚数を組み合わせた数)の浮動小数点数(Complex float)を求める事が出来るとの事だが、今回は実数の話なのでそこには触れない(ぶっちゃけ分からないから触れられない が正解)。

>>> n = mpf('4.45')
>>> n
mpf('4.4500000000000002')

繰り返しになるが、nはmpfメソッドで求めた、実数4.45を浮動小数点数で表したmpfオブジェクトを代入したもの。

dir関数でn(に代入されたmpfオブジェクト)の属性(attribute)を調べると以下の通り。

>>> dir(n)
['__abs__', '__add__', '__bool__', '__class__', '__cmp__', '__complex__', '__delattr__', '__dict__', '__dir__', '__div__', '__doc__', '__eq__', '__float__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__le__', '__long__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__neg__', '__new__', '__nonzero__', '__pos__', '__pow__', '__radd__', '__rdiv__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__round__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__setstate__', '__sizeof__', '__slots__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__weakref__', '_cmp', '_ctxdata', '_mpf_', 'ae', 'bc', 'conjugate', 'context', 'exp', 'imag', 'man', 'man_exp', 'mpf_convert_arg', 'mpf_convert_lhs', 'mpf_convert_rhs', 'real', 'sqrt', 'to_fixed']

この内の’exp’プロパティ(the exponentの略)で指数部、’man’プロパティ(the mantissaの略)で仮想部を得る事が出来る。

>>> n = mpf('4.45') # からの続き
>>> n.man
5010254585449677 # 4.45をバイナリ演算した仮数部(十進法表記)
>>> n.exp
-50 # 同様に指数部

この内manプロパティで出力した数字は、バイナリ演算(binary arithmetic)で出力したものなので、下記の関係が成立する。

>>> n.man * 2 ** n.exp
4.45
>>> 5010254585449677 * 2 ** -50
4.45

このn.manの部分を二進法で表記する事で、求める4.45の二進数表記が得られる。

>>> bin(n.man)
'0b10001110011001100110011001100110011001100110011001101'

これだと小数点の位置も何も分かり難い。
下記の通り正式に使える表記にする。

>>> print(f'{bin(n.man)} x 2**{n.exp}')
0b10001110011001100110011001100110011001100110011001101 x 2**-50

正式な表記にはなったものの、桁数が多いので読み難いのは確か。
桁数が増える程に精度が上がるが、反比例して読みにくくなる。
計算に使う訳では無い今回の様な場合、mp.prec = 10位に調整して更に読みやすくする。

>>> mp.prec = 10
>>> print(mp)
Mpmath settings:
  mp.prec = 10                [default: 53]
  mp.dps = 2                  [default: 15]
  mp.trap_complex = False     [default: False]
>>> m = mpf('4.45')
>>> m
mpf('4.4531')
>>> m.man
285
>>> m.exp
-6
>>> print(f'{bin(m.man)} x 2**{m.exp}')
0b100011101 x 2**-6

100011101の右から6番目にカンマを打つと100.011101

以上から4.45の二進法表記は100.011101…と分かる。

コメント

タイトルとURLをコピーしました