Webページでは数式を表すためにLaTeX表記が使えるMathJaxを利用しています。 WebブラウザにはSafari/Chrome/Firefoxを使って下さい(IEでは表示できないようです。)

オブジェクト指向--有理数計算を例にして

Pythonはオブジェクト指向プログラミング言語(OOP: Object-Oriented Programming language)の1つで、標準的に提供されている豊富なクラス(データ構造とそれができること:メソッド)以外に、問題解決のためにプログラマーが新しいクラスを定義することができる。

悩ましい浮動小数計算

Pythonを含む大抵のプログラミング言語では $\frac{22}{3}$を 22 / 3 で計算すると、整数商 7 を返す((Python2 では 22 /3 、Python3では 22 // 3 で計算する)。 有理数 22 / 3 を値としての計算をするためには浮動小数点数(floating point numbers)を使って 22.0 / 3 とするのであるが、Python では次のように結果する。

>>> 22.0 / 3
7.333333333333333
浮動小数点数は、計算機ハードウェアの中では基数を 2 とする (2進法の) 分数として表現されている。 たとえば、少数 0.125 は基数10 と 2 を使うと次のように表される。 \begin{align*} 0.125 &= [0.125]_{10}=\frac{1}{10}+\frac{2}{10^2}+\frac{5}{10^3}\\ &=[0.001]_2=\frac{0}{2}+\frac{0}{2^2}+\frac{1}{2^3} \end{align*} しかし、一般に 2 を基数とした表現で何桁使おうとも、10 を基数をした数を正確に表現することはできない。 たとえば、数 0.1 = 1 / 10 は基数 2 では循環小数 (repeating fraction) となる(0011が繰り返される)。 \[ 0.1 = [0.0\dot{0}\dot{0}\dot{1}\dot{1}]_2= 0.0001100110011001100110011001100110011001100110011\dots \] Pythonでは、大抵の処理系では浮動小数点をfloat 型として 53bit の精度で保持する。 10進数で 0.1 と書いたときに内部では次のような2進の小数が格納される。 \[ [0.00011001100110011001100110011001100110011001100110011010]_2 \] これは 1/10 に近いが、厳密には同じ値とはならない。 事実、次のような数である。 \[ [0.1000000000000000055511151231257827021181583404541015625]_{10} \not =\frac{1}{10} \]

演習: コンピュータにおける浮動小数点数の保持の構造 符号部仮数部指数部 とは何か、どのような規格があるのか(IEEE 754など)を調べてみなさい。 浮動小数点数の取り扱いは、数値計算における計算誤差や処理速度まで後半に及ぶ。

次の結果は、浮動小数計算を内部で実現するハードウエアの宿命である(Pythonのバグではない)。

>>> 0.1 + 0.2
0.30000000000000004
多くのプログラミング言語では、そのままの形では \[ \frac{1}{10}+\frac{2}{10} \] を正しく取り扱うことができないということだ。 それでも、Pythonが頑張って次のように計算する。
>>> 22.0 / 3 + 2.0 /3
8.0
しかし、この結果は \[ \frac{22}{3} + \frac{2}{3} = \frac{24}{3} = 8 \] を正しく計算していると信じる根拠とはならない。

演習: 上の浮動小数計算の結果を確かめてみなさい。 その他に ?! と思うような浮動小数計算の例をPythonで探してみなさい。

有理数計算モジュール fraction

Pythonでは fractions ('s'が付いている)モジュールをインポートすると有理数計算を行うことができる(Python2: 数値と数学モジュール fractions - 有理数)。 有理数(rational numbers)とは、整数である分母(denominator)と分子(numerator)からなる分数の形(ただし分母はゼロでない)の数 \[ \frac{p}{q},\qquad p,q\in\mathbb{Z}, q\not= 0 \] である。

有理数 $\frac{p}{q}$ を Fraction(p, q) であらわして、以下のような有理計算 $\frac{22}{3}+ \frac{2}{3} = \frac{8}{1}$ を正しく実行することができる。 計算結果も常に Fraction(p', q') となって有理数 $\frac{p'}{q'}$ の形を保持する。
>>> import fractions
>>> fractions.Fraction(22,3) + fractions.Fraction(2,3)
Fraction(8, 1)

また、 $\frac{2}{3}+ \frac{1}{5} + \frac{3}{7} = \frac{136}{105}$ も

>>> fractions.Fraction(2,3) + fractions.Fraction(1,5) + fractions.Fraction(3, 7)
Fraction(136, 105)
と正しい結果を返す。 このとき、Fraction(a, b) は既約分数(irreducible fraction)を返すことにも注意しよう。
>>> fractions.Fraction(24,128)
Fraction(3, 16)

演習: 上のような有理数計算が実行されることを確かめなさい。

Pythonの数学計算モジュール math を使うと数学計算が可能になる(複素数版 cmath もある)。 $\sin, \cos, \tan$などの三角関数群、指数関数 exp(x) や対数関数 log(x), 平方根 sqrt(x) や 床関数 $\lfloor x\rfloor =$ floor(x)、天井関数 $\lceil y\rceil =$ ceil(y) などに加えて、定数として、円周率 $\pi=$ pi や $\mathrm{e}=$ e が使える。

math モジュールを使うと、次のように Fraction に浮動小数 $\pi$ を与えて有里数表示も得ることができる。

>>> import math
>>> fractions.Fraction(math.pi)
Fraction(884279719003555, 281474976710656)
演習: Pythonでは標準的に「比較的大きな」整数を保持できる。 上で得た $\pi$ の有理値がどれほど正しいかを数表などで確かめてみなさい。次の結果を小数点以下を確認する。
>>> 884279719003555.0/281474976710656
3.141592653589793
演習: 自然対数の底 $\mathrm{e}$ の有理表示についてどうなるかを実行し、その値の正確さを確認しなさい。

スクリプトを書いて実行しながら、不足分のクラスメソッド定義を追加していく

Python で新しいクラスを定義するには、クラス名とクラスに属するオブジェクトを操作するメソッドを用意して次のように書く。 構造単位の字下げにも注意を払う。

class クラス名:
    コンストラクタや
    メソッドを定義する
    ......
    ......

以下の例で明らかなようにオブジェクト指向プログラミングを修得するためには、新しいクラス定義を記して。それの動作を検証するテストスクリプトを実行しながら、クラス設定と必要なメソッドを考えていくというサイクルがたいへん重要だ。 「教科書」で説明しているソースを眺めるだけでは、「体得」に到達することは難しい。

rationalクラスを構築する

オブジェクト指向プログラミングにおけるクラスの概念を理解するには大変有効なケーススタディとして、有理数クラス rational を定義してみよう。 上で観たように既にPythonにには有理数計算を実行できるモジュール fractions が備わっているのであるが、新しいクラスの設計と実装の理解のために、自力で有理計算クラスを定義してみる(数の抽象クラス numbers があり、numbers.Rationalは定義されているが、ここで定義する rational クラスがバッティングする心配はない)。

rational クラスを定義するためには、まずコンストラクタメソッド __init__(2文字のアンダースコア(_)で前後を挟まれている!!)を定義する。 コンストラクタは、クラスのインスタンス(instance)生成時に常に実行される初期化関数のようなものである。

__init__() の定義において、最初の特別な引数 self省略できずに必須、クラスオブジェクト自身の参照に必要だ。 インスタンス生成に必要とするのは有理数 top / bottom に必要な2つの仮引数(整数) top と bottom を与えると、rational クラス自身の局所変数 num と den (これを self.num, self.den と参照するしている)。 ただし、ここでは bottom を省力した場合には bottom に 1 を代入するようにデフォルト値を設定するように書いている。 rational(4) とだけしたときは rational(4,1) の動作となる。

class rational:
    """class rational represents a rational number.
    """
    def __init__(self, top, bottom = 1):
        """constructor for rational numbers
        """
        self.num = top
        self.den = bottom

2重引用符3回 """ と """ で囲まれた文字列はPython Documentとして pydoc などで閲覧できる。 コメント代わりに、丁寧にクラスの意味やメソッドの定義について説明を加えるとよい。

演習: Pythonドキュメントの様子を pydoc を使って確認しなさい。 拡張子 .py を省略する。
$ pydoc rational_test

動作を検証するスクリプト を rational_test.py に追加保存し、それを実行することを前提に必要なメソッドを考えていこう。 ここまでの段階で次のようなスクリプト rational_test.py を書いて実行することができる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class rational:
    """class rational represents a rational number.
    """

    def __init__(self, top, bottom = 1):
        """constructor, initailly involed when creating rational instances
        """
        self.num = top
        self.den = bottom

###  end of class ration

r1 = rational(3,5)

16行目で $\frac35$ を意図したrationalクラスのインスタンス rational(3,5) を生成して変数 r1 に代入している。

演習: これを実行してもエラーは生じないが、何も起こらない。 16行明に print r1 とするとどうだろうか?試してみなさい。

コンストラクタだけの定義では、クラス自身の局所変数 self.num と self.den に値が渡っただけで、それをどのように操作するのがクラス自身はまだ知らない。 そこで、rational クラスに次のメソッドを追加する。 メソッド定義の引数に self をセットしていることに注意。 実際には、rational インスタンス r にメソッド show を作用させるには r.show() と引数はとらない(コンストラクタもそうであった)。

    def show(self):
        """
        show rational number in q/p
        """
        print self.num, '/', self.den

ここまでで、次のスクリプトが得られる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class rational:
    """class rational represents a rational number.
    """
    def __init__(self, top, bottom = 1):
        """constructor, initailly involed when creating rational instances
        """
        self.num = top
        self.den = bottom

    def show(self):
        """
        show rational number in q/p
        """
        print self.num, '/', self.den

###  end of class ration

r1 = rational(3,5)
r1.show()
print r1
演習: スクリプト rational_test.py を実行すると、22行の有理数を表示には成功するが、23行目でエラー発生することを確かめなさい。

print はプリントすべきオブジェクトを出力可能な文字列に 変換され ているときに上手く動作する。 今のままでは、変数に格納されているインスタンスの格納アドレスを算法してしまうので上手く働かないためにエラーが生じた。

printはrationalクラスでも上手く働くようにするには、オブジェクトを文字列に変換する元から備わっていたメソッド __str__ を今の場合でも働くように実装を拡張すればよい。 これをオーバーライド(override)という。 クラス rational ないで次のようにオーバーライドする、

    def __str__(self):
        '''Display self as a string. override __str__ method
        '''
        return str(self.num) + '/' + str(self.den)

こうして、次のスクリプトが得られる。 少し長くなるがすべてを書きだそう。 クラス定義の後に、インスタンス内容を2つのやり方で表示するスクリプトを書いている

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class rational:
    """class rational represents a rational number.
    """

    def __init__(self, top, bottom = 1):
        """constructor, initailly involed when creating rational instances
        """
        self.num = top
        self.den = bottom

    def __str__(self):
        '''Display self as a string. override __str__ method
        '''
        return str(self.num) + '/' + str(self.den)

    def show(self):
        """
        show rational number in q/p
        """
        print self.num, '/', self.den

###  end of class ration

r1 = rational(1,4)
r1.show()
print r1
r2 = rational(4,3)
print r1, r2
print r1 + r2
演習: スクリプト rational_test.py を実行して、32行目でエラーが発生することを確かめなさい。

既約分数で rational クラスを定義する

しかしこの段階においても、たとえば インスタンス r1 = rational(18,24) として、これを表示すると 18 / 24 と当たり前に表示される。 しかし、18 / 24, 6 / 8, 24 / 32 などは全て同じ有理だと見なし、その代表として 6/8 と見るのである。 つまり、有理数は既約分数(irreducible fraction)としてインスタンス化され、計算結果も既約化されるようにクラス定義をすべきである。

\[ \frac{q}{p} \Rightarrow \frac{q}{\mathrm{gcd}(q,p)} \Big/ \frac{p}{\mathrm{gcd}(q,p)} \]

$\mathrm{gcd}(q,p)$ は 整数 $q, p$ の最大公約数(GCD: Greatest Common Divisor)である。 したがって、rational クラスが既約分数として取り扱えるようにするためには、クラス定義の外側で、最大公約数を計算する次のような関数 gcd を定義して、rationalクラスのコンストラクタを書き換えおく必要がある。

def gcd(m, n):
    """GCD for lowest terms representation of rational numbers
    """
    while m % n != 0:#  Euclidean Algorithm
        old_m = m
        old_n = n
        m = old_n
        n = old_m % old_n
    return n
演習: 上の関数 gcd を定義して、既約分数を取り扱うようなクラス定義であつようにコンストラクタを修正したスクリプト rational_test.py を書いて、それが上手く働いているかを確かめるような実行結果を表示してみなさい。

rationalクラスに加減乗除メソッドを追加する

rational クラスでは次の加減乗除からなる四則演算をメソッド化して実装する必要がある。 いずれも分母がゼロでない有理数でのみ演算が定義され、その結果も分母がゼロでない場合に意味を持つ。

\begin{align*} \frac{n_1}{d_1}+\frac{n_2}{d_2} &= \frac{n_1\times d_2 + n_2\times d_1}{d_1\times d_2}\\ \frac{n_1}{d_1}-\frac{n_2}{d_2} &= \frac{n_1\times d_2 - n_2\times d_1}{d_1\times d_2}\\ \frac{n_1}{d_1}\times \frac{n_2}{d_2} &= \frac{n_1\times n_2}{d_1\times d_2}\\ \frac{n_1}{d_1}\div \frac{n_2}{d_2} &= \frac{n_1\times d_2}{n_2\times d_1} \end{align*}

これらを既に定義されている __add__(記方法 -), __sub__(記方法 -), __mul__(記方法 *), __div__(記方法 / )としてメソッドのオーバーライドとして定義する。 たとえば、掛け算のメソッド __mul__ は次のように rational クラス内でオーバーライドして定義する。

    def __mul__(self, other):
        """Override: Multiplication of two rational numbers , allowing notation self * other
        """
        return  rational(self.num * other.num, self.den * other.den)

こうすると、上の演習のように既約分数が扱えるようにコンストラクタを修正しておくと、 \[ \frac{3}{5} \times \frac{4}{6} = \frac{3\times 4}{5\times 6}=\frac{2}{5} \] と同じく、直接 fraction(3,5) * fraction(6, 4) をprintしてみると 2/5 を表示する。

演習: ratinal クラスに上dで定義した __mul__ メソッドを加えて、実際に rational クラスのインスタンスが記号計算 * で計算できることを確かめなさい。
演習: クラスメソッドの演算の残り __add__, __sub__, __div__ を定義して、rationalクラスを使って、既約有理数計算が分母がゼロにならない限り自由にできることを確認するスクリプトを書きなさい。

rationalクラスの(とりあえずの最後のメソッドとして)、数の浮動小数への変換 float メソッドをオーバーライドしておこう。

    def __float__ (self):
        """Override: float value of  self.
        """
        return  float (self.num ) / float (self.den)

このメソッドは、rationalクラスのインスタンス rational(1,3) に対して、float(rational(1,3)) が浮動小数であるように扱うことを可能にする。

演習: クラス rationalのメソッド を使って、float(1/3) + float(2/3) がどうなるかを確かめなさい。 float(rational(1,10)) + float(rational(2,10)) はどうか。 この結果をどのように考えればよいだろうか。