5. オブジェクト指向への第一歩

この章と次の章で,以下のように放物運動をする質点の運動学シミュレーションを実装します.この例を通して,この章ではクラスの定義とオブジェクト指向プログラミングの基本的な考え方を学びます.

  • 左クリック: 画面境界で消える質点を投てき
  • 右クリック: 画面境界で跳ね返る質点を投てき
  • ESCキー: 終了

5.1. 枠組みだけを作る

まずは,プログラム全体の枠組みを作っておきます.真っ黒のウィンドウを開いて,60 frames/s でループを回し, pygame.QUIT イベントもしくは Esc キーで止められるようにしておきます.

projects フォルダの中に particle フォルダを作成し,VSCode でそのフォルダを開いた状態にしてください.そこに particle.py というファイルを作成し開いてください.VSCode のタイトルバーは particle.py - particle - VSCodium (Computer Seminar I) になるはずです.

注意

そろそろ忘れているかもしれないので再度の注意です. Open File (ファイルを開く) や Open Workspace (ワークスペースを開く) ではなく,必ず Open Folder (フォルダを開く) を使ってください.

また,開くフォルダは C:/cs1/projects/particle です. C:/cs1/projects ではありません.

particle.py に以下の内容を入力してください.

particle.py (ver 1.0)
 1import pygame
 2
 3
 4def main():
 5    pygame.init()
 6    width, height = 600, 400
 7    screen = pygame.display.set_mode((width, height))
 8    clock = pygame.time.Clock()
 9
10    while True:
11        frames_per_second = 60
12        clock.tick(frames_per_second)
13
14        should_quit = False
15        for event in pygame.event.get():
16            if event.type == pygame.QUIT:
17                should_quit = True
18            elif event.type == pygame.KEYDOWN:
19                if event.key == pygame.K_ESCAPE:
20                    should_quit = True
21        if should_quit:
22            break
23
24        screen.fill(pygame.Color("black"))
25        pygame.display.update()
26
27    pygame.quit()
28
29
30if __name__ == "__main__":
31    main()

前章までの内容を理解していれば,特に説明の必要はないと思います.この章では関数を切り出すのとは別のアプローチで抽象化するため,まずは main 関数1個にすべてを入れておきました.

Source Control サイドバーを開いて Git リポジトリを初期化して,ファイルの内容をコミットしておいてください.以降,これまで通り,キリのいいところで適宜コミットしておくようにしてください.

5.2. 放物運動

加速度 \(\boldsymbol{a}(t)\) を受ける質点の速度 \(\boldsymbol{v}(t)\) と位置 \(\boldsymbol{x}(t)\) は以下のように書けます.

\[\begin{split}\frac{d \boldsymbol{v}}{dt} &= \boldsymbol{a}\\ \frac{d \boldsymbol{x}}{dt} &= \boldsymbol{v}\end{split}\]

コンピュータでシミュレートするため,時間を \(\Delta t\) ごとに離散化して以下のように近似します.

\[\begin{split}\Delta \boldsymbol{v} &= \boldsymbol{a} \Delta t\\ \Delta \boldsymbol{x} &= \boldsymbol{v} \Delta t\end{split}\]

マウスの左ボタンクリック位置を初期位置とし,初期速度を乱数で発生させることにして, y 軸方向を重力方向と見立てて,一定の加速度がかかっている場合の計算をしてみます.

乱数の生成には Python 標準の random.uniform 関数を使うことにします. random.uniform(a, b)a から b の範囲の一様乱数を生成します.

particle.py (ver 2.0)
 1import random
 2import pygame
 3
 4
 5def main():
 6    pygame.init()
 7    width, height = 600, 400
 8    screen = pygame.display.set_mode((width, height))
 9    clock = pygame.time.Clock()
10
11    dt = 1.0
12    gy = 0.5
13    particle_is_alive = False
14    x, y = 0, 0
15    vx, vy = 0, 0
16
17    while True:
18        frames_per_second = 60
19        clock.tick(frames_per_second)
20
21        should_quit = False
22        for event in pygame.event.get():
23            if event.type == pygame.QUIT:
24                should_quit = True
25            elif event.type == pygame.KEYDOWN:
26                if event.key == pygame.K_ESCAPE:
27                    should_quit = True
28            elif event.type == pygame.MOUSEBUTTONDOWN:
29                if event.button == 1:
30                    particle_is_alive = True
31                    x, y = event.pos
32                    vx = random.uniform(-10, 10)
33                    vy = random.uniform(-10, 0)
34        if should_quit:
35            break
36
37        if particle_is_alive:
38            vy += gy * dt
39            x += vx * dt
40            y += vy * dt
41
42        screen.fill(pygame.Color("black"))
43        if particle_is_alive:
44            radius = 10
45            pygame.draw.circle(screen, pygame.Color("green"), (x, y), radius)
46        pygame.display.update()
47
48    pygame.quit()
49
50
51if __name__ == "__main__":
52    main()
1行目
random モジュールをインポートします.
13-15行目
最初に左クリックするまでは質点は存在しないことにします. particle_is_alive という変数を用意し,これが False のときは運動計算も描画もしないことにします.位置座標 x, y, 速度 vx, vy も,適当な値で初期化しておきます.
28-33行目
type 属性が pygame.MOUSEBUTTONDOWN であるような Event 型オブジェクトを検出します.このとき,Event 型オブジェクトの button 属性にマウスボタン番号が入っています.左ボタンは番号 1 です.
37-40行目,43-45行目
運動計算と描画処理を particle_is_aliveTrue のときだけ行います.
あれ? 前回は左ボタンは 0 だったのでは?
はい,これは pygame の変なところですね.

前回 0 だったのは pygame.mouse.get_pressed が返すシーケンスへのインデックスです.Python のシーケンスのインデックスだから 0 から始まって,左ボタンが 0,(もしあれば) 中ボタンが 1,右ボタンが 2 です.

一方,今回のはインデックスではないので 0 から始まるという縛りがない…からなのかどうかはわかりませんが,左ボタンが 1,中ボタンが 2,右ボタンが 3 です.迷惑.

速度と位置の計算順序おかしくないですか?
これは意図的です.シンプレクティック積分法 (symplectic integrator) と呼ばれます.

運動方程式の積分を離散時間で行う方法にはいろいろあり,それぞれ誤差の生じ方が違います.長時間の計算を行う際は,解が時間ともに発散してしまわないことは特に重要です.Symplectic 積分法は,簡単な割にエネルギーが増大しにくい方法として知られています.

