虎之助の徒然記

【Python】numpy 整数の演算結果が float64 に。摩訶不思議なデータ型変換ルール。

【概要】 numpyで扱っていた整数が、いつのまにか float64 の浮動小数になっていました。pyhonでは、被演算子のデータ型の組み合わせによっては、演算結果のデータ型が変更されます。動的型付けの影響です。調べてみると、この変換にはいろいろなパターンがあります。そして、整数の演算同士の演算でも、その結果が浮動小数となる摩訶不思議なデータ型の変換ルールがあることも分かりました。

1. はじめに

 GPTテーブルを読み取るPythonスクリプト(関連記事は、ここ)を書いていて、困ったのが、セクタ数をバイト数に変換する際、numpy.uint64 のつもりの演算結果が float64 の浮動小数になってしまったこと。これでは、ファイルの頭出し ( f.seek() ) ができません。

 とりあえずの解決策は見つけたのですが、動作は良く理解していないままでした。本稿では、uint64の演算がfloat64 へ変換されるケースについて、まず述べます。その後、pythonにおける演算結果のデータ型の割り付け規則についてまとめます。

 なお、本稿は、python3 での動作について述べています。python2 の場合は動作が異なります(付録B参照)。

2. uint64 の演算結果が、float64 になる

2.1 整数が浮動小数になって、f.seek できない

 numpyのfloat32のスカラー値 (numpy.float32) と ( numpy ではない ) 普通のスカラー値の整数 (<class 'int'>) や浮動小数 (<class 'float'>) との演算を行うと、自動的に numpy.float64 の浮動小数になるそうです*1

 このようなデータ型の変更は、numpy.float32に限らず、numpy.unit64の場合にも発生します。

 例えば、セクタ数 (sector=2)をバイト数 (byte=1024) に変換するつもりで以下のように演算すると、byte は numpy.float64 の変数になってしまいます。

>>> import numpy as np
>>>
>>> sector = np.array(2,'uint64')
>>> 
>>> sector
array(2, dtype=uint64)
>>> type(sector)
<class 'numpy.ndarray'>
>>> sector.dtype
dtype('uint64')

この uint64 の numpy の変数 sector を用いて、バイト数に変換して、ファイルを f.seek(sector*512) しようとすると、エラーとなります。

>>> f = open('tmpfile','rb')
>>> f.seek(sector*512)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'numpy.float64' object cannot be interpreted as an integer

 sector*512 は、numpy.float64 の変数になっているようです。

>>> byte = sector*512
>>> byte
1024.0
>>> type(byte)
<class 'numpy.float64'>
>>> byte.dtype
dtype('float64')

 float64 になった値をint() や astype() で整数に変換すればよいのかしれませんが、美しくありません。

2.2 float64 への昇格問題

 これは、型の昇格というpythonの動作によるものです。普通のスカラー値、numpy によるスカラー値 ( 0次元配列)、numpy の配列の3つの場合について、乗算をした結果は、次の表のようになります。

512
<class 'int'>
-
np.array(512,'uint64')
<class 'numpy.ndarray'>
'uint64'
np.array([512],'uint64')
<class 'numpy.ndarray'>
'uint64'
2
<class 'int'>
-
1024
<class 'int'>
-
1024.0
<class 'numpy.float64'>
'float64'
[1024]
<class 'numpy.ndarray'>
'uint64'
np.array(2,'uint64')
<class 'numpy.ndarray'>
'uint64'
1024.0
<class 'numpy.float64'>
'float64'
1024
<class 'numpy.uint64'>
'uint64'
[1024]
<class 'numpy.ndarray'>
'uint64'
np.array([2],'uint64')
<class 'numpy.ndarray'>
'uint64'
[1024]
<class 'numpy.ndarray'>
'uint64'
[1024]
<class 'numpy.ndarray'>
'uint64'
[1024]
<class 'numpy.ndarray'>
'uint64'

乗算結果の上段は、print()の結果、中段はtype()の結果、下段はdtype属性。

 この表から、次のことが分かります。

  • float64 に型変換されるのは、普通のスカラー値 (<class 'int') と numpy のスカラー値との間で演算が行われた場合のみ。
  • それ以外は、すべて整数型のまま。

 整数同士の演算結果が浮動小数に型変換されるのは、奇妙な仕様です。

3. データ型変換のルール

 前節で調べたのは、型の組合せの極一部です。主な整数型、浮動小数型について、プログラムを作って調べました(付録A参照)。

3.1 調べたデータ型

 調査対象は、普通のスカラー値の int ( <class 'int'>)、普通のスカラー値の float( <class 'float'>)、numpy の整数 ( 32ビット、64ビット、符号有無)、浮動小数(32ビット、64ビット )、及び、それらの配列の計14個の型です。

  • 普通のスカラー値のint (i)
  • 普通のスカラー値のfloat (f)
  • numpyのint32 (i4)、uint32 (u4) 、int64 (i8)、uint64 (u8)、及び、それらの配列
  • numpyのfloat32 (f4), float64(f8)、及び、それらの配列

