↓ メインコンテンツへスキップ
  1. 第07講 クラス(2/2)/

第07講 週次課題

課題07-1 点と線分
#

難易度 ⭐⭐

点(Point)と線分(Line)を表す2つのクラスを作成してください。

  • Point クラスは、2次元平面上の点を表すクラスです。
    • x 座標と y 座標を持ち、コンストラクタで初期化します。
    • 他のPointオブジェクトからの距離を計算するメソッド distance を持ちます。
      • 他のPointオブジェクトが渡されなかった場合は、原点 (0, 0) からの距離を計算します。
    • __str__ メソッドでは、f"({self._x}, {self._y})" という文字列表現を返します。
  • Line クラスは、2つの Point オブジェクトからなる線分を表すクラスです。
    • 2つの Point オブジェクトを持ち、コンストラクタで初期化します。
    • 線分の長さを計算するメソッド distance を持ちます。
    • __str__ メソッドでは、f"({self._p1}, {self._p2})" という文字列表現を返します。
    • is_on メソッドは、引数に Point オブジェクトを受け取り、その点が線分上にあれば True を返し、そうでなければ False を返します。
      • 線分は両端の点を含みます。端点と一致する点は線分上にあるものとします。
      • 2つの端点が同じ点(長さ 0 の線分)のときは、その点と一致する点だけが線分上にあるものとします。

is_on の判定方法
#

点 \(P\) が線分 \(AB\) 上にあるかどうかは、次の 2つの条件を両方 満たすかどうかで判定します。

  1. \(P\) が \(A\)、\(B\) を通る 直線 の上にある。
  2. \(P\) が \(A\) と \(B\) の 間 にある。すなわち、\(x\) 座標、\(y\) 座標のそれぞれについて、\(A\) と \(B\) の座標の小さい方以上、大きい方以下である。

条件 1 だけでは、線分を延長した直線上の点(例えば線分 \((1, 1)\)〜\((5, 5)\) に対する \((0, 0)\))も線分上と判定してしまいます。

条件 1 は、\(A = (x_1, y_1)\)、\(B = (x_2, y_2)\)、\(P = (x, y)\) として、 \((x_2 - x_1)(y - y_1) - (y_2 - y_1)(x - x_1)\) が \(0\) かどうかで判定できます(\(AB\) と \(AP\) が平行なら \(0\) になります)。 傾きを求めて \(y = ax + b\) に代入する方法でも構いませんが、その場合は \(x_1 = x_2\) となる垂直な線分で 0 割りになるので、別に扱う必要があります。

ただし、浮動小数点数(float)の計算には誤差があり、本来 \(0\) になるはずの値が \(0\) ちょうどにならないことがあります(例えば 0.1 * 3 - 0.3 は 0 ではなく 5.55e-17 になります)。 そのため、求めた値 got が 0 かどうかは got == 0 ではなく、abs(got) < 0.00001 のように誤差を許容して判定してください。

以下のプログラム(assignment07-1.py)を実行したときに、テストが全て通るように plane_figures.py を作成してください。

import plane_figures as pf

p1 = pf.Point(3, 4)
assert abs(p1.distance() - 5.0) < 0.00001, "原点からの距離が正しくありません"
assert abs(p1.distance(p1)) < 0.00001, "同じ点からの距離が正しくありません"
assert str(p1) == "(3, 4)", "文字列表現が正しくありません"

p2 = pf.Point(1, 1)
assert abs(p2.distance() - 2 ** 0.5) < 0.00001, "原点からの距離が正しくありません"

l1 = pf.Line(p2, pf.Point(5, 5))
assert abs(l1.distance() - 32 ** 0.5) < 0.00001, "線分の長さが正しくありません"
assert str(l1) == "((1, 1), (5, 5))", "文字列表現が正しくありません"
assert l1.is_on(pf.Point(3, 3)), "点 (3, 3) は線分上にあるはずです"
assert l1.is_on(pf.Point(1, 1)), "端点 (1, 1) は線分上にあるはずです"
assert l1.is_on(pf.Point(5, 5)), "端点 (5, 5) は線分上にあるはずです"
assert not l1.is_on(pf.Point(0, 0)), "点 (0, 0) は直線上にはありますが、線分の外なので線分上にはないはずです"
assert not l1.is_on(pf.Point(6, 6)), "点 (6, 6) は直線上にはありますが、線分の外なので線分上にはないはずです"
assert not l1.is_on(pf.Point(2, 3)), "点 (2, 3) は線分上にないはずです"

l2 = pf.Line(pf.Point(2, 0), pf.Point(2, 4))  # 垂直な線分
assert l2.is_on(pf.Point(2, 2)), "点 (2, 2) は垂直な線分上にあるはずです"
assert not l2.is_on(pf.Point(2, 5)), "点 (2, 5) は垂直な線分の外なので線分上にはないはずです"
assert not l2.is_on(pf.Point(3, 2)), "点 (3, 2) は垂直な線分上にないはずです"

l3 = pf.Line(pf.Point(0, 0), pf.Point(1, 3))  # 浮動小数点の誤差が出る例
assert l3.is_on(pf.Point(0.1, 0.3)), "点 (0.1, 0.3) は線分上にあるはずです(誤差を許容していますか)"

l4 = pf.Line(pf.Point(1, 1), pf.Point(1, 1))  # 長さ 0 の線分
assert l4.is_on(pf.Point(1, 1)), "長さ 0 の線分上にあるのは端点だけです"
assert not l4.is_on(pf.Point(2, 2)), "点 (2, 2) は長さ 0 の線分上にないはずです"

課題07-2 乱数ジェネレータ
#

難易度 ⭐⭐