Symplectic 法の要点は,先に速度を更新し,その更新された速度を用いて位置の更新をすることです.同じことですが,先に位置を更新してからその位置で力の発生を評価し,その力によって生じる加速度で速度を更新していると解釈することもできます.(なお,今の例では力は重力加速度のみで常に一定なので,あまりご利益はありません.今後の拡張で効いてきます)


左ボタンクリックを短時間で繰り返してもらうとわかりますが,左クリックをすると現在の質点が消えて,新しいのが生まれます.ちょっと不自然ですね.現在のものが画面外に出るまでは新しいものを作れないようにします.

particle.py (ver 3.0)
21        should_quit = False
22        for event in pygame.event.get():
23            if event.type == pygame.QUIT:
24                should_quit = True
25            elif event.type == pygame.KEYDOWN:
26                if event.key == pygame.K_ESCAPE:
27                    should_quit = True
28            elif event.type == pygame.MOUSEBUTTONDOWN:
29                if event.button == 1 and not particle_is_alive:
30                    particle_is_alive = True
31                    x, y = event.pos
32                    vx = random.uniform(-10, 10)
33                    vy = random.uniform(-10, 0)
34        if should_quit:
35            break
36
37        if particle_is_alive:
38            vy += gy * dt
39            x += vx * dt
40            y += vy * dt
41            if x < 0 or x > width or y > height:
42                particle_is_alive = False
43
44        screen.fill(pygame.Color("black"))

if 文の条件に使われる真偽値の論理演算には,演算子 and, or, not を使うことだけ思い出せば,特に難しいことはないはずです.


これをさらに発展させて,複数の質点を同時に動かすことを考えます.

すぐに思いつくのは x, y, vx, vy, particle_is_alive をそれぞれリストにする方法です.例えば k 番目の質点の運動計算は以下のようになるでしょう:

vy[k] += gy * dt
x[k] += vx[k] * dt
y[k] += vy[k] * dt

これはこれで可能なのですが,質点が頻繁に増えたり減ったりする場合には,すべてのリストの要素を append したり del したりする必要があって管理が煩雑です.うっかり一つ忘れたりすると (例えば particle_is_alive だけ append し忘れたりすると),変数間でインデックスがずれてしまい,原因追求が難しいバグにつながります.

それよりも,質点 1 個をひとまとまりのデータとして扱って,xvx などは「そこに所属するもの」とするのが合理的です.以降ではそのような方針を取ります.

5.3. データをまとめる

要するにこれからやるのは xvx といった属性を持つオリジナルの型のオブジェクトを作ることです.オブジェクトの型,つまりそのオブジェクトがどんな属性を持ちどんな操作が可能なのかを定義する仕組みをクラス (class) と呼びます.

質点を表す Particle クラスを実際に定義してみましょう.まずは質点 1 個だけのままです.

particle.py (ver 4.0)
 1import random
 2import pygame
 3
 4
 5class Particle:
 6    pass
 7
 8
 9def main():
10    pygame.init()
11    width, height = 600, 400
12    screen = pygame.display.set_mode((width, height))
13    clock = pygame.time.Clock()
14
15    dt = 1.0
16    gy = 0.5
17    p = Particle()
18    p.is_alive = False
19    p.x, p.y = 0, 0
20    p.vx, p.vy = 0, 0
21
22    while True:
23        frames_per_second = 60
24        clock.tick(frames_per_second)
25
26        should_quit = False
27        for event in pygame.event.get():
28            if event.type == pygame.QUIT:
29                should_quit = True
30            elif event.type == pygame.KEYDOWN:
31                if event.key == pygame.K_ESCAPE:
32                    should_quit = True
33            elif event.type == pygame.MOUSEBUTTONDOWN:
34                if event.button == 1 and not p.is_alive:
35                    p.is_alive = True
36                    p.x, p.y = event.pos
37                    p.vx = random.uniform(-10, 10)
38                    p.vy = random.uniform(-10, 0)
39        if should_quit:
40            break
41
42        if p.is_alive:
43            p.vy += gy * dt
44            p.x += p.vx * dt
45            p.y += p.vy * dt
46            if p.x < 0 or p.x > width or p.y > height:
47                p.is_alive = False
48
49        screen.fill(pygame.Color("black"))
50        if p.is_alive:
51            radius = 10
52            pygame.draw.circle(screen, pygame.Color("green"), (p.x, p.y), radius)
53        pygame.display.update()
54
55    pygame.quit()
56
57
58if __name__ == "__main__":
59    main()
5-6行目

Particle という名前のクラスを定義しています.クラス名の後のコロン : を忘れないようにしてください.Python にも慣れてきたと思いますので,コロンが出てきたら,次の行からは 1 レベルインデントして内容を書くんだな,と予測できると思います.その通りです.

…その通りなのですが,実はこの例ではここに書くべき「内容」がありません.Python では,このように文法上何かを書かなくてはならないのに書くべきものがない場合, pass 文を置きます.何もしない文でした.

内容が何もないというのは奇妙に思えるかも知れません.しかしここでは Particle という名前のクラスを作ったという事実だけで十分です.

17-20行目

p = Particle() と書くことで,Particle 型のオブジェクトを生成し,変数 p に割り当てます.一般に,クラス名の後ろに () をつけて関数として呼び出すと,その型のオブジェクトを生成できます.

続いて, p が指すオブジェクトの属性 x, y, vx, vy, is_alive を定義します.ここで定義するから,6 行目は pass だけでよかったわけです.

Particle 型のオブジェクトのことを,Particle 型のインスタンス (instance),クラス Particle のインスタンスなどとも呼びます.オブジェクトを生成することをインスタンス化する (instantiate) ともいいます.

34-38, 42-47行目
変数 p の属性を使って初期位置・速度設定,運動計算,描画の処理を行います.
あれ? 前の章までに出てきた pygame.Color 関数とか pygame.time.Clock 関数とかってもしかして…?
はい,クラス名の後ろに () をつけて,オブジェクトを生成しているのでした.Color の方は () の中に引数を取りましたが,その仕組みの説明もこの後すぐに出てきます.
C言語の構造体に似ているなと思ったのですが,構造体だと定義するときに x とか vx とかのメンバ変数を列挙しますよね.Python のクラスではどうして pass で済んじゃうんですか?
クラスは確かに構造体に似ています.

C言語では

struct Particle {
    float x;
    float y;
    float vx;
    float vy;
    int is_alive;
};

と書くと struct Particle という名前の新しい型を作ることができ,その型のデータはメンバ変数 x, y, vx, yv, is_alive を持つようになります.クラスというのは,メンバとして変数のほかに関数 (メソッド) を持てるように構造体を拡張したもので,C++ や Java など他の言語にも用意されています.

