Skip to content

Latest commit

 

History

History

d04

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

d04: クラス

0.1 + 0.2 はいくつでしょうか? 数学的には当然 0.3 ですね.

実際,pythonで計算してみます.

>>> x = 0.1
>>> y = 0.2
>>> z = x + y
>>> print(z)
0.30000000000000004

なんと,計算結果を保存したzを表示したところ,0.3ではない数値が出力されました.pythonはこんな簡単な計算もできないのです.

しかし,なぜでしょうか?

いちおう,念の為にxに0.1,yに0.2が正しく代入されているか見てみましょう.

>>> print(x)
0.1
>>> print(y)
0.2

ただしく代入されています.うーむ....

正しい答えが求まらない理由は,計算機の世界では全ての値が2進数で表されるからです.

0.1は10進数ですが,2進数で表すとどうなるでしょうか?64ビット浮動小数点で表されます. しかし,64ビットすなわち0と1の桁数が64桁という制限があるため,完全な0.1を表わせません.

(1年後期のプログラミング概論にて,小数の2進数表記について学んでいます.理解できていない人は復習してください) メモリ4ビットで表現できる数値

その証拠に,xyを有効数字17桁で表示してみると,

>>> print(format(x,'.17g'))
0.10000000000000001
>>> print(format(y,'.17g'))
0.20000000000000001

(.17gは有効数字17桁表示の指定)

よって,計算機の中で足し算計算されたzも0.3ちょうどにはならないのです.

>>> print(format(z,'.17g'))
0.30000000000000004

というか,そもそも0.3を計算機では表わすことが不可能です. (.17g というフォーマットは,小数点以下17桁という意味)

>>> print(format(0.3,'.17g'))
0.29999999999999999

まぁ,とは言っても非常に近い数値になっているので通常は気にしません.

ですが,高精度な計算や,比較演算(if x == 0など)では由々しき問題となります.

では,このような小数の数値を完全に正確に表すには,どうすればよいでしょうか.

数値を分数で表わしてはどうでしょう.

0.1を 1/10 だから(1,10),0.2を 1/5 だから(1,5)のように分子と分母の2つの整数値で記憶すれば,完全に正確に表すことができます.

0.1 + 0.2 であっても,分数の足し算,1/10 + 1/5 は分子と分母それぞれを整数の世界で計算できますよね.計算結果も理論的に完全に正確に表せます.

課題k01 (1)

k01パッケージを開発します.