以下の特徴を持つ乱数生成クラス MyRandom(my_random.py)を作成してください。

  • MyRandom クラスは、線形合同法(Linear Congruential Generator; LCG)による乱数生成器です。
    • 線形合同法は \(X_{n+1} = (a * X_n + c) \mod m\) という漸化式で表されます。
      • 上記の a は乗数(multiplier)、c は加数(increment)、m は剰余(modulus)と呼びます。
  • コンストラクタは、seed と modulus と multiplier と increment を受け取り、初期化します。
    • seed は乱数の初期値です。デフォルト値は 0 です。
    • modulus は剰余演算に使う値です。デフォルト値は 2 ** 32 です。
    • multiplier は乗算に使う値です。デフォルト値は 1664525 です。
    • increment は加算に使う値です。デフォルト値は 1013904223 です。
  • next メソッドは、次の0以上1未満の乱数を生成し、返します。
    • 漸化式の \(X_n\) には直前に生成した値を使います(最初は seed)。生成した \(X_{n+1}\) は、次の呼び出しのためにオブジェクトに保存しておく必要があります。
    • 得られた\(X_{n+1}\)は\(0\)以上\(m\)未満の整数となりますので、範囲を \(0\)以上\(1\)未満に変換するために、\(X_{n+1} / m\) を返します。
    • 引数 skip を指定すると、skip 個の乱数を読み飛ばしてから、その次の乱数を返します。 例えば next(2) は乱数を 3 個生成し、最初の 2 個を捨てて 3 個目を返します。next() と next(0) は同じ意味です。
class MyRandom:
    def __init__(self): # 引数(seed、modulus, multiplier, increment)を追加してください。
        pass            # seed, modulus, multiplier, increment はデフォルト引数を持つようにしてください。

    def next(self, skip = 0): # skip 個の乱数を読み飛ばしてから、次の乱数を返します。
        for _ in range(skip):
            self._next_impl()
        return self._next_impl()

    def _next_impl(self):
        pass        # 線形合同法により X_{n+1} を求めて保存し、X_{n+1} / m を返してください。

以下のテストコード(assignment07-2.py)を通るようにしてください。

import my_random as mr

rg = mr.MyRandom(0, 2 ** 32, 1664525, 1013904223)
r1 = rg.next()
r2 = rg.next()
assert abs(r1 - 0.236067972844467) < 0.000001, "1個目の乱数が正しくありません"
assert abs(r2 - 0.278566908556968) < 0.000001, "2個目の乱数が正しくありません(next のたびに X_n を更新していますか)"
assert abs(rg.next(2) - 0.384077370865270) < 0.000001, "3, 4個目を読み飛ばして、5個目の乱数を返すはずです"
assert abs(rg.next() - 0.621807487215847) < 0.000001, "6個目の乱数が正しくありません"

rg2 = mr.MyRandom()  # デフォルト引数は上と同じ値なので、同じ乱数列になる。
assert abs(rg2.next() - r1) < 0.000001, "デフォルト引数が正しくありません"

rg3 = mr.MyRandom(seed = 42)
assert abs(rg3.next() - 0.252345174783841) < 0.000001, "seed を変えると別の乱数列になるはずです"

乱数の期待値との比較は abs(得られた値 - 期待値) < 0.000001 のように、差の 絶対値 で行っています。 得られた値 - 期待値 < 0.000001 と書くと、得られた値が期待値より小さければ何でも通ってしまう(例えば常に 0 を返す実装でも通る)ので注意してください。

課題07-3 ベクトル
#

難易度 ⭐⭐⭐

次のプログラム(assignment07-3.py)を実行したときに、テストが全て通るように vector.py を作成してください。 vector.py 中で Vectorクラスを定義し、以下のメソッドを用意してください。

  • __init__ コンストラクタ。n個の引数(可変長)を受け取り、ベクトルを表す。
  • __add__ ベクトル同士の足し算を計算し、新しいベクトルを返す。
  • __mul__ ベクトル同士の内積を計算し、スカラー値を返す。
  • __getitem__ ベクトルの要素を取得する。
  • __len__ ベクトルの長さを返す。
  • __str__ ベクトルの文字列表現を返す。
  • norm ベクトルのノルム(大きさ)を計算し、返す。
import vector as v

v1 = v.Vector(3, 4, 5)
v2 = v.Vector(1, 2, 3)

v3 = v1 + v2
assert v3[0] == 4 and v3[1] == 6 and v3[2] == 8, "ベクトルの足し算が正しくありません"
assert v1 * v2 == 26, "ベクトルの内積の計算が正しくありません"
assert str(v1) == "(3, 4, 5)", "ベクトルの文字列表現が正しくありません"
assert len(v1) == 3, "ベクトルの長さが正しくありません"
assert v1.norm() == 50 ** 0.5, "ベクトルのノルム(大きさ)の計算が正しくありません。"

v4 = v.Vector(1, 1, 1, 1, 1)
assert len(v4) == 5, "ベクトルの長さが正しくありません"
assert v4.norm() == 5 ** 0.5, "ベクトルのノルム(大きさ)の計算が正しくありません。"
assert v4 + v1 == None, "要素数の異なるベクトル同士の足し算は計算できないので、None が返るはずです。"
assert v1 * v4 == None, "要素数の異なるベクトル同士の内積は計算できないので、None が返るはずです。"

なお、リストを可変長引数として受け取る方法は以下の通りです。

>>> list = [1, 2, 3]
>>> v1 = Vector(list)  # これは、[1, 2, 3] という1つの要素を持つベクトルになってしまう。
>>> v1.elements
([1, 2, 3],)           # ベクトルの要素は [1, 2, 3] という値一つ。
>>> v2 = Vector(*list) # このようにリスト変数の前に * をつけると、リストの要素を展開して引数として渡すことができる。
>>> v2.elements
(1, 2, 3)