C, C++, Java 等では,変数はあらかじめ宣言する必要がありますので,構造体やクラスの定義時にメンバ変数を宣言しなくてはなりません.これに対して Python では変数の宣言は必要ないので,クラスを定義する時点で変数を列挙しておく必要はありません.

もう少し後で,クラス内に関数 (メソッド) を定義します.関数については Python でも使用前に定義しておかないといけないので, pass するわけにはいきません.

変数名や関数名と違って Particle のように語頭を大文字にするんですね?
はい,Python のクラス名のつけ方の慣習です.複数単語なら UpperCamelCase のように書きます.

このような書き方を UpperCamelCase または PascalCase などと呼びます.なお,先頭だけを小文字にするものは lowerCamelCase と呼ばれます.(単に camel case といったときどちらを指すかは人によるようです)

オブジェクトとインスタンスは同じ意味ですか?
多くの場合ほぼ同じような意味で使われますが,微妙に意味合いが違います.

そもそもインスタンスとは,実例,具体例の意味です.なので「x はインスタンスです」という文は意味をなしません (「何の実例だよ!」という反応を招きます).「x はクラス A のインスタンスです」などとして初めて意味をなします.

一方,「x はオブジェクトです」という文はそれ自体で意味をなし得ます.Python の場合は,「ああ,x には属性とかメソッドとかがあるんですね」とか「Online Python Tutor で可視化したときに右側に出てくるんですね」という理解ができます.

(以下はややこしい話なので,慣れないうちは気にしない方がよいです)

「x はクラス A のオブジェクトです」という文は間違いではないのですが,「x は A 型のオブジェクトです」「x はクラス A のインスタンスであるオブジェクトです」という方が誤解のおそれがありません. (なぜかというと,実は Python ではクラスもオブジェクトの一種だからです.ややこしいですね)


それでは満を持して複数の質点を同時にシミュレートするように拡張します. Particle 型オブジェクトのリスト particle_list を作って複数の質点を管理します.

particle.py (ver 5.0)
 1import random
 2import pygame
 3
 4
 5class Particle:
 6    pass
 7
 8
 9def main():
10    pygame.init()
11    width, height = 600, 400
12    screen = pygame.display.set_mode((width, height))
13    clock = pygame.time.Clock()
14
15    dt = 1.0
16    gy = 0.5
17    particle_list = []
18
19    while True:
20        frames_per_second = 60
21        clock.tick(frames_per_second)
22
23        should_quit = False
24        for event in pygame.event.get():
25            if event.type == pygame.QUIT:
26                should_quit = True
27            elif event.type == pygame.KEYDOWN:
28                if event.key == pygame.K_ESCAPE:
29                    should_quit = True
30            elif event.type == pygame.MOUSEBUTTONDOWN:
31                if event.button == 1:
32                    p = Particle()
33                    p.is_alive = True
34                    p.x, p.y = event.pos
35                    p.vx = random.uniform(-10, 10)
36                    p.vy = random.uniform(-10, 0)
37                    particle_list.append(p)
38        if should_quit:
39            break
40
41        for p in particle_list:
42            p.vy += gy * dt
43            p.x += p.vx * dt
44            p.y += p.vy * dt
45            if p.x < 0 or p.x > width or p.y > height:
46                p.is_alive = False
47
48        particle_list[:] = [p for p in particle_list if p.is_alive]
49
50        screen.fill(pygame.Color("black"))
51        for p in particle_list:
52            radius = 10
53            pygame.draw.circle(screen, pygame.Color("green"), (p.x, p.y), radius)
54        pygame.display.update()
55
56    pygame.quit()
57
58
59if __name__ == "__main__":
60    main()
17行目
particle_list 変数を用意します.空のリストで初期化しておきます.
31-37行目

Particle 型オブジェクトは,マウスボタンが押されたときに生成して属性を初期化し, particle_listappend します.

複数の質点の存在を許すため,if 文の条件の「 is_alive でないならば」は外します.

41行目
particle_list の各要素について運動計算と範囲外に出たかどうかのチェックをします.
48行目
is_alive 属性が False な質点を削除します.リスト内包記法を使って, is_alive 属性が True であるものだけを残したリストを作り, particle_list の全要素を置き換えます.
51行目
particle_list の各要素を描画します.
リスト内包記法に慣れていないので,削除のところがわかりにくいです.こういう Python 特有の記法ではなく,もっと普通の書き方ではできないんですか?
他の書き方もできますが,たぶんリスト内包が一番わかりやすいです.

以下,他の手段について検討していきますが,たぶん途中で「あー,もういい,わかった,リスト内包でいいよ」ってなると思います.そこで読むのをやめて結構です.

まず安直に考えると,以下のような方法が思いつくと思います:

for p in particle_list:
    if not p.is_alive:
        particle_list.remove(p)

しかしこれはエラーになります.リストの要素を for 文で回している最中にそのリストを変更することは許されていません.

これを回避するためによく使われるのは以下のようなテクニックです:

for p in particle_list[:]:
    if not p.is_alive:
        particle_list.remove(p)

何が変わったかわかるでしょうか.for 文で要素を回すリストを particle_list[:] としています.つまりいったん particle_list のコピーを作ることで,上の制限を回避しています.言われないと気付かないですよね.

さらに細かいことをいうと,上の方法は性能面でもちょっと微妙です. for 文でリストの各要素についてループしながら別のリストの要素を remove しているわけですが,particle_list.remove(p) はリストの先頭から p を探す処理を含みます.ループの内側にループが入っているわけですが,削除すべき要素は特定できているのだから,こんな二重ループは不要なはずです.

これを避けるには,インデックスを使って:

for i in range(len(particle_list)):
    if not particle_list[i].is_alive:
        del particle_list[i]

と書けばよい…と思いましたか? これは期待通りに動きません. particle_list の要素を前から削除していくと,削除された要素以降のインデックスが変わってしまいます.よって正しくは range の結果を逆順にして:

for i in reverse(range(len(particle_list))):
    if not particle_list[i].is_alive:
        del particle_list[i]

としなくてはなりません.

…ほら,リスト内包の方がわかりやすいですよね.

48行目で,スライスへの代入になっているのはなぜですか? 単に particle_list = ... としてはダメですか?
今回の例ではどちらでも構いませんが,両者の動作は微妙に異なることに注意してください.後の章ではスライスへの代入であることが重要になる例が出てきます.

particle_list = ... とした場合,右辺で作られた新しいリストが変数 particle_list に新たに割り当てられます.もともと particle_list に割り当てられていたリストは破棄されます.

一方 particle_list[:] = ... と書いた場合,もともと particle_list に割り当てられていたリストの内容が右辺のリストの内容で置き換えられます.

5.4. データの操作を整理する