3.2 データ型の変換テーブルと変換ルール

 これらの型の演算結果の組合せは、以下の図のテーブル(マトリックス)に示す通りです。[ ]の場合は、配列です。最上段、最左列がそれぞれの被演算子のデータ型、それぞれの被演算子と演算を行った結果をテーブルにしています。上右三角の部分と左下三角の部分は同じ結果となるので、省略しています。

3.2.1 加算・減算・乗算の場合のデータ型の変換テーブル

 演算は、加算・減算・乗算、いずれの場合も同じ結果です。除算は異なります。

f:id:toranosuke_blog:20180928224221p:plain
 14個のデータ型の演算結果のデータ型。

 i, f は、<class 'int'>、<class 'float'>、それ以外は numpy の型コードによる表記(例えば、i4 であれば int32、[i4] は int32 の配列)。赤は整数が浮動小数に変換される場合、青色は、型の昇格がある場合、オレンジは型の降格がある場合、灰色は符号が変わる場合。赤・青の細線は被演算子のいずれかが昇格・降格の場合、太線は被演算子の両方にとって昇格の場合。スカラー型の int、float は、それぞれ int64、float64 の精度として、昇格・降格を判定。

f:id:toranosuke_blog:20180928193435p:plain

 スカラー値、0次元配列(スカラー値相当)のみ。64ビットの符号なし整数 (uint64) と符号あり整数 (int, int32, int64) の時に64ビットの浮動小数 (float64) に変換される。

f:id:toranosuke_blog:20180928193453p:plain
配列のみ。

3.2.2 加算・減算・乗算の場合のデータ型の変換ルール

 この結果から次の変換ルールがあることが分かります。

  • 型の順位:float64 > float32 > int64, uint64 >int32, uint32
  • スカラー同士の演算
    • 同じ順位の型同士の演算で、型は変わらない。
    • float32、float64との演算 ⇒ float64
    • 符号なし整数と符号あり整数の演算は、昇格する。
      • uint32 と int32 ⇒ int64
      • uint64 とint, int32, int64 ⇒ float64
    • 型の順位が異なる場合は、順位が上位の型に合わせる
      • int32とint64 ⇒ int64
      • uint32とint64 ⇒ int64
      • uint32とuint64 ⇒ uint64
  • 配列とスカラーの演算
    • float32の配列とスカラーの演算 ⇒ float32の配列
    • float64の配列とスカラーの演算 ⇒ float64の配列
    • 整数配列とfloat32, float64の演算 ⇒ float64の配列
    • 整数配列と整数型スカラーの演算 ⇒ 入力配列の型と同じ型の配列
       スカラーの型順位が上位の場合、スカラーのデータ型が降格する。
  • 配列と配列の演算
    • スカラー同士の演算に準じる。

 動的型付けの演算では、何らかの変換ルールで、演算結果のデータ型を決める必要があります。uint64 と符号あり整数型の演算の場合を除けば、概ね理解できる変換ルールです。

 符号なし整数と符号ありの整数は昇格させるという基準で設定されている変換ルールと思いますが、だからと言って、昇格した先のデータ型が浮動小数というのは、理解しがたい仕様です。

3.2.3 除算の場合のデータ型の変換ルール

 除算の場合の変換テーブルは、以下の図の通りです。

f:id:toranosuke_blog:20180928230322p:plain
除算のデータ型。

 変換ルールは、次の通りです。

  • 除算の場合は、基本的にfloat64を出力する。
  • 但し、
    • float32とfloat32の演算 ⇒ float32
    • float32の配列とfloat32の配列 ⇒ float32の配列
    • float32の配列とスカラーの演算 ⇒ float32の配列

3.3 摩訶不思議なデータ型の変換ルール

 pythonは予め出力のデータがを決めない動的型付けのため、被演算子に基づき決めた何らかのデータ変換ルールに従って、演算を行う必要があります。そのデータ変換ルールが、前節で説明したような変換ルールだったわけです。

 概ね、理解できる変換ルールですが、整数同士の演算を浮動小数点に昇格させてしまうのは、直感的に予想できない変換ルールです。また、uint64 の主たる使用目的の一つはアドレス指定と思います。

 そうであれば、uint64を浮動小数へ変換するルールは、奇異なルール、摩訶不思議なルールと言わざるを得ません。

 浮動小数で困る場合には、int() や astype() などで整数型へ変換を行えば済む話かもしれません。それでも、float64 への昇格ではなく、64ビットの整数への変換の方が分かりやすく、適切な仕様ではないでしょうか。

