Pythonプログラミング(ステップ2・補足・数の表現)

このページでは、コンピュータが数値をどのように扱っているかについて補足する。

0. 2進数と16進数

ディジタル機器の内部では、電圧の高低など、区別可能な2つの状態の組み合わせによって処理が行われているから、 状態を2進数に対応付けるのは自然な発想と言えるし、事実、コンピュータ科学では「二進数でモノを考える」場合が多い。

0と1の並びを2進数の表現と考えると、例えば 10101011は $$ 1 \times 2^7 + 0 \times 2^6 + 1 \times 2^5 + 0 \times 2^4 + 1 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 1 \times 2^0 $$ を表し、10進では171となる。このとき、一番右側の桁($2^0$を乗じている桁)はLSB(Least Significant Bit)と呼ぶ。 反対に、最上位の桁はMSB(Most Significant Bit)である。

大きな数を二進数で表すと桁数が多くなって、人間にとっては扱いづらい。 そこで、2進数と相性の良い16進表示がよく用いられている。 2進数を16進数に変換するには、4桁ずつをまとめて、それぞれ16進数の桁に対応させればよい。 例えば、2進数 101010111010 1011 に分けると、上の桁1010 は(10進で)10、下の桁1011は(10進で)11となる。

2進数の各桁を表現するのに0,1の2つの記号、10進では0から9までの10個の記号を用いるように、16進数を表すには16個の記号が必要となる。 そこで9より上をアルファベット文字に対応させて

 2進 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
10進    0    1    2    3    4    5    6    7    8    9   10   11   12   13   14   15
16進    0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F

と表すのが一般的である。すると、10101011 の16進表現は AB ということになる。


1. 整数の表現

符号あり整数

32ビットコンピュータの内部では、整数は32桁の2進数として取り扱われており、その場合、表現できる数の範囲は、-2147483648〜2147483647 である。 数値とビットの配置の対応は、正の数については、直感されるのと同様

数値       ビットのパターン
0          00000000000000000000000000000000
1          00000000000000000000000000000001
2          00000000000000000000000000000010
3          00000000000000000000000000000011
4          00000000000000000000000000000100
5          00000000000000000000000000000101
...
2147483647 01111111111111111111111111111111

である。正の数はMSB(最上位のビット)が0である。

一方、負の数は

数値        ビットのパターン
-1          11111111111111111111111111111111
-2          11111111111111111111111111111110
-3          11111111111111111111111111111101
-4          11111111111111111111111111111100
-5          11111111111111111111111111111011
...
-2147483647 10000000000000000000000000000001
-2147483648 10000000000000000000000000000000

のように表現され、MSBがいつも1となる。

この負数の表現方法は、以下に述べる考え方に基いている。

ある整数 $a$ の符号を反転させた数を $b \; (=-a)$ とすると、当然 $a+b=0$ が成り立つべきである。上記のビットのパターンはまさにそのように構成されており、例えば $a=1, \ b=-1$とすると

  a=  00000000000000000000000000000001
  b=  11111111111111111111111111111111
a+b=  00000000000000000000000000000000

となっている(繰り上がりの桁は、表現可能な桁からはみ出してしまうので、実質、「無かった」ことになる)。 このような負数の扱い方2の補数による表現と呼ぶ。 上記の例から明らかなように、$a$ の符号を反転するには、$a$ のビットを全て反転した数にさらに1を加えた数を $b$ とすればよい。

桁数に制限のあるような符号付きの整数を扱う場合は、表現できる数値の範囲の他、「予期せぬ」挙動にも注意が必要である。

例えば、2147483647 * 2の結果は-2となる。なぜなら、2147483647のビットパターンをひとつ左にシフトする(2倍する)と、それは-2のパターンに一致するからだ。同様に(-2147483647)*22になる。

その一方で、(-1)/2は、ビットパターンを1つ右にシフトした値2147483647なるわけではない(結果は0である)。 演算の結果が、表現できる範囲の境界をまたいでしまわない限り、符号を含め、値は正しく評価される(そうでないと困る・・)。