質点 1 個に関するデータをひとまとまりとして,複数の質点を動作させることはできました.そうこうしているうちに main 関数が長くなってきました.データをまとめるだけではコードは簡潔にはならないのです.これまで同様に関数に切り分けていきたいと思います.

しかしデータをまとめたことが無駄なわけではありません.単に計算手順のまとまりに着目するだけでなく,データのまとまりにも着目することですっきりさせることができます.

具体的には以下の方針で考えます:

  • main 側からは Particle をできるだけひとまとまりのものとして扱い,個々の属性を直接読み書きするコードは関数を作って閉じ込める
  • 完全に閉じ込めるのが難しい場合は, main 側から個々の属性の読み取りだけは許す (書き込むのは可能な限り避ける)
particle.py (ver 6.0)
 1import random
 2import pygame
 3
 4
 5class Particle:
 6    pass
 7
 8
 9def init_particle(p, pos, vel):
10    p.is_alive = True
11    p.x, p.y = pos
12    p.vx, p.vy = vel
13
14
15def update_particle(p, width, height, dt, gy):
16    p.vy += gy * dt
17    p.x += p.vx * dt
18    p.y += p.vy * dt
19    if p.x < 0 or p.x > width or p.y > height:
20        p.is_alive = False
21
22
23def draw_particle(p, screen):
24    radius = 10
25    pygame.draw.circle(screen, pygame.Color("green"), (p.x, p.y), radius)
26
27
28def main():
29    pygame.init()
30    width, height = 600, 400
31    screen = pygame.display.set_mode((width, height))
32    clock = pygame.time.Clock()
33
34    dt = 1.0
35    gy = 0.5
36    particle_list = []
37
38    while True:
39        frames_per_second = 60
40        clock.tick(frames_per_second)
41
42        should_quit = False
43        for event in pygame.event.get():
44            if event.type == pygame.QUIT:
45                should_quit = True
46            elif event.type == pygame.KEYDOWN:
47                if event.key == pygame.K_ESCAPE:
48                    should_quit = True
49            elif event.type == pygame.MOUSEBUTTONDOWN:
50                if event.button == 1:
51                    p = Particle()
52                    vx = random.uniform(-10, 10)
53                    vy = random.uniform(-10, 0)
54                    init_particle(p, event.pos, (vx, vy))
55                    particle_list.append(p)
56        if should_quit:
57            break
58
59        for p in particle_list:
60            update_particle(p, width, height, dt, gy)
61
62        particle_list[:] = [p for p in particle_list if p.is_alive]
63
64        screen.fill(pygame.Color("black"))
65        for p in particle_list:
66            draw_particle(p, screen)
67        pygame.display.update()
68
69    pygame.quit()
70
71
72if __name__ == "__main__":
73    main()
9-12行目と 52-54行目

Particle 型オブジェクト p の初期化を行う init_particle という関数を作り,main からはそれを呼びます.

main 側 (52-54行目) に現れるのは p だけで, p.xp.vx などに直接触れなくなったことに着目してください. init_particle に第1引数として p を渡し,その属性の操作を任せています.

15-20行目と60行目, 23-25行目と66行目
同様に,状態更新を行う関数と描画を行う関数を切り出します.
62行目
ここは変更がありません.ここでは p の属性 is_alive に直接触っています.ここで関数を切り出そうとしても単に is_alive 返すだけの関数を作るだけで,別に読みやすくなりません.素直に main 側から属性を読み出すことにします.
リストからの質点削除のところ,右辺をまるごと関数として切り出せばきれいになるんじゃないですか?
ここでは Particle 単体についての処理を切り出すことを考えています.

Particle のリスト」についての処理を整理するのは別の話です.あとで AppMain というクラスを作るときにこの辺の整理に踏み込みます.

個々の属性の読み書きを関数に閉じ込めるのが難しい場合に,属性の読み取りだけを許すようにする (書き込みはできるだけ避ける) のはなぜですか?
グローバル変数のときと同じ理由です.

読み書き両方を避けられるに越したことはないですが,それが難しい場合,せめて外部から属性が書き換えられるのだけでも防げれば,混乱を最小限にすることができます.

5.5. データとその操作をまとめる

今作った init_particle, move_particle, draw_particle は,クラス Particle の定義の外側で定義されています.しかし,これらは Particle 型オブジェクトの属性を扱う関数なので,Particle クラスの「内部のもの」であることが明確にわかるよう書けるのが自然です.

Python では, class ClassABC: の内側で def method_xyz(): と書いて関数定義をすることでこれを実現します.その結果 ClassABC.method_xyz という名前の「関数」が作られます.これを ClassABCメソッド method_xyz と呼びます.

そして:

object_abc.method_xyz(引数1, 引数2, 引数3)

と書くことで:

ClassABC.method_xyz(object_abc, 引数1, 引数2, 引数3)

と書いたのと同じ効果を得ることができるようになっています.ただし object_abcClassABC のインスタンスであるとします.

つまり,クラス名を直接指定してメソッドを呼び出すのではなく,そのオブジェクトがどのクラスのインスタンスかによって呼ばれるメソッドが決まります.そしてそのメソッドには,最初の引数としてオブジェクト自身が自動で渡されます.

これがこれまで何度となく目にしてきたオブジェクト.メソッド() という形式の呼び出し方の正体です.

particle.py (ver 7.0)
 1import random
 2import pygame
 3
 4
 5class Particle:
 6    def __init__(self, pos, vel):
 7        self.is_alive = True
 8        self.x, self.y = pos
 9        self.vx, self.vy = vel
10
11    def update(self, width, height, dt, gy):
12        self.vy += gy * dt
13        self.x += self.vx * dt
14        self.y += self.vy * dt
15        if self.x < 0 or self.x > width or self.y > height:
16            self.is_alive = False
17
18    def draw(self, screen):
19        radius = 10
20        pygame.draw.circle(screen, pygame.Color("green"), (self.x, self.y), radius)
21
22
23def main():
24    pygame.init()
25    width, height = 600, 400
26    screen = pygame.display.set_mode((width, height))
27    clock = pygame.time.Clock()
28
29    dt = 1.0
30    gy = 0.5
31    particle_list = []
32
33    while True:
34        frames_per_second = 60
35        clock.tick(frames_per_second)
36
37        should_quit = False
38        for event in pygame.event.get():
39            if event.type == pygame.QUIT:
40                should_quit = True
41            elif event.type == pygame.KEYDOWN:
42                if event.key == pygame.K_ESCAPE:
43                    should_quit = True
44            elif event.type == pygame.MOUSEBUTTONDOWN:
45                if event.button == 1:
46
47                    vx = random.uniform(-10, 10)
48                    vy = random.uniform(-10, 0)
49                    p = Particle(event.pos, (vx, vy))
50                    particle_list.append(p)
51        if should_quit:
52            break
53
54        for p in particle_list:
55            p.update(width, height, dt, gy)
56
57        particle_list[:] = [p for p in particle_list if p.is_alive]
58
59        screen.fill(pygame.Color("black"))
60        for p in particle_list:
61            p.draw(screen)
62        pygame.display.update()
63
64    pygame.quit()
65
66
67if __name__ == "__main__":
68    main()
6-20行目

