データ抽象 Data Abstraction
<データのタイプについて>
データの種類のことを型(type)という。
1、2、3…などの数値は数値型、'hello'などの文字は文字列型などのようにそれぞれ分類されている。
違う型同士での計算はPythonでは出来ない(JavaScript、PHPなど他の言語では可能)ので、型を揃えて操作する必要がある。
そのデータの型が何なのかを知るのに、type()が使える。
>>> type(3)
<class 'int'>
>>> type(sum)
<class 'builtin_function_or_method'>
>>> type(0.5)
<class 'float'>
>>>
数値型で気をつけておきたいのが、整数型(int)と浮動小数型(float)では微妙に異なる点がある。それは整数型は誤差なしの値であるのに対して、浮動小数型は曖昧な(誤差ありの)値になるということ。
>>> 1/3 == 0.33 # 誤差が大きいのでFalseになる
False
>>> 1/3 == 0.3333333333333333333333 # 誤差が小さいのでTrueになる
True
>>> 1/3
0.3333333333333333
>>>
<データ抽象>
データ抽象とは実装に依存せずにデータ構造を抽象的に定義すること。
データの具体的な表現を示さず、関数だけを示している。これを抽象データ型(Abstract Data Type, ADT)という。
例として、分数を表すプログラムを考える。
通常では分数表現を書いた場合、Pythonは先に計算をしてしまうので分数を表示することが出来ない。なので、これを表示するために3つの関数を考える。
- rational(n, d) 分子n、分母dの分数を返す.
- numer(x) 分数xの分子を返す.
- denom(x) 分数xの分母を返す.
これらの定義を用いて、分数の足し算、掛け算を定義すると、
>>> def add_rationals(x, y):
nx, dx = numer(x), denom(x)
ny, dy = numer(y), denom(y)
return rational(nx * dy + ny * dx, dx * dy)
>>> def mul_rationals(x, y):
return rational(numer(x) * numer(y), denom(x) * denom(y))
また、分数を表示する関数は、
>>> def print_rational(x):
print(numer(x), '/', denom(x))
と書くことができる。
ここで、リストを用いてrational, numer, denomを定義することができる。
def rational(n, d):
return [n, d]
def numer(x):
return x[0]
def denom(x):
return x[1]
>>> half = rational(1, 2)
>>> print_rational(half)
1 / 2
>>> third = rational(1, 3)
>>> print_rational(mul_rationals(half, third))
1 / 6
>>> print_rational(add_rationals(third, third))
6 / 9
しかし、6 / 9は本当は2 / 3と表示されるべきである。
そこで、Pythonにもともと組み込まれているgcd(greatest common denominator、分子と分母の最大公約数)をimportしてrational関数を少し変える必要がある。
>>> from fractions import gcd
>>> def rational(n, d):
g = gcd(n, d)
return (n//g, d//g) # 分子と分母をそれぞれ最大公約数で割る
>>> print_rational(add_rationals(third, third)) 2 / 3
このデータ抽象の概念を使えば、関数の特徴をいじることなく色々な表現をすることができる。その状態を保つために、abstraction barrierという概念がある。
コードを書くとき、一般化されたコードを書くことで、他の関数の実装に関係なく求めたいものを求められるように定義する必要があるという事。
先ほどの分数の例でいうと、例えば分数の2乗を計算するプログラムを書くとする。
>>> def square_rational(x):
return mul_rational(x, x)
これなら分数がどのように実装されていても関係なく求めたい結果を求められる。
しかし、次の例はabstraction barrierに違反している。
>>> def square_rational_violating_once(x): return rational(numer(x) * numer(x), denom(x) * denom(x))
こうしてしまうと、もしnumerやdenomの実装方法が変わってしまった場合、分数の2乗を正しく計算できなくなる可能性がある。
次の例も同様で、
>>> def square_rational_violating_twice(x): return [x[0] * x[0], x[1] * x[1]]
他の関数の実装方法に依存した書き方のため、リストの中身が変わってしまったりすると正しく計算できなくなる。
Abstraction barrierとは、こういった他の実装方法に依存されない方法で関数を書きましょうといったルールのようなものである。
そこで、一般化されたプログラムを書くには、コンストラクタとセレクタを使って書く必要がある。
コンストラクタ、セレクタの説明の前に、オブジェクトとクラスについて。
オブジェクトとは、変数と関数が一緒になっているもの。
例:
x = 2 (変数)
def square(x):
return x * x (処理をする関数)
これらを一まとめにしたものがオブジェクト。
オブジェクトにまとめられている関数をメソッドと呼ぶ。
オブジェクトのメリットとして、変数をどの関数で処理するかが明確になるのでバグが出にくいこと、再利用や分類がしやすいことなどが挙げられる。
クラスとは、オブジェクトの設計図でのことで、オブジェクトがどのような性質を持っていて、どのような振る舞いをするのか、ということをコードに書いてクラスという形にまとめることで様々なオブジェクトを生み出すことができる。
今回の分数の例の場合、rationalがコンストラクタ、numer、denomがセレクタとなる。
詳しくはまた別の記事で。