Pythonの整数表現

コンピュータで整数を扱う際には、上で述べたような注意が必要であるが、Python 3での整数(int型)では(実用上)使用できる桁数に制限が無い。 Python 2のint型は32ビットまたは64ビットの有限の桁数であるが、long型を使うことで、桁数の制限は無くなる。


2. ビット演算

プログラミングにおいては、整数の値そのものではなく、ビットのパターンを問題とするケースも多い。 そのため、ビット操作のための演算子がいくつか用意されている。

ビット演算 動作
 a & b 
aとbのビット毎の論理積(AND)
a,bの同じ位置のビットが共に1の場合のみ、結果の対応するビットが1
ビット毎に、$0\&0=0,\ 1\&0=0,\ 0\&1=0,\ 1\&1=1$
 a | b
aとbのビット毎の論理和(OR)
a,bの同じ位置のビットのいずれかが1の場合のみ、結果の対応するビットが1
ビット毎に、$0|0=0,\ 1|0=1,\ 0|1=1,\ 1|1=1$
 a ^ b 
aとbのビット毎の排他的論理和(XOR, Exclusive OR)
ビット毎に、$0 \wedge 0 = 0, \ 1 \wedge 0 = 1,\ 0 \wedge 1 = 1, \ 1 \wedge 1 = 0$
 ~ a 
aのビットを反転(aの1の補数)した値
 a << b 
aのビットをbだけ左にシフトした値
 a >> b
aのビットをbだけ右にシフトした値

上の表で、演算の結果、a,bの値が変化するわけではないことに注意。a + b がaとbの和を与えるが、a,bの値は変化しないのと同じことである。 以下に、ビット演算子を用いた、いくつかのパターンを示す:

整数 a の m ビット目が1かどうかを調べる: if a & (1<<m):

整数 a の m ビット目を1にセットする: a = a | (1<<m) ;

整数 a の m ビット目を0にセットする: a = a & ~(1<<m) ;

整数 a の m ビット目を反転する: a = a ^ (1<<m) ;


3. 実数の表現

実数を近似的に表現するには、数字の並びの決まった位置に小数点があるとみなして、XXXX.YYYYY のように解釈するのが一番簡単である。 これを固定小数点数と呼ぶ。 この方法はその単純さ引き換えに、表現できる数の範囲が(整数型と同様に)限られてしまう。

ところが、実数を使いたいような場面では、大きな数(例えば、光速は $2.9979 \times 10^8$ m/s )や小さな数(プランク定数は $6.626 \times 10^{-34}$ m$^2$ kg/s)がしばしば登場する。 そこで、数値を、仮数部(光速の場合は 2.9979)と、指数部(光速の場合は $10^8$の8)の組み合わせによって表現する方法が一般的である。 そのように表した数を浮動小数点数と呼ぶ。

例えば32ビット計算機の実数(Pythonのfloat32型)では、IEEE 754方式と呼ばれる浮動小数点数が広く用いられており、符号に 1 ビット、仮数部に23ビット、指数部に8ビットが割当られている。 日常的には10のべき乗を使って大きい(小さい)数を表現するが、IEEEの方式では2のべき乗が用いられる。 指数部に8ビットが割当られているため、表現できる数の大きさの範囲は、凡そ、$2^{-127}$〜$2^{127}$ となる(大体、$10^{\pm 38}$)。 一方、仮数部には23ビットが割当られているので、有効桁数は10進に換算すると約7桁(弱)になる。

IEEE 754方式では、「数ではない数」が特定のビットのパターンに割当られている。例えば、平方根関数sqrt( )の引数に負の値を与えると、結果は NaN (Not a Number)となるが、この NaN は、指数部がオール1 (255)で、仮数部が非ゼロのパターンで表現されている。 加えて、正負の無限大も定義されている。

さらに精度や桁数が必要な場合には、float64型(C言語のdouble型)を用いることができる。