先ほど切り出した 3 つの関数をクラスの定義内に移して Particle 型に属するメソッドにします.ようやく pass ではなく意味のあることを書けるようになりました.

Particle 型に属していることはわかるので,関数名の後半の _particle の部分はもはや不要です.また,初期化用のメソッドは必ず __init__ という名前にするのが Python の約束です.

3 つのメソッドとも,第1引数が self という名前に変わったことに注意してください.ここには,呼び出し側の . の前にあるオブジェクト自身が渡されてきます.

self という名前は実は単なる慣習ですが,必ずこの名前を使います.

46-49行目

これまで p = Particle() としてオブジェクトを作り, init_particle(p, arg1, arg2) という形式で初期化していましたが,このオブジェクト生成と初期化をまとめて p = Particle(arg1, arg2) と書けるようになりました.

なぜならば,この書き方により,オブジェクトの生成と Particle.__init__(p, arg1, arg2) の呼び出しが行われる約束になっているからです.

Python ではこのような __xyz__ という形の名前を持つメソッドは特殊メソッド (special method) と呼ばれます.プログラム中で直接呼び出すためのものではなく,何らかの条件で自動的に呼び出されるものにこういう名前がついています.

55行目,61行目
movedraw の呼び出しも「メソッド呼び出し形式」に書き換えました.
クラスとオブジェクト (あるいはインスタンス?) の関係がよくわからなくなってきました.いま私達はクラスを定義したんですか? オブジェクトを定義したんですか?
オブジェクトがどのような属性やメソッドを持つかを記述するものがクラスです.クラスの定義に基づいて,そのインスタンスであるオブジェクトを生成することができます.

クラスと,そのインスタンスであるオブジェクトの関係は,「整数」と,そのインスタンスである「1」とか「2」の関係と同じです.「猫」と,そのインスタンスである「磯野さんちのタマ」とか「西根さんちのミケ」の関係と同じです.

クラスを定義することで,そのインスタンスであるオブジェクトがどのように振舞うかが定まります.その意味で,クラスを定義する工程はオブジェクトを定義する工程であるともいえます.

クラス A にメソッド m が定義されているときに,A のインスタンス a を作ったとします.m を指す用語として「A のメソッド m」というのと「a のメソッド m」というのはどちらが正しいですか?
どちらも正しいです.文脈 (ある特定の a の話をしているのか,クラス A 全般の話をしているのか) によって使い分ける場合があるだけです.
クラスを使ってプログラムを整理する場合は,こうやっていったん外部関数を作り,それをメソッドに置き換えていくのが普通なんですか?
いいえ,今回の手順はクラスの概念を理解してもらうための冗長なものです.本来は最初からメソッドとして作るのが普通です.

一方,クラスを使わずに書かれた既存のコードをクラスを使って整理することもよくありますが,そういう場合もここまで回りくどいことはせず,すぐ後で見る AppMain を作る例のようになるのが普通です.

通りすがりの Java プログラマです.普通 Java では,属性変数はすべて外部からは読み書きともにできないようにして,必要なものには読み書きを行うためのメソッドを用意します.今回の例のように is_alive 属性を外から読めるようにする設計には違和感があるのですが.
プログラミング言語ごとに特徴が異なるので,推奨される書き方も異なります.Java なら確かにそのように書くべきでしょう.Python でもそのような書き方を好む人もいます.

質問で述べられているような設計を Python で書いてみると以下のようになります (関係する箇所以外は省略しています):

class Particle:
    def __init__(self):
        self._is_alive = True

    def is_alive(self):
        return self._is_alive

元のコードの属性 is_alive_is_alive という名前に変えました.Python の慣習で,属性の名前が _ から始まるときは「クラス外からは読み書きしてはいけない」約束になっています.(Java や C++ の用語でいうならば private 指定をしていることに相当します)

この属性の値をクラス外から読み出すときは,別途用意したメソッド is_alive を使います.例えば元のコードの 57 行目は以下のように書くことになります:

particle_list[:] = [p for p in particle_list if p.is_alive()]

このように属性を読み取るだけのメソッドをゲッター (getter) と呼びます.同様に,属性に値をセットするメソッドを設ける場合,それをセッター (setter) と呼びます.総称してアクセッサ (accessor) と呼ぶ場合があります.

Java や C++ ではこのような書き方が必要となるのは,6 章で述べるようなプロパティ (property) 機構がこれらの言語に用意されていないからです.詳しくは 6 章まで待ってください.

とはいえ,Python でもこのようにゲッター・セッターを用意するのを好む人もいます.他の言語と設計を共通にしたいなど,理由はいろいろあり得ると思います.

5.6. 結局何をしたのか

関係するデータをひとまとまりにして,それらを操作するメソッドを用意することで,プログラムを整理する例を見てきました.何となく読みやすくなったかなと思ってもらえるとよいのですが,まだピンと来ない人も多いかも知れません.

このように整理することで何が変わったのかを標語的にいうと,「運動法則に従って動く」という責務 (responsibility) を,メインプログラムからオブジェクトに移したといえます.当初のコードでは,メインプログラムが質点の動きを事細かに指示していました.これに対して整理後のプログラムでは,メインプログラムはオブジェクトに「自分の状態を更新してください」「自分を描画してください」と依頼するだけで,その詳細はオブジェクト自身に任せるスタイルになっています.

このようにプログラムが果たすべき責務を分割してオブジェクトに任せていくスタイルをオブジェクト指向プログラミング (object-oriented programming) と呼びます.

いや,責務だの何だのいったい何を言っているのかわかりません.任せるも何も,結局プログラミングするのは自分ですよね?
たとえすべてのコードを自分一人で書くとしても,同時に考慮しなくてはならないものを減らすことができるので有益です.

Particle の中身を作っているときはそれを使う側の詳細に注意を払うことなく,使う側のコードを書いているときは Particle の中身を気にしなくて済むようにすることができます (理想的には).

この特徴が,複数人で分業するときに有益なのは言うまでもありません.

何だか「オブジェクト指向」の定義が曖昧だと思います.クラスを使ってプログラムを書くことがオブジェクト指向ってことではないんですか?
オブジェクト指向という概念には明確な定義がありません.