4. 最後に

 pythonのデータ型の変換についてまとめました。pythonでは、uint64型と符号あり整数型の演算が浮動小数に変換されるという摩訶不思議なデータ型の変換ルールがあり、注意が必要です。

 私のような Python 初心者でなくても、はまりそうな点ではないかと思います。

(2018/9/29)

関連記事

付録A:演算結果のデータ型を表示するスクリプト

 python3のスクリプトです。動作確認は、ubuntu18.04で行っています。

 python2のスクリプトも作っています(python3との違いは、print文の関係のみ)。python2のスクリプトと実験結果と合わせて、ここ (Googleドライブ) に置いておきます。

#!/usr/bin/python3
#
# ckeck_types.py
# 
# make tables of data types converted with operation in python. 
#
# copyright (c) 2018 Toranosuke Tenyu

import sys
import numpy as np

def add_bra(cc,bra=False):

    if bra == True:
        return "["+cc+"]"
    else:
        return cc

def cc (dd):

    ti = type(2)
    tf = type(2.0)

    bra = "[" in "{}".format(dd)

    if hasattr(dd,"dtype"):
        code = dd.dtype
        if code == 'int32':
            return add_bra('i4',bra)
        if code == 'uint32':
            return add_bra('u4',bra)
        if code == 'int64':
            return add_bra('i8',bra)
        if code == 'uint64':
            return add_bra('u8',bra)
        if code == 'float32':
            return add_bra('f4',bra)
        if code == 'float64':
            return add_bra('f8',bra)
    else:
        code = type(dd)
        if code == ti :
            return 'i'
        if code == tf :
            return 'f'

def print_dtype(dd,dd_print=False):

    if dd_print:
        print("%5s"%dd,"%5s"%cc(dd),end=" ")
    else:
        print("%5s"%cc(dd),end=" ")
            

def print_conv(data,dd_print=False,dd_trig=True,op="mul"):

    if dd_print:
        print("%10s"%"",end="  ")
    else:
        print("%5s"%"",end=" ")

    for dd in data:
        print_dtype(dd,dd_print)

    print("")

    for dd2 in data:
        print_dtype(dd2,dd_print)

        for dd1 in data:

            if op == "mul":
                val = dd2*dd1
            elif op == "add":
                val = dd2+dd1
            elif op == "sub":
                val = dd2-dd1
            else:
                val = dd2/dd1
                
            print_dtype(val,dd_print)
            if dd_trig :
                if cc(dd1) == cc(dd2):
                    break

        print("")
            

        
if __name__ == '__main__': 

    i2 = 2
    f2 = 2.0

    # print orignal data
    # dd_print = True
    dd_print = False

    # print only lower triangular matrix
    dd_trig = True
    #dd_trig = False

    op = "mul"
    # op = "add"
    # op = "sub"
    # op = "div"

    print(sys.version)

    print("")
    
    data = [
        i2,
        f2,
        np.array(i2  ,'i4'),
        np.array([i2],'i4'),
        np.array(i2  ,'u4'),
        np.array([i2],'u4'),
        np.array(i2  ,'i8'),
        np.array([i2],'i8'),
        np.array(i2  ,'u8'),
        np.array([i2],'u8'),
        np.array(f2  ,'f4'),
        np.array([f2],'f4'),
        np.array(f2  ,'f8'),
        np.array([f2],'f8')
    ]
    print_conv(data,dd_print,dd_trig,op)
    print("")

    data = [
        i2,
        f2,
        np.array(i2  ,'i4'),
        np.array(i2  ,'u4'),
        np.array(i2  ,'i8'),
        np.array(i2  ,'u8'),
        np.array(f2  ,'f4'),
        np.array(f2  ,'f8')
    ]
    print_conv(data,dd_print,dd_trig,op)
    print("")

    data = [
        np.array([i2],'i4'),
        np.array([i2],'u4'),
        np.array([i2],'i8'),
        np.array([i2],'u8'),
        np.array([f2],'f4'),
        np.array([f2],'f8')
    ]
    print_conv(data,dd_print,dd_trig,op)
    print("")

付録B. python2との違い

 python2とpython3で加算・乗算・乗算については、同一動作でした。

 しかし、除算については、python3と動作が異なります。変換テーブルは、以下の通りです。

f:id:toranosuke_blog:20180929153135p:plain
Python2の除算演算における変換規則。

 この変換規則は、加算・減算・乗算の変換規則と同一です。つまり、python2では四則演算の全てで同じ変換規則を使っています。

 一方、python3では、加算・減算・乗算に関しては、python2と同じですが、除算に関しては、基本的に float64 を出力するように変更されています。

 つまり、python2とpython3の違いは、除算結果のデータ型の変換ルールの違いとなります。