$ poetry new k01`

分子と分母のタプルで表した2つの数値の四則演算を行う関数 Frac_addFrac_devk01フォルダの下のmyfunc.py に定義し, 次のソースファイル k01_1.py を実行せよ.ただし,z1z4は,約分された分子と分母のタプルで表示せよ.

# coding: utf-8
# k01_1.py
from k01 import myfunc as my

x = (1,10) # 0.1
y = (1,5)  # 0.2

z1 = my.Frac_add(x,y)
z2 = my.Frac_sub(x,y)
z3 = my.Frac_mul(x,y)
z4 = my.Frac_dev(x,y)

print(f'{x}+{y}={z1}')
print(f'{x}-{y}={z2}')
print(f'{x}*{y}={z3}')
print(f'{x}/{y}={z4}')

ただし,仮想環境のpythonを使った実行は,次のように行う.

$ poetry run python k01_1.py

また,フォルダk01,ファイルmyfunc.pyk01_1.pyは以下のような階層構造とする.

k01/
├── .venv/
├── test/
│   └── k01_1.py
├── k01/
│   └── myfunc.py
:

提出物:

  • poetry buildによってdist/以下に作成された,whlファイル
  • k01_1.pyの実行した結果を表示したターミナルのスクショ.

約分のしかた

分子と分母の最大公約数を使うと約分ができます.

# coding: utf-8
# myfunc.py
def gcd(p,q):
    """ pとqの最大公約数
    """
    while p % q !=0:
        old_p = p
        old_q = q
        p = old_q
        q = old_p % old_q

    return q

def Frac_reduction(x):
    """ 分数をタプルで表したxを約分して,新しいタプルyに変換
    """
    num = x[0]
    den = x[1]
    Ngcd = gcd(num, den)
    new_num = num//Ngcd
    new_den = den//Ngcd
    y = (new_num, new_den)

    return y

とすると,

>>> from k01 import mufunc as my
>>> x = 2
>>> y = 10
>>> Ngcd = my.gcd(x,y)
>>> print(Ngcd)
2
>>>
>>> a = (x,y) # 分数x/yのつもり
>>> z = my.Frac_reduction(a)
>>> print(z)
(1,5)

タプル表示だと,分数に見えないので

(1)では,計算結果をタプルのまま表示していた

print(f'{x}+{y}={z1}')

だと,(1, 10)+(1, 5)=(3, 10)と表示される.見にくいので,分子と分母のタプルを引数で与えると,「分子/分母」の文字列を返すFrac_str関数をmyfunc.pyに追加し. 次のソースファイル k01_2.py を実行すると,きちんと割り算の形の文字列で表示される

# coding: utf-8
from k01 import myfunc as my

x = (1,10) # 0.1
y = (1,5)  # 0.2

z1 = my.Frac_add(x,y)
z2 = my.Frac_sub(x,y)
z3 = my.Frac_mul(x,y)
z4 = my.Frac_dev(x,y)

print(f'{my.Frac_str(x)} + {my.Frac_str(y)} = {my.Frac_str(z1)}')
print(f'{my.Frac_str(x)} - {my.Frac_str(y)} = {my.Frac_str(z2)}')
print(f'{my.Frac_str(x)} * {my.Frac_str(y)} = {my.Frac_str(z3)}')
print(f'{my.Frac_str(x)} / {my.Frac_str(y)} = {my.Frac_str(z4)}')

Frac_str関数は,以下の様に書ける.

def Frac_str(x):
    num = x[0]
    den = x[1]
    s = '(' + str(num) + '/' + str(den) + ')' # str関数は数値を文字列で変換する関数.
                                              # 文字列に対する'+'演算子は文字列をつなげる
    return s

クラス

そんなわけで,分数をタプルで表して,計算も表示もできるようになったのだが,「なんかソースコードが汚い!」

z1 = my.Frac_add(x,y)
z2 = my.Frac_sub(x,y)
z3 = my.Frac_mul(x,y)
z4 = my.Frac_dev(x,y)

print(f'{my.Frac_str(x)} + {my.Frac_str(y)} = {my.Frac_str(z1)}')
print(f'{my.Frac_str(x)} - {my.Frac_str(y)} = {my.Frac_str(z2)}')
print(f'{my.Frac_str(x)} * {my.Frac_str(y)} = {my.Frac_str(z3)}')
print(f'{my.Frac_str(x)} / {my.Frac_str(y)} = {my.Frac_str(z4)}')

と,書いていたけれど,次のように,分数専用の関数でなく,これまで使ってきた普通の演算子使ったり,print関数使ったりしても,きちんと計算できたらよいじゃないか?

z1 = x + y
z2 = x - y
z3 = x * y
z4 = x / y

print(f'{x} + {y} = {z1}')
print(f'{x} - {y} = {z2}')
print(f'{x} * {y} = {z3}')
print(f'{x} / {y} = {z4}')

そういえば,変数に対する演算子の作用は,型タイプによって違う.xyの型によって,演算子+が作用した結果x+yが異なる.

>>> x = int(1) # xは整数 1
>>> y = int(2)
>>> z = x+y
>>> print(z)
3
>>> x = float(1) # xは浮動小数点 1.0...
>>> y = float(2)
>>> z = x+y
>>> print(z)
3.0
>>> x = str(1) # xは文字列 '1'
>>> y = str(2)
>>> z = x+y # 文字列と文字列の'+'は文字列の連結
>>> print(z)
12

ということで,分数の型タイプを自作し,加減乗除の算術演算子が課題k01(1)で作った分数用の加減乗除関数に入るようにすればよい.

この型タイプをpythonでは「クラス」という.type(変数)で表示される.

>>> x = float(1)
>>> type(x)
<class 'float'>

クラスは,数値や文字列だけでなく,あらゆる変数に設定される.

>>> x = turtle.Turtle()
>>> type(x)
<class 'turtle.Turtle'>

また,変数xをクラス<class 'foo'>に設定する場合に,x = foo(...)というようにクラス名を関数のようにして使う.

クラス定義のしかた

分数クラス <class 'Frac'>を作ってみよう.

class Frac():
    def __init__(self, num, den = 1):
        """ 変数をクラスに設定するときの処理.すなわち`変数 = Frac(...)`としたときの処理
        """
        self.num = int(num)
        self.den = int(den)

    def __str__(self):
        """ クラス変数を文字列に変換する処理.すなわち`str(変数)`としたときの処理
        """
        s = '(' + str(self.num) + '/' + str(self.den) + ')'
        return s

    def __add__(self, arg):
        """ クラス変数とargとの'+'演算子の処理.すなわち`変数 + arg'としたときの処理
        """
        den = self.den * arg.den
        num = self.num * arg.den + self.den * arg.num
        ret = Frac(num,den)
        return ret

    def foo(self,arg):
        """ クラス変数に対するfoo関数の処理.`変数.foo(arg)`としたときの処理
        """
        なんとかかんとか

    owner = 'kotaro'

クラスの構造について説明すると,

  • class 自作クラス名(親クラス名):
    • 親クラスとは,テンプレートとなるクラスのことで,コピー元である.
    • 作られたクラスは「子クラス」と言う.子クラスは親クラスの中で定義される関数や定数をすべて引き継ぎ,さらに上書きしたり,新たな関数や定数の定義を加えたりすることができる.
    • いまの場合は,親クラス名を書いていないので,親無しである.
  • def foo(self,arg):
    • 変数xがクラス<class 'Frac'>に設定されているとき,x.foo(arg)で呼び出される関数の定義で,雰囲気的には,foo(x, arg).このようなドット関数を 「メソッド」 と呼ぶ.クラスの属性関数,クラス関数,クラスメソッドなどと呼ぶ.
    • 仮引数にあるselfは予約語(勝手に使えない変数名)で,メソッドにおけるドットの前にある変数名を表す.
  • owner = 'kotaro'
    • クラスでは定数を定義できる.x.ownerで呼び出せる.クラスの属性定数,クラス定数などと呼ばれる.
      >>> print(x.owner)
      'kotaro'
      
  • def __init__(self,num,den):
    • __(アンダーバー2連続,「ダンダー」)で囲まれた名前は予約語.__init__は変数をクラスに設定する特別関数名であり, 「コンストラクタ(constructor)」 と呼ばれる.
    • __init__(self,arg,...)は,x = クラス名(arg,...)としたときの処理を行う関数 となっている.計算処理も行えるが,クラス属性変数を定めるのが主な役目.
  • def __str__(self):
    • __で囲まれている予約語であり,__str__は,変数をどのような文字列に変換するかを決める特別関数名.
    • x.__str__()str(x)と同じ意味である.
  • def __add__(self, arg):
    • __で囲まれている予約語であり,__add__+演算子の処理を定義している特別関数名.
    • x.__add__(y)が呼ばれると__add__(self=x, arg=y)が作動する.
    • x.__add__(y)x + yと全く同じ意味.

課題k01(1)では,

>>> x = (1,10)

として,分子と分母のタプルで分数を表していたが,変数が2つの値のペアを記憶していればよいので,分数クラスに設定する際に,クラス変数,x.numx.denにそれぞれ分子と分母を記憶することにする.

>>> from k01 import myclass as my
>>> x = my.Frac(1,10)
>>> print(x.num)
1
>>> print(x.den)
2

クラス定義をフォルダk01の下のmyclass.pyに定義することにより,分数クラスに設定したxyに対し,以下の処理が行えるようになる.

from k01 import myclass as my

x = my.Frac(1,10)
y = my.Frac(1,5)

z = x + y

print(f'{x} + {y} = {z}')

課題k01 (2)

上記のままのクラス定義だと

>>> print(f'{x}+{y}={z}')
(1/10) + (1/5) = (15/50)

のように出力され,答えが約分された「(3/10)」と表示されない.約分された表示がされるようclass Frac()を修正せよ.

また,上記のままのクラス定義だと,加減乗除のうち,加算しか対応できていない.

class Frac()__sub____mul___,__truediv__メソッドを加えることで,-, *, /演算子にも対応できるようにせよ.

次のソースファイル k01_2.py を実行した結果を提出せよ.z1z4は約分された表現とする.

ただし,k01_2.pyおよび,myclass.pyの配置は以下の通りとする.

k01/
├── test/
│   ├── k01_1.py
│   └── k01_2.py
├── k01/
│   ├── myfunc.py
│   └── myclass.py
:
# k01_2.py
from k01 import myclass as my

x = my.Frac(1,10)
y = my.Frac(1,5)

z1 = x + y
z2 = x - y
z3 = x * y
z4 = x / y

print(f'{x}+{y}={z1}')
print(f'{x}-{y}={z2}')
print(f'{x}*{y}={z3}')
print(f'{x}/{y}={z4}')

提出物:

  • poetry builddist以下に作成された whl ファイル
  • k01_2.pyの実行結果を表示したターミナルのスクショ

k01提出期限

初回提出: 5/17

最終提出: 6/2

提出物の採点について,

採点はTA(時間給アルバイト)が行っています.授業時間外の採点業務は,「時間外業務」なので果てしなくブラックなグレーで,サービスになります.

よって,提出は期限までいつでも可能ですが,授業時間外では採点が提出後にすぐ行われるとは考えないでください.

そんなわけで,おすすめはLACSに提出したら授業時間中にTAの誰かに提出物を見せてください.

TAによって担当学生が決まっていますので,TAからはLACSの提出物が見えません.自分のPCをTAに見せてください. TAはその場で採点するなり,コメントするなりしてくれます. (TAは,見た学生の番号と採点した場合は点数を,授業後にまとめて私に教えてください.) 授業後にLACSの成績に点数を反映させます.