誰かが何らかの文書で厳密に定義したというようなものではなく,長い年月をかけて醸成され,計算機科学者やプログラマのコミュニティの中で緩やかに合意形成されてきたものです.

なので,少し古い本を読めばちょっと違った力点の置き方で説明されていることもあると思います.現代においても,人によってちょっとずつ言うことが違いますが,「責務を分割してオブジェクトに任せる」という理念が要点であることには多くの人が同意するものと思います.

オブジェクト指向型と呼ばれるプログラミング言語に用意されているいろいろな機能は,その理念を実現しやすくするための手段で,具体的にどんな機能を使うかは言語ごとに違います.そのため,機能の有無によってオブジェクト指向を定義するのは難しいです.

クラスを使えばオブジェクト指向とか,オブジェクトを使えばオブジェクト指向といったふうに簡単に述べられればよいのですが,そんなに単純ではありません.クラスを持たないオブジェクト指向プログラミング言語も存在しますし,言語仕様にオブジェクトという概念があるのにオブジェクト指向型とは言えない言語もあります (実は C 言語がそうです.C の言語仕様に出てくる「オブジェクト」はオブジェクト指向とは何の関係もありません).

私は今後C言語をメインで使っていくことになりそうなのですが,この実習でオブジェクト指向プログラミングを学ぶのは無駄ですか?
そんなことはありません.オブジェクト指向プログラミングを直接サポートしない言語でも,その考え方を役立てることはできます.

C言語であれば,構造体と関数を使って particle.py ver 6.0 のような形にコードを整理することはで可能ですし,実際,大規模なプログラムはこのようなスタイルで書く人が多いです.

「同時に考える必要のある要素を減らす」ための方法論は,オブジェクト指向プログラミングしかないのですか?
他にもいろいろあります.例えば,関数型プログラミングなどは学ぶ価値が高いと思います.

関数型プログラミング (functional programming) はおおむね以下の点を特徴とするものです.

  • 関数は数学的な意味での純粋な関数とする.つまり,引数が定まれば返り値が一意に定まるような関数だけを考える.
  • 変数への再代入は許さない.

言い換えれば,関数も変数も,いわゆる普通の数学と同じ扱いをするということです (x = x + 1 などという代入は,もう慣れてしまって驚かないかも知れませんが,数学的には奇妙なものです).

非常に制限が強いように感じるかも知れませんが,さまざまなテクニックを駆使することで,他のプログラミング言語と同等の計算を実現できます.

関数型プログラミングが「同時に考える必要のある要素を減らす」のに効果的であるのは上記の特徴からよくわかると思います.関数の動作は引数以外の影響を受けることがありませんし,「この変数ってどこかで値が変わっちゃってないかな?」などと考える必要がありません.数学との親和性が高いことから,理論的な解析や検証にも向いています.

ソフトウェア全体を関数型プログラミングで構築するなら,関数型プログラミングをサポートする言語を使うべきですが,他のプログラミング言語を使う場合でも部分的に関数型の考え方を取り入れることは可能であり有益です (ちょうど,C 言語でもオブジェクト指向の考え方を部分的に取り入れられるように).例えば Python でも,リスト内包表記などは「関数型プログラミングっぽい」特徴を持つ機能です.

5.7. もう少し整理する

ここまでは,質点を表す Particle クラスだけを作りました.同じように他のデータもまとめていくことを考えます.

5.7.1. World

現状のプログラムでは Particlemove メソッドに引数として width, height, dt, gy を渡しています.言わば,メインプログラムが質点に「動いてください」と依頼する際に,「ただし世界の大きさは width × height で,時間ステップは dt で,重力加速度は gy だとします」と毎回伝えていることになります.

一方,運動法則に従う責務を任せるのであれば,質点オブジェクトがこの世界に生成された時点でこれらの情報を教えてしまい,あとはそれぞれの質点の自己責任でこれらの情報を参照するべきだと考えることもできます.

ちょっと大仰な名前ですが,World というクラスを作り,これら 4 つの変数を World 型オブジェクトの属性であると考えてみることにします.

particle.py (ver 8.0)
 1import random
 2import pygame
 3
 4
 5class World:
 6    def __init__(self, width, height, dt, gy):
 7        self.width = width
 8        self.height = height
 9        self.dt = dt
10        self.gy = gy
11
12
13class Particle:
14    def __init__(self, pos, vel, world):
15        self.is_alive = True
16        self.x, self.y = pos
17        self.vx, self.vy = vel
18        self.world = world
19
20    def update(self):
21        self.vy += self.world.gy * self.world.dt
22        self.x += self.vx * self.world.dt
23        self.y += self.vy * self.world.dt
24        if self.x < 0 or self.x > self.world.width or self.y > self.world.height:
25            self.is_alive = False
26
27    def draw(self, screen):
28        radius = 10
29        pygame.draw.circle(screen, pygame.Color("green"), (self.x, self.y), radius)
30
31
32def main():
33    pygame.init()
34    width, height = 600, 400
35    screen = pygame.display.set_mode((width, height))
36    clock = pygame.time.Clock()
37
38    world = World(width, height, dt=1.0, gy=0.5)
39    particle_list = []
40
41    while True:
42        frames_per_second = 60
43        clock.tick(frames_per_second)
44
45        should_quit = False
46        for event in pygame.event.get():
47            if event.type == pygame.QUIT:
48                should_quit = True
49            elif event.type == pygame.KEYDOWN:
50                if event.key == pygame.K_ESCAPE:
51                    should_quit = True
52            elif event.type == pygame.MOUSEBUTTONDOWN:
53                if event.button == 1:
54                    vx = random.uniform(-10, 10)
55                    vy = random.uniform(-10, 0)
56                    p = Particle(event.pos, (vx, vy), world)
57                    particle_list.append(p)
58        if should_quit:
59            break
60
61        for p in particle_list:
62            p.update()
63
64        particle_list[:] = [p for p in particle_list if p.is_alive]
65
66        screen.fill(pygame.Color("black"))
67        for p in particle_list:
68            p.draw(screen)
69        pygame.display.update()
70
71    pygame.quit()
72
73
74if __name__ == "__main__":
75    main()
5-10行目

クラス World を定義します.メソッドは __init__ だけです. self のほかに 4 つの引数を受け取って自身 (self) の属性としてセットします.

Particle クラスでもそうでしたが,一般に __init__ の主な仕事は属性の値を初期化することです.

38行目

関数 main の最初の方で World 型のオブジェクトを生成して,変数 world に入れておきます.

Python では大文字・小文字は区別されます.先頭が大文字の World をクラス名,全部小文字の world を変数名に使っています.

14-18行目と 56行目

main 関数内で Particle 型オブジェクトを生成するときに引数として world を渡しています. Particle__init__ の定義も修正していて,渡された world を自身の属性として保持しています.

ここで保持されているのが World 型オブジェクトへの参照だという点に注意してください. World 型オブジェクトはこのシミュレーション世界にただひとつ存在し,それぞれの質点が持つ self.world という荷札からの紐がつながっている状態になります.それぞれの質点がコピーを持っているわけではありません.

20-24行目と 62行目
Particleupdate メソッドにはもはや self 以外の引数は不要です.計算に必要な値は self.world の属性から読み出して使います.
だんだん構造がわからなくなってきました.
Online Python Tutor で可視化してみるとよいです.

残念ながら Online Python Tutor では pygame は使えませんが,簡略化したものを作ってみればよいです.構造を理解するには十分です:

class World:
    def __init__(self, width, height, dt, gy):
        self.width = width
        self.height = height
        self.dt = dt
        self.gy = gy


class Particle:
    def __init__(self, pos, vel, world):
        self.is_alive = True
        self.x, self.y = pos
        self.vx, self.vy = vel
        self.world = world

def main():
    w = World(600, 400, 1, 2)
    p1 = Particle((100, 120), (0, 0), w)
    p2 = Particle((200, 220), (0, 0), w)

main()
Online Python Tutor で可視化してみると,右側の Objects のところに class があったり instance があったりして混乱します.クラスはオブジェクトなんですか?
これは確かに混乱を招くところです.最初のうちはあまり気にしない方がよいと思います.

Python では,データだけでなく関数もモジュールもオブジェクトとして扱われるという話を (以前の Q&A で) したことがあります.同様に,クラスもオブジェクトとして扱われます.

この「オブジェクトとして扱われる」ということの意味がピンとこないかもしれませんが,例えば,変数に代入したり,関数やメソッドに引数として渡したりできるというくらいの意味で理解しておいてもらえればよいです.これは便利な点で,後の章でも利用することがあります.

そんなわけで,Online Python Tutor ではクラスも関数もモジュールもすべて Objects のところに並ぶことになるのですが,あまり気にせずに,各種インスタンスやデータの配置だけに注目する方がわかりやすいと思います.

こういう World 型オブジェクトみたいなのを作ってすべての Particle 型オブジェクトから共有したら,グローバル変数を作ってしまうのとほとんど同じじゃないですか?
確かにその懸念はあります.しかし,グローバル変数にしない意義はあります.

例えば,複数の世界 (重力加速度の違う2つの環境とか) をシミュレートして比べたい場合に,World のインスタンスを複数作って,それぞれの世界の計算を並行して進めながら可視化したりすることができます.もし height, width, dt, gy がグローバル変数だったなら,そういうことはできません.

一方,World のインスタンスが他のほとんどすべてのオブジェクトから共有されていることで,グローバル変数と同様のデメリットがあるのも確かです.なので,World に持たせる属性は最小限にするべきで,かつ読み取り専用とするのが望ましいといえます.実際,そのような設計にしています.

5.7.2. AppMain

メインプログラム側もクラスにしてしまう例を示します.ここでは AppMain という名前にしましょう (Application Main のつもり).この運動学シミュレーションアプリケーション自身を表すオブジェクトを作ることにして,その属性と操作を定めようということです.

particle.py (ver 9.0)
32class AppMain:
33    def run(self):
34        pygame.init()
35        width, height = 600, 400
36        screen = pygame.display.set_mode((width, height))
37        clock = pygame.time.Clock()
38
39        world = World(width, height, dt=1.0, gy=0.5)
40        particle_list = []
41
42        while True:
43            frames_per_second = 60
44            clock.tick(frames_per_second)
45
46            should_quit = False
47            for event in pygame.event.get():
48                if event.type == pygame.QUIT:
49                    should_quit = True
50                elif event.type == pygame.KEYDOWN:
51                    if event.key == pygame.K_ESCAPE:
52                        should_quit = True
53                elif event.type == pygame.MOUSEBUTTONDOWN:
54                    if event.button == 1:
55                        vx = random.uniform(-10, 10)
56                        vy = random.uniform(-10, 0)
57                        p = Particle(event.pos, (vx, vy), world)
58                        particle_list.append(p)
59            if should_quit:
60                break
61
62            for p in particle_list:
63                p.update()
64
65            particle_list[:] = [p for p in particle_list if p.is_alive]
66
67            screen.fill(pygame.Color("black"))
68            for p in particle_list:
69                p.draw(screen)
70            pygame.display.update()
71
72        pygame.quit()
73
74
75if __name__ == "__main__":
76    app = AppMain()
77    app.run()
32-72行目

クラス AppMain を定義し,これまで main 関数だったものをそっくり run メソッドにします.内容はコピーしただけです.

こうやって普通の関数をメソッドに変える場合,メソッドは普通の関数とは違って第1引数に必ず self を入れなくてはならない点に注意してください.(慣れないうちは忘れやすいです.また,他の言語に慣れている人も忘れやすいです).

76-77行目
関数 main を呼んでいたところで,AppMain 型オブジェクトを生成して変数 app に割り当て,そのメソッド run を呼ぶことにします.

ここまで書いて動作確認できたら,引き続いて run の中の処理の一部を,初期化,状態更新,描画などなどのメソッドに切り分けていきます.

普通の関数の場合,ローカル変数の値を受け渡すために,引数や返り値を設ける必要がありました (hello_pygame.py で関数に切り分けたときのことを思い出してください).一方,メソッドに切り分ける場合は, self の,つまりオブジェクト自身の属性にしてしまってメソッド間で共有するという手もあります.

update, draw, add_particle をメソッドとして切り出すことを考えると,これらと run の間で, particle_list, screen, world, pos, button をどうやって受け渡すかを考える必要があります.

ここでは以下のように考えます.これは一つの設計例に過ぎません.(唯一の正解などというものはないのです)

  • particle_list, screen, world はこのシミュレーションアプリケーションが動いている間保持され続けるべきものなので self の属性にする
  • pos, button はマウスボタンが押されたときに発生する一時的な値なので,引数として受け渡す
  • run メソッドの中だけで使うものはローカル変数として残す

self の属性の初期化はすべて __init__ にまとめておくことにすると,結局以下のように整理されます.

particle.py (ver 10.0)
32class AppMain:
33    def __init__(self):
34        pygame.init()
35        width, height = 600, 400
36        self.screen = pygame.display.set_mode((width, height))
37        self.world = World(width, height, dt=1.0, gy=0.5)
38        self.particle_list = []
39
40    def update(self):
41        for p in self.particle_list:
42            p.update()
43        self.particle_list[:] = [p for p in self.particle_list if p.is_alive]
44
45    def draw(self):
46        self.screen.fill(pygame.Color("black"))
47        for p in self.particle_list:
48            p.draw(self.screen)
49        pygame.display.update()
50
51    def add_particle(self, pos, button):
52        if button == 1:
53            vx = random.uniform(-10, 10)
54            vy = random.uniform(-10, 0)
55            p = Particle(pos, (vx, vy), self.world)
56            self.particle_list.append(p)
57
58    def run(self):
59        clock = pygame.time.Clock()
60
61        while True:
62            frames_per_second = 60
63            clock.tick(frames_per_second)
64
65            should_quit = False
66            for event in pygame.event.get():
67                if event.type == pygame.QUIT:
68                    should_quit = True
69                elif event.type == pygame.KEYDOWN:
70                    if event.key == pygame.K_ESCAPE:
71                        should_quit = True
72                elif event.type == pygame.MOUSEBUTTONDOWN:
73                    self.add_particle(event.pos, event.button)
74            if should_quit:
75                break
76
77            self.update()
78            self.draw()
79
80        pygame.quit()
81
82
83if __name__ == "__main__":
84    app = AppMain()
85    app.run()
説明を聞くとわかるのですが,こうやって切り分ける作業を自分でできる気がしません.自分でプログラムを作るときも常にこうやって整理しなくてはならないのですか?
いきなり無理をしなくていいですよ.

ここでは (テキストがあまり長くなるのを避けるため) 4 つのメソッドを一気に切り出しましたが,普通は 1 つずつ考えます.その都度,関連する変数をどう扱うかを考えます.そうやって順次やっていくうちに「ああ,さっき引数にしたアレは,やっぱり self の属性にするべきだったな」と考え直して修正する場合などももちろんあります.

こういう作業は本質的に「設計」なので,唯一の正解があるわけではありません.particle.py の ver 9.0 のように大きな run メソッド 1 個だけのままでも,別に間違いというわけではないです.逆に,ver 10.0 が理想形というわけでもありません.実際,例えばイベント処理の部分は別のメソッドか,さらには別のクラスに分けるのが普通の設計だと思います.できる範囲で考えてみるのがいいのではないでしょうか.

AppMain の中の変数を,run のローカル変数とするのと, AppMain 型オブジェクトの属性にするのとで,具体的にどういうメリットとデメリットがあるのですか?
グローバル変数を使うことのメリット・デメリットに似ています.

属性変数としてメソッド間で共有すると,

  • 引数として渡さなくてよいので呼び出し側のコードが簡潔
  • 受け渡すべきものが増えたり減ったりしたときの対応が楽

というメリットがあります.一方で,

  • 同時に把握しておかなくてはならない要素が増える

というグローバル変数と同様のデメリットが生じます.

こういう AppMain みたいなクラスは必ず作る方がよいですか? main 関数といくつかの補助関数のままでもよいように思うのですが.
あくまで設計例ですので,どちらが正解ということはありません.
これまでの例を見ると,関数やメソッドの名前は「動詞」あるいは「動詞 + 名詞」の形でつけられていることが多いようです.何かそういうルールでもあるんですか?
特にルールというわけではありませんが,そのようにつけると読みやすくなることが多いです.もちろん例外もあります.

動詞を使わない場合の例として,返り値を表す名詞をそのまま使うことも多いです.Python の標準関数でいうと,len (length を返す) や math.sqrt (square root を返す) などがそうです.

返り値が真偽値型の場合は,動詞の三人称単数現在形から始める命名もよく使われます.例えば文字列型のメソッド isalpha (その文字列のすべての文字がアルファベットなら True を返す), startswith (その文字列が引数として渡された文字列で始まるなら True を返す) などです.

コツとしては,その関数やメソッドを呼び出すコードが英語っぽく読めるにはどういう名前にするとよいだろうか,と考えるとうまい命名につながることが多いです.例えば三単現を使うのは,if 文の条件などで使うときに読みやすくなるからだと考えるとよくわかると思います:

if password.isalpha():
    print("Password should include non-alphabet letters.")

5.8. 演習

以下のように Particle の色や半径も属性として持たせるように修正します. __init__ の引数 radiuscolor はデフォルト値つきとして,これまで通りに使うこともできるようにしています.

particle.py (ver 11.0)
13class Particle:
14    def __init__(self, pos, vel, world, radius=10.0, color="green"):
15        self.is_alive = True
16        self.x, self.y = pos
17        self.vx, self.vy = vel
18        self.world = world
19        self.radius = radius
20        self.color = pygame.Color(color)
21
22    def update(self):
23        self.vy += self.world.gy * self.world.dt
24        self.x += self.vx * self.world.dt
25        self.y += self.vy * self.world.dt
26        if self.x < 0 or self.x > self.world.width or self.y > self.world.height:
27            self.is_alive = False
28
29    def draw(self, screen):
30        pygame.draw.circle(screen, self.color, (self.x, self.y), self.radius)
31

ここまで修正したら,いったんコミットしておいてください.次の章では,この時点から再開します.

例題5-1

質点の色や半径も乱数で生成するように AppMain 側を修正してください.

(色の一覧は適当なリストとしてあらかじめ用意しておいて結構です)

例題5-2

現在位置から複数個の ((異なる初速度の) 質点を投てきするメソッド add_multiple_particlesAppMain に追加し,マウスの右ボタンのクリック1回で複数の質点を投てきできるようにしてください.

例題5-3

質点の初速度がランダムではなくユーザの操作によって決まるようにすることを考えます.

AppMain を修正し,マウス左ボタンを押下したときにその位置が記憶され,マウス左ボタンを離したとき (MOUSEBUTTONUP イベント) に質点が投てきされるようにしてください.その際,押下時に記憶した位置と現在位置との相対関係から,初速度が直感的に与えられるようにしてください.

(操作性を上げるために表示方法を工夫するのも面白いです)

例題5-4

直方体を表すクラスを定義し,体積,総表面積,対角長を計算するメソッド volume, surface_area, diagonal を設けることを考えます.

以下のプログラムの pass の部分を書き換えて,プログラムの実行が成功するようにしてください:

import math

class Cuboid:
    pass

shape1 = Cuboid(width=6, height=4, depth=5)
assert shape1.volume() == 120
assert shape1.surface_area() == 148
assert shape1.diagonal() == math.sqrt(77)

shape2 = Cuboid(width=3, height=3, depth=2)
assert shape2.volume() == 18
assert shape2.surface_area() == 42
assert shape2.diagonal() == math.sqrt(22)