4. 基本的なプログラミング (データ構造編)¶
前の章の続きで,主にリスト等のデータ構造を見ていきます.
既習の人はやはり実践編と演習を中心に見ていくのでもよいかも知れません.ただし,「Python のデータモデル」の節で紹介する Online Python Tutor をもし使ったことがなければぜひ試すようにしてください.Python に慣れている人にとっても大変有用です.
4.1. リスト (list型)¶
4.1.1. シーケンス¶
何らかのデータが直列に並んでいて,インデックス番号によって要素にアクセスできるデータ構造をシーケンスと呼びます.そのうち最も重要なものがリストです:
x = [10, 20, 30, 40, 50] # リストの定義
len(x) # 5 (リスト長)
x[0] # 10 (最初の要素のインデックスは 0)
x[-1] # [50] (最後の要素)
x[-3] # [30]
20 in x # True (要素として含むとき True)
99 not in x # True
[10, 20, 30] == [10, 20, 30] # True (すべての要素が等いとき True)
連続するインデックスによって部分列にアクセスできます.スライスと呼びます:
x[2:4] # [30, 40] (注: x[4] は含まれない)
x[2:] # [30, 40, 50]
x[:2] # [10, 20]
連結演算子と繰り返し演算子が使えます:
[10, 20, 30] + [40, 50] # [10, 20, 30, 40, 50]
[10, 20] * 3 # [10, 20, 10, 20, 10, 20]
x[2:4]
と書いたとき x[4]
を含まないんですか? わかりにくくないですか? そもそもどうしてインデックスは 0
から始めるんですか? 数学のベクトルや行列みたいに 1 から始めればいいのに.数学のベクトルや行列にならうなら 1 から始めて最後のインデックスを含む方がよいというのはその通りだと思います.Fortran や MATLAB などは数学記法との類似を重視するのでこの流儀です.
一方で,コンピュータのメモリの中のデータの場所をリストの先頭位置 + インデックス
で表そうと考えると,先頭要素のインデックスは 0 にする方が自然です.Python だけでなく,C や C++,Java
などこちらの流儀を採用する言語も多いです.
最後のインデックスに扱いについても考え方はいろいろです.「含めない派」がよく理由として挙げるのは以下のようなものです.
x[n:n+k]
と書いたときに要素数がk
個だとわかりやすいN
番目より前とN
番目以降を分けるときにx[:N]
とx[N:]
と書けて便利例えば C 言語の文字列のように,データの個数が未知で,目印となるような特別な要素 (番兵, sentinel) が現れたら終わりとみなす方式との整合性がよい (番兵はデータに含めない):
for (i = 0; str[i] != '\0'; i++) { putchar(str[i]); }
4.1.2. リストのいろいろな作成方法¶
range
関数により連番を作ることができます:
range(5) # range型オブジェクト (これ自体はリストではない)
list(range(5)) # [0, 1, 2, 3, 4] (0 から 5 未満)
list(range(2, 6)) # [2, 3, 4, 5] (2 から 6 未満)
list(range(2, 10, 3)) # [2, 5, 8] (2 から 10 未満,ステップ 3)
既存のリストの各要素を加工したリストを作るには,リスト内包記法が便利です:
x = [1, 2, 15, 30]
[2 * a for a in x] # [2, 4, 30, 60] (x の各要素 a について 2 * a を取る)
[2 * a for a in x if a % 2 == 0] # [4, 60]
[2 ** n for n in range(5)] # [1, 2, 4, 8, 16]
range
に引数を3つ渡すと,どれが最初でどれが最後でどれが増分だか一見わかりにくくないですか?step = 3
for x in range(2, 10, step):
print(x)
4.1.3. リストの変更¶
リストは変更可能 (mutable) です:
x[2] = 12345 # 要素 30 を 12345 に変更する
x[2:4] = [-20, -30] # スライスの変更
リストを伸縮させるときの基本操作は,末尾への要素追加と,末尾からの要素削除です:
x = []
x.append(10) # x: [10]
x.append(20) # x: [10, 20]
item = x.pop() # x: [10], item: 20
任意位置への要素挿入,任意位置の要素削除もできますが,末尾の追加・削除より効率が悪いです:
x = [0, 10, 20, 30]
x.insert(2, 12345) # x: [0, 10, 12345, 20, 30]
item = x.pop(1) # x: [0, 12345, 20, 30], item: 10
del x[1] # x: [0, 12345, 30] (値を返す必要がない場合)
del x[:] # x: [] (スライスの削除)
x.clear() # x: [] (del x[:] と同じ意味.少し速い)
x.remove(12345) # x: [0, 30] (値を指定して削除)
append
メソッドを呼ぶ前に x = []
と定義しておく必要があるのはなぜですか?x
の内容がリスト型のオブジェクトにならないと append
メソッドが使えないからです.なので,例えば以下のように書くならもちろん x = []
は不要です:
x = [10]
x.append(20)
item = x.pop()
4.1.4. 実践編¶
さて,ようやく hello_pygame.py
に戻りましょう.これまではプレイヤキャラクターは静止画のまま上下左右に動くだけでしたが,歩行をしているかのようなアニメーションをつけてみます.
アニメーションというと難しく聞こえるかも知れませんが,これからやることは簡単で,ただのパラパラマンガです.
これまで表示してきた p1_walk01.png
と同じところに,ちょっとずつ姿勢が違うプレイヤーの画像が置いてあります.p1_walk04.png
から p1_walk07.png
までを繰り返せば何となく歩いているように見えそうです.
というわけで,これらを読み込んでリストを作り,順番に繰り返し表示してみます.これまでは整数のリストの例ばかり扱ってきましたが,リストに入れられるものは整数型に限りません.ここでは Surface
型のリストを作ります.
1import pygame
2
3
4def init_screen():
5 pygame.init()
6 width, height = 600, 400
7 screen = pygame.display.set_mode((width, height))
8 return screen
9
10
11def create_text():
12 font_size = 50
13 font_file = None
14 antialias = True
15 font = pygame.font.Font(font_file, font_size)
16 text_image = font.render("hello, pygame", antialias, pygame.Color("green"))
17 return text_image
18
19
20def create_player():
21 player_images = [
22 pygame.image.load("../../assets/player/p1_walk04.png").convert(),
23 pygame.image.load("../../assets/player/p1_walk05.png").convert(),
24 pygame.image.load("../../assets/player/p1_walk06.png").convert(),
25 pygame.image.load("../../assets/player/p1_walk07.png").convert()
26 ]
27 return player_images
28
29
30def draw(screen, player_image, text_image, mouse_pos):
31 screen.fill(pygame.Color("black"))
32 screen.blit(player_image, mouse_pos)
33 mouse_x, mouse_y = mouse_pos
34 text_offset_x = 100
35 screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
36 pygame.display.update()
37
38
39def main():
40 screen = init_screen()
41 text_image = create_text()
42 player_images = create_player()
43 frame_index = 0
44
45 while True:
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.key == pygame.K_b:
54 pass
55 if should_quit:
56 break
57 mouse_pos = pygame.mouse.get_pos()
58
59 frame_index += 1
60 animation_index = frame_index % len(player_images)
61 draw(screen, player_images[animation_index], text_image, mouse_pos)
62
63 pygame.quit()
64
65
66if __name__ == "__main__":
67 main()
- 20-27行目
プレイヤ画像を読み込んだ
Surface
のリストを作って返す関数create_player
を定義します.リストの作り方が力技すぎて格好悪いですね.後でなんとかします.
- 42行目
関数
create_player
を呼び出し,返ってきたリストを変数player_images
に代入しておきます.関数
create_player
と関数main
の中の変数player_images
はそれぞれの関数のローカル変数なので,返り値を通して受け渡してやる必要があるわけです.- 43行目
時間の進行とともに表示するプレイヤ画像を変えるので,現在時間を保持しておく変数を用意します.
ディスプレイに表示される映像やビデオカメラで得られる動画などの各時刻の画像をフレーム (frame) と呼びます.ここではディスプレイに表示するフレームの番号
frame_index
をカウントしておいて,それを時刻として扱うことにします.- 59行目
- フレームごとに
frame_index
に 1 を加え,カウントアップしていきます. - 60-61行目
len(player_images)
でplayer_images
の要素数が得られます.剰余演算子%
を使って,フレーム番号を要素数で割った余りを求めます.今は要素数が 4 なので,結果は 0, 1, 2, 3 のいずれかです.これを
player_images
の要素を指定するインデックスにすることで,このリストの中のSurface
を順に繰り返し表示されることになります.
さて,動かしてみると,足のバタバタが速すぎてどうも歩いているようには見えないのではないかと思います.ちょっと調節してみることにします.
現状のプログラムの問題は,そもそもどのくらいの速さでパラパラマンガをめくっているかわからないことです.使っているコンピュータが速ければフレームは速く進みますし,遅ければその分ゆっくりになります.まずこのフレームの更新速度 (フレームレート) を一定にしておきましょう.
39def main():
40 screen = init_screen()
41 text_image = create_text()
42 player_images = create_player()
43 clock = pygame.time.Clock()
44 frame_index = 0
45
46 while True:
47 frames_per_second = 60
48 clock.tick(frames_per_second)
49
50 should_quit = False
51 for event in pygame.event.get():
52 if event.type == pygame.QUIT:
53 should_quit = True
54 elif event.type == pygame.KEYDOWN:
55 if event.key == pygame.K_ESCAPE:
56 should_quit = True
57 elif event.key == pygame.K_b:
58 pass
59 if should_quit:
60 break
61 mouse_pos = pygame.mouse.get_pos()
62
63 frame_index += 1
64 animation_period = 6
65 animation_index = (frame_index // animation_period) % len(player_images)
66 draw(screen, player_images[animation_index], text_image, mouse_pos)
67
68 pygame.quit()
69
70
71if __name__ == "__main__":
72 main()
- 43行目
pygame.time.Clock
関数が返すClock
型のオブジェクトを変数clock
に入れておきます.これを時間管理に使います.- 47-48行目
while
ループの先頭で (実はどこでもよいのですが,ループ内の決まった 1箇所で),clock
に属するtick
メソッドを呼び出します.その際引数としてフレームレートを指定します.単位は frames/s (fps),つまり 1 秒間に何フレーム表示するかです.ここでは 60 frames/s を指定しています.現代のコンピュータ用ディスプレイの画面が更新される標準的なレートです.Clock
型のオブジェクトは,前回tick
メソッドが呼ばれた時刻を覚える機能を持っています.ループの中で呼ばれると,前回の呼び出しからの経過時間を計算し,適切な待ち時間を入れます.今は 60 frames/s を指定しているので,前回からの経過時間が 16.66 ms 以上になるように調整します.
ここまでの変更で一度動かしてみると,やっぱりバタバタが速すぎるのではないかと思います.今どうなっているかというと,足の動きが 1 サイクルする 4 コマのアニメーションを 60 frames/s で再生しているので,15 cycles/s です.そりゃまだ速いわけですよね.
- 64-65行目
- というわけでもう少し遅くします.整数除算演算子
//
を使ってframe_index
を適当な数で割ってから使うことにします.ここでは経験的に 6 で割ることにしました.別の値に変えてみても構いません.
clock.tick
の引数に 60 を指定すると,フレームレートはぴったり 60 frames/s になるのですか?clock.tick(frames_per_second)
を入れる前より,入れた後の方が足のバタバタが速くなったように見えるのですが,何か間違っているのでしょうか.ちょっと思考実験してみてください.1.0 秒間で 1 回転する円運動を, 0.9 秒ごとにシャッターを切るカメラで撮影します.撮影と同じ速さで動画として再生すると,逆向きのゆっくりな回転に見えるはずです.もっと極端な状況として,1.0 秒ごとにシャッターを切れば,静止して見えます.
同様に,動画内で自動車のタイヤホイールやヘリコプターのローターがゆっくり回転して見えるなんてことは多くの人が経験があるのではないかと思います.
速い動きを,その動きを十分捉えらないくらい遅い周期で離散時間化すると,本来の運動とは異なる周波数成分が現れます.より正確な説明が欲しい人は,サンプリング定理,エイリアシングなどのキーワードで調べてください.
4.2. リストと類似したデータ構造¶
4.2.1. タプル (tuple型)¶
タプルは変更不能 (immutable) なシーケンスです.長さを変えたり要素を変更することはできません.変更を伴わない操作はリストと同様にできます:
>>> tup = (10, 20, 30)
>>> tup[1] = 123
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
一番わかりやすいけど一番つまらない理由としては,変更に対応する必要がない分だけ内部構造が最適化されているため,動作が速くメモリ使用量も少ないことが挙げられます.
それ以上に重要なのは,プログラムを読んでいるときに「これはタプルだから絶対に変更されない」と確信できることです.例えばある関数
f
に引数としてリスト x
が渡されているのを見かけたとしましょう:
x = [10, 20, 30]
f(x)
この呼び出しの結果,x
の内容が変化するかどうかは,関数
f
の動作を詳細に理解していないとわかりません.一方,もし
x
がタプルだったら,たとえ f
が何をする関数なのかを知らなかったとしても,少なくとも x
が変化することはないとして以降のコードを読み進めることができます.これにより,プログラムの読解が格段にしやすくなります.このあたりは,C 言語や C++ で「変更しないものには可能な限り const
をつけなさい」と教わるのと同じ事情です.
別の意義として,この後で出てくる「辞書」のキーになれるという利点もあります.
あれ? 以下のようにするとタプルを「変更」できてしまったのですがどうしてですか?:
>>> tup = (10, 20, 30)
>>> tup = (40, 50, 60)
>>> tup
(40, 50, 60)
それは,新しいタプル (40, 50, 60)
を作って,変数 tup
に割り当てただけです.元の (10, 20, 30)
というタプルが変わったわけではありません.
この辺の事情は,4.4 節「Python のデータモデル」まで進んでもらうともう少し理解しやすくなります.
4.2.2. 文字列 (str型)¶
文字列も変更不能なシーケンスです.表記方法はいろいろあります:
"hello, world"
'hello, world' # シングルクォートでもよい
"what's new"
'1" is equal to 25.4 mm'
'1h 23\' 45"' # \ を使って文字の役割をエスケープできる
"hello\nworld"
文字列型には特有のメソッドが多数あります.以下はごく一部の例です:
"Computer Seminar I".split() # ['Computer', 'Seminar', 'I']
", ".join(["abc", "def", "ghi"]) # 'abc, def, ghi'
"2021-10-01".replace("-", "/") # '2021/10/01'
"number {} and string {}".format(10, "abc") # 'number 10 and string abc'
文字列型の format
メソッドでは各種書式を指定できます.以下はごく一部の例です:
>>> "{1} {2}, {0}".format(2021, "October", 1) # 引数指定
>>> "{:5}".format(123) # 5文字に満たなければ空白づめ
>>> "{:05}".format(123) # 5文字に満たなければ0づめ
>>> "{:.4f}".format(3.141592) # 小数点以下4桁に丸め
>>> "{:10.4f}".format(3.141592) # 丸め + 空白づめ
>>> "{0} {0:5} {0:05}".format(123) # 引数指定 + 書式
それよりも,使いたいと思ったときにすぐにマニュアル・書籍その他で調べられるようにしておくこと (どこを調べればよいのか,どう読めばよいのかをわかっていること) が重要です.
文字列型のメソッドの公式ドキュメントは以下のページにあります.
「読みづらいなー」と思う人は自分に合うドキュメントを探してみてください.「チートシート」という名前で公開されているものには読みやすいものが多いです.
format
メソッドの書式は C言語の printf
で使う書式に似てますが,微妙に違うのがイラッとします.format
メソッドより %
演算子の方がとっつきやすいかも知れません.4.2.3. 辞書 (dict型)¶
辞書はシーケンスとは似て非なるデータ構造で,整数以外のものをインデックスの代わりに使えます:
score = {"Alice": 80, "Bob": 80, "Charlie": 80} # 辞書の定義
score["Alice"] # 80 (キー "Alice" に関連付けられた値)
score["Alice"] = 90 # 辞書は変更可能
score["Dave"] = 95 # 追加
del score["Charlie"] # 削除
"Alice" in score # True (キーが存在するかどうか)
list(score) # ['Alice', 'Bob', 'Dave'] (型変換でキーのリストが得られる)
4.2.4. 実践編¶
文字列処理を使って,プレイヤ画像のリストを作るところをもう少しスマートにしてみます.4 つのファイル名は,連番の数字のところが違うだけなので,文字列の format
メソッドを使って生成することができます.
20def create_player():
21 file_path = "../../assets/player/p1_walk{:02}.png"
22 player_images = []
23 for k in range(4, 8):
24 player_images.append(pygame.image.load(file_path.format(k)).convert())
25 return player_images
一見ややこしそうですが,k
を 4, 5, 6, 7 と変えながら,
file_path
の中の {:02} の部分に 2 文字ゼロ詰め表記で入れることでファイル名文字列を生成しています.あとは 1 つずつ append
していくだけです.
同じことを,リスト内包記法を使うと少しだけ簡潔に書けます.
20def create_player():
21 file_path = "../../assets/player/p1_walk{:02}.png"
22 player_images = [pygame.image.load(file_path.format(k)).convert()
23 for k in range(4, 8)]
24 return player_images
hello_pygame.py
の ver 5.0 あたりから気になっていたのですが,C言語では,ファイルをオープンするときは必ずエラーチェックをするように習いました.pygame.image.load
を使うときはエラーチェックは必要ないのですか?基本的なことだけ示します.まず,どのようなエラーが出るかを確認します.以前の Q&A で示しましたが,例えばファイルがなかった場合
pygame.image.load
は:
FileNotFoundError: No such file or directory.
というエラーメッセージを出力します.この最初の
FileNotFoundError
は発生したエラーの種類を表すもので,これを覚えておきます.
FileNotFoundError
が起きたときに他の処理をしたい場合は,以下のような書き方をします:
def create_player():
try:
file_path = "../../assets/player/p1_walk{:02}.png"
player_images = [pygame.image.load(file_path.format(k)).convert()
for k in range(4, 8)]
return player_images
except FileNotFoundError:
player = pygame.Surface((64, 64))
player.fill(pygame.Color("red"))
return [player]
ファイルがある限りは今まで通りの動作をするはずです.ファイルがなかった場合 (例えば,上のコードのファイル名のうち png
の部分をわざと間違えて jpg
と書いてみてください),プレイヤ画像の代わりに 64×64 画素の赤い四角形が表示されると思います.
try
節には通常の処理を普通に書きます.エラーが起きなければ
except
節はスキップされます.try
節の実行中にエラーが発生した場合,直ちに except
の処理に移ります,except
の後ろで指定されたエラー (この場合 FileNotFoundError
) だった場合は,except
節の内容が実行されます.指定されていないエラーの場合は異常終了します.
FileNotFoundError
のようなエラーの種類を表すオブジェクトを
Python では例外 (exception) と呼びます.try
文により例外を捕捉し,異なる扱いをさせることを例外処理 (exeption handling) といいます.
この方式の便利なところは,関数呼び出しをまたいで例外を処理できる点です.例えば今の例では,FileNotFoundError
例外が発生したのは pygame.image.load
関数の内部です.それを,特に
pygame.image.load
の返り値をチェックしたりすることなく,呼び出し元の create_player
側で捕捉できたことになります.何なら
create_player
は元のままとした上で,もう 1 レベル上の main
関数で捕捉させることもできます:
def main():
screen = init_screen()
text_image = create_text()
try:
player_images = create_player()
except FileNotFoundError:
player = pygame.Surface((64, 64))
player.fill(pygame.Color("red"))
player_images = [player]
clock = pygame.time.Clock()
frame_index = 0
例外を発生させたいときは raise
文を使います.詳細はドキュメントを参照してください.
4.3. 演習¶
例題4-1
対話的実行で以下のようなリストの操作を行ってください.実行前に結果 (「???
」のところ) を予想して,実際の結果と一致するか確認してください.予想が間違っていたら理由を考えて,正しい理解をするようにしてください.
>>> x = [10, 20, 30, 40, 50]
>>> x[3] = 99
>>> x
???
>>> x = [10, 20, 30, 40, 50]
>>> x[1:4]
???
>>> x = [10, 20, 30, 40, 50]
>>> x[:] = []
>>> x
???
>>> x = [10, 20, 30, 40, 50]
>>> x.append(99)
???
>>> list(range(2, 7))
???
>>> list(range(7, 2, -1))
???
>>> "/".join([c for c in ("red", "green", "blue") if len(c) >= 4])
???
4.4. Python のデータモデル¶
4.4.1. 変数は箱ではない¶
リストのように変更可能なデータ構造を扱い始めると,一見不可解な現象に出会うことがあります.例えば:
>>> x = [10, 20, 30]
>>> y = x
>>> y[0] = 123
>>> y
[123, 20, 30]
>>> x
[123, 20, 30]
リスト y
を変更したら,x
も変わってしまいました.何が起きたか想像できるでしょうか.
鍵になるのは2行目の y = x
です.ここでリスト x
の内容である
[10, 20, 30] がそっくりコピーされて y
に代入される様子をイメージした人も多いのではないかと思います.しかし実際に起きることは違います.
この点をしっかり理解するためには,これまで「変数」と呼んできたもののイメージを刷新する必要があります.「変数 x
」というと,何やら
x
という名前の書かれた箱があり,その中にデータが保存される様子を多くの人が想像するのではないかと思います.しかし Python の変数はそういうものではありません.データには名前はついておらず.名前の書かれた荷札が別の場所にあります.その荷札からデータまで紐が延びています.

変数とはこの荷札のことであり,代入とは荷札とデータを紐で結ぶことを意味します.
このように考えると,先ほどの例の動作は正しく理解できます.
x = [10, 20, 30]
により,[10, 20, 30] というデータが作成され,
x
と書かれた荷札が作られて,両者は紐で結ばれます.次に y = x
が実行されると,y
と書かれた新しい荷札が作られ,x
からの紐が延びている先の [10, 20, 30] と紐で結ばれます.同じデータに 2 つの荷札がついた状態になります.y[0] = 123
を実行すると,荷札 y
から紐をたどってリストのデータにたどり着き,その最初の要素を 123 に変更します.
4.4.2. Online Python Tutor¶
このような正しいイメージを掴むために有用なウェブサイトを紹介します.
「Start visualizing your code now」をクリックすると,コードを入力できるページに進みます.入力欄の下にあるドロップダウンリストにてすべて [default] と書かれたものが選択されていることを確認してください.さっきのコード例を入力して,「Visualize Execution」をクリックしてください:
x = [10, 20, 30]
y = x
y[0] = 123
以下のような表示が得られると思います.「Next」ボタンをクリックすると,プログラムが1行ずつ実行され,各時点での「荷札とデータ」の様子が右側に図示されます. (以下の図も動きます)
4.4.3. リスト以外も同じこと¶
今見てもらったような変数とデータの関係はリストに限りません.整数も実数も,あるいは pygame の Surface
も Color
も,すべて同じ「荷札と紐のモデル」で動作します.
「えっ? そんな馬鹿な.さすがに整数や実数は違うでしょ?」と思った人も多いかと思います.試しにさっきと同じようなものをリストではなく整数で考えてみましょう:
>>> x = 10
>>> y = x
>>> y = 123
>>> y
123
>>> x
10
今度は y
を書き換えても x
は変わりません.「やっぱり違うじゃないか.この場合は,x
という箱と y
という箱があって,その中に 10
とか 123 とかのデータが入っているんでしょ?」と思うかも知れません.
しかしそうではないのです.実際に起きていることは以下のように説明できます.1行目の x = 10
で整数データ 10 が作成され,荷札 x
が作られて,両者が紐で結ばれます.2行目の y = x
で荷札 y
が作られて,
x
の繋がり先である 10 と紐で結ばれます.この時点で x
と y
は同じデータを共有しています.
重要なのは次です.y = 123
を実行すると,整数 123 が作られ,荷札
y
から延びている紐は 10 から切り離され,123 と結ばれます.決して
10 が 123 で上書きされるわけではありません.

このような仕組みのため,整数の場合は「箱のモデル」で考えた場合と結果的に同じにことになります.そのため,これまで「荷札のモデル」を意識せずに話を進めてこれたのでした.
Online Python Tutor でも,デフォルトの設定では,整数や実数などについては「荷札のモデル」で可視化するのを省略しています.省略せずに可視化するには,コード入力欄の下のドロップダウンリストで「inline primitives」の代わりに「render all objects on the heap」を選択してください:
x = 10
y = x
y = 123
を入力して Visualize Code すると,先ほどの説明のように「紐が切り離される」のがわかると思います.一つ前のリストの場合のコード例も試してみてください.リストの要素として直列に並んでいるのも「荷札」であり,整数データへの紐が延びているのを確認できます.
結局,リストと整数の挙動の違いをもたらしたのは,リストは要素を変更可能なのに対して,整数は変更不能である (というか,変更できる「要素」に相当するものが存在しない) という点に尽きます.実際にはすべてが「荷札モデル」で動きますが,変更不能なものは「箱モデル」で考えても矛盾しません.
「A から延びた紐が B に繋がっている」というのはもちろん正確な用語ではありません.より正しい表現は「A は B への参照 (reference) を保持している」「A は B を参照している (A references B, A refers to B)」「A が B を指している (A points to B)」などです.
コンピュータの内部では,メモリ内の特定の場所にデータを書き込んだりそこから読み出したりする場合に,アドレスと呼ばれる数値を使って場所を指定します.x = 10
を実行すると,数値 10 がメモリのどこかに書き込まれ,変数 x
はそのアドレスを保持します.
ある変数が保持しているアドレスを知るには id
という関数を使うことができます:
>>> x = 10
>>> id(x)
2528242330192
>>> y = x
>>> id(y)
2528242330192
>>> y = 123
>>> id(y)
2528242522288
id
が返す具体的な数値にはあまり意味がありません.ここでは,1
回目の id(y)
は id(x)
と同じ値であること,2回目の
id(y)
は値が変わっていることにだけ注意してください.
もう一つポインタと大きく違うのは,実行できる操作が厳しく制限されていることです.例えば C ではポインタに整数を加減算したりしてアドレス計算をすることができますが Python ではできません.このように制限することで,C のポインタより安全に扱えるようになっています.
4.4.4. is と == (is not と !=)¶
以上のように理解すると,2 つのデータが互いに「等しい」というときに,異なる 2 種類の意味があることがわかります.
ほとんどの場合に重要なのは,それらが「同じ値を持っているかどうか」
(等価性, equality) です.演算子 ==
や !=
で判定するのはこれです.例えば:
>>> x = [10, 20, 30]
>>> y = [10, 20, 30]
>>> x == y
True
となります.しかしこのとき x
と y
が指しているデータの実体は別物です.例えばこの直後に y[0] = 99
としても x[0]
は変わりません.別物だからです.
この「実体が同じものであるかどうか」(同一性, identity) を判定するときには,演算子 is
と is not
を使います.先ほどの x
と y
は等価ですが同一ではありません:
>>> x is y
False
>>> x is not y
True
となります.一方,以下のように y
が x
と同じものを指すようにすると同一になります:
>>> y = x
>>> x is y
True
C言語に慣れている人は,ポインタの比較に相当するのが is
や is
not
だと考えるのがわかりやすいかもしれません.
4.4.5. 実践編¶
「hello, world」の文字列を,マウスの左ボタンが押されている間だけ表示するようにします.
27def draw(screen, player_image, text_image, mouse_pos):
28 screen.fill(pygame.Color("black"))
29 screen.blit(player_image, mouse_pos)
30 if text_image is not None:
31 mouse_x, mouse_y = mouse_pos
32 text_offset_x = 100
33 screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
34 pygame.display.update()
44 while True:
45 frames_per_second = 60
46 clock.tick(frames_per_second)
47
48 should_quit = False
49 for event in pygame.event.get():
50 if event.type == pygame.QUIT:
51 should_quit = True
52 elif event.type == pygame.KEYDOWN:
53 if event.key == pygame.K_ESCAPE:
54 should_quit = True
55 elif event.key == pygame.K_b:
56 pass
57 if should_quit:
58 break
59 mouse_pos = pygame.mouse.get_pos()
60 buttons_pressed = pygame.mouse.get_pressed()
61 if buttons_pressed[0]:
62 text_image_shown = text_image
63 else:
64 text_image_shown = None
65
66 frame_index += 1
67 animation_period = 6
68 animation_index = (frame_index // animation_period) % len(player_images)
69 draw(screen, player_images[animation_index], text_image_shown, mouse_pos)
- 27-34行目
draw
関数を修正して,引数text_image
がNone
だったら表示しないようにします.None
との比較には==
ではなくis
を使います.None
は,存在し得る他のデータとは絶対に同一にならないと定められているオブジェクトです.同一かどうかの判定なのでis
を使います.- 60-64行目,69行目
マウス左ボタンが押されているなら,変数
text_image_shown
にtext_image
が指すSurface
型オブジェクトを,押されていないならNone
を割り当てます.draw
の呼び出し時には第3引数としてこれを渡します.関数
pygame.mouse.get_pressed
は,pygame.key.get_pressed
と同じような要領で,各マウスボタンが押されているかどうかの真偽値をシーケンスとして得ます.左ボタンのインデックスは 0 です.
4.4.6. 関数呼び出しと変数の関係¶
関数を呼び出す場合の引数,返り値,ローカル変数の挙動を正しく理解しておくことは重要です.以下のコードを Online Python Tutor で「render all objects」を指定して実行してみてください:
def func(x, a):
x[0] = 123
a = 456
y = [99, 88, 77]
return y
def main():
xm = [10, 20, 30]
am = 1
ym = func(xm, am)
return
main()
いくつか重要なことが理解できると思います.
- 関数が呼び出されると,それぞれの関数用にフレーム (frame) (映像のフレームとは別の用語) と呼ばれるものが作られて,引数やローカル変数はその中に置かれます.フレーム内に置かれるのは「荷札」だけです.実際のデータは決して置かれません.
- 引数や返り値の受け渡しは,代入文と同様に行われます.つまり,送り元の変数が指しているデータを,送り先の変数からも指して共有します.
- 関数の実行中,その関数のフレームは保持されます.そこから他の関数を呼び出している間も保持されます (例えば
main
からfunc
を呼んでいる間もmain
フレームは存在しています).しかし,関数からリターンするとその関数のフレームは削除され,その中にあった変数も削除されます. - 変数が削除されても,それが指しているデータが削除されるわけではありません.
最後の項目は注意が必要です.例えば上の例で func
からリターンする前後の挙動に注目してください.変数 y
が指しているリスト [99, 88,
77] は関数 func
の中で作られたものです.func
からリターンすると変数 y
という「荷札」は消えますが,それが指している [99, 88,
77] まで消えるわけではありません.このリストは変数 ym
が指すようになり, main
関数の中で利用できます (この例では何の利用もせずに終わりますが).
では,一度作成されたデータはずっと残るのかというとそうではなく,現在残っているどの変数からも参照されなくなったものは,自動的に削除されます.例えば a
が指していた整数 456 は, func
からリターンすると削除されます.
データの削除が起きるのは関数からのリターン時に限りません.例えば:
x = 100
x = 200
のような2行を実行すると,1行目で作られた整数データ 100 は,2行目を実行した時点でどこからも参照されなくなるので削除されます.
Online Python Tutor のトップページ https://pythontutor.com/ で, Related services のところの C Tutor を開いてください.C のコードの実行状況を同様に可視化できます.
さっきの Python コードに相当するものを C で書くと以下のようになると思います.これを C Tutor で可視化してみてください:
int *func(int *x, int a) {
x[0] = 123;
a = 456;
int y[3] = {99, 88, 77};
return y;
}
int main() {
int xm[3] = {10, 20, 30};
int am = 1;
int *ym = func(xm, am);
return 0;
}
すぐに気づくのは,整数も配列も,可視化画面の左側の「フレーム」内に置かれるということです.右側の領域に矢印が延びることはありません.値を再代入すると,フレーム内に置かれた変数の中身が直接書き換わります.つまり,C の場合これらの変数は「箱モデル」で動いているわけです.
関数 func
の実行中は,func
のフレーム内に 3 要素の配列
y
が保持されます.return y;
によってその先頭アドレスが
main
に返されるのですが,その時点で func
のフレームは破棄されるので,main
内の ym
は何も指しません (選択したコンパイラのバージョンによって,NULL
だったりゴミだったりするはずです).もし main
側でこの後 ym[0] = 999;
などとしようものなら,実行時エラーが発生します.
これが「C 言語では関数内で定義したローカル配列をリターンするな」と教わる理由です.
では,Python ではなぜリストをリターンしても大丈夫なのかというと,リスト (に限らずあらゆるデータ) は「右側の領域」に確保されており,関数のフレームとともに破棄されるのはデータを指している「荷札」だけだからです.
Python の動作に近いことを C で実現するなら以下のようになります:
#include <stdlib.h>
int *func(int *x, int a) {
x[0] = 123;
a = 456;
int *y = (int *)malloc(sizeof(int) * 3);
y[0] = 99;
y[1] = 88;
y[2] = 77;
return y;
}
int main() {
int *xm = (int *)malloc(sizeof(int) * 3);
xm[0] = 10;
xm[1] = 20;
xm[2] = 30;
int am = 1;
int *ym = func(xm, am);
return 0;
}
malloc
(Memory ALLOCation) 関数は初めて見た人もいるかも知れません.細かい話を抜きにすると,この例では,整数 3 つ分の配列を「右側の領域」に確保する仕事をします.このようにすると,func
のフレームが破棄された後も y
が指していた配列データは生き残り,main
関数内の変数 ym
を通して読み書きできます.
問題は,(この例では main
もこの後すぐ終了するので問題ありませんが,) 仮にこの後プログラムの実行がさらに継続するとして,これらの配列データがどこからも参照されなくなっても,Python のように自動で解放してはくれないことです.C で malloc
関数を使ってメモリを確保したときは,プログラマが自分の責任で free
関数を呼び出して解放する必要があります.これは煩雑で,注意して書かないとバグの温床になります.
メモリ領域の確保というのは時間のかかる重い処理です.参照されなくなったデータを検出して解放する (ゴミ集め,garbage collection と呼ばれます) のはさらに重い処理です.すべてのデータについてこれらをやっているとプログラムの実行が遅くなります.そのため C ではすべてプログラマに任せる方針を取っています.
このあたりのバランスの取り方はプログラミング言語によってさまざまです.C++ は基本的には C と同じですが,スマートポインタという仕組みを使うことで必要に応じて Python と同様の自動メモリ管理を行えます.
Java では,int
や float
といった基本型のデータは C と同様ですが,配列やユーザ定義型などは Python と同様の方式で自動管理されます.「じゃあ int
や float
を自動管理することはできないの?」というと,そのために Integer
や Float
という型が別途用意されていて,そちらを使うと Python 方式になるというアクロバティックな仕組みが採用されています.
実際のところ,Python ではすべてのデータが (それどころか関数やモジュールすらも) オブジェクトとして扱われます.なので「これはオブジェクトですか? オブジェクトではないですか?」と問うこと自体にあまり意味がありません.ここで重要なのは,オブジェクトと,それを指す変数 (= 荷札) がしっかり区別できることだけです.
4.4.7. リストを使いこなす¶
リストの要素には異なる型のものが混在して構いません.「荷札」が並んでいるだけなので,それらが指す先はどんな型のものでもよいのです:
>>> x = [10, 20, 3.14, "abc"]
>>> x
[10, 20, 3.14, 'abc']
したがって,リストもリストの要素になれます.これを利用するとリストだけでかなり複雑なデータ構造を構築できます:
>>> x = [10, 20, [99, 88, ["a", "b", "c"]], 40, 50]
>>> x
[10, 20, [99, 88, ['a', 'b', 'c']], 40, 50]
>>> x[2]
[99, 88, ['a', 'b', 'c']]
>>> x[2][2]
['a', 'b', 'c']
>>> x[2][2][2]
'c'
どういうデータ構造になっているか想像できるでしょうか.ぜひ Online Python Tutor で可視化してみてください.
代入文 y = x
では,y
が x
と同じものを指すようになるだけで,データのコピーは全く発生しないのでした.ではコピーをするにはどうしたらよいでしょうか.一つの方法は,list
型のメソッド copy
を使用することです.Online Python Tutor で以下を可視化してみてください:
x = [10, 20, [99, 88, ["a", "b", "c"]], 40, 50]
y1 = x
y2 = x.copy()
x.copy()
と全く同じことが,「最初の要素から最後の要素までのスライス」 x[:]
を指定することでも行えます.以下の行を追加して確認してください:
y3 = x[:]
このどちらの方法でも,コピーされるのはあくまでリストの「1段目」だけであることに注意してください.つまり,x
が指している先で直列に並んだ荷札たちをコピーしているだけです.荷札のさらにその先まではコピーしないので,「2段目」以降は共有されます.これを浅いコピー (shallow copy) と呼びます.
2段目以降もすべてコピーして完全に独立したデータを作り直したい場合もあるかも知れません.深いコピー (deep copy) と呼ばれます.以下のようにコツコツとやればよいのですがちょっと面倒です:
x = [10, 20, [99, 88, ["a", "b", "c"]], 40, 50]
y4 = x.copy()
y4[2] = x[2].copy()
y4[2][2] = x[2][2].copy()
Python 標準の copy
モジュールに deepcopy
という関数が用意されており,これを使うと一発でやってくれます.コツコツ作ったのと同じ結果になることを Online Python Tutor で確認してください:
import copy
x = [10, 20, [99, 88, ["a", "b", "c"]], 40, 50]
y5 = copy.deepcopy(x)
4.5. 演習¶
例題4-2
対話的実行で以下のようなリストの操作を行ってください.実行前に結果
(「???
」のところ) を予想して,実際の結果と一致するか確認してください.予想と違ったら Online Python Tutor で確認し,正しい理解をするようにしてください.
>>> x = [10, 20, 30]
>>> y = x
>>> x.append(40)
>>> y
???
>>> x = [10, 20, 30]
>>> y = x
>>> x = [10, 20, 30, 40]
>>> y
???
>>> x = [10, 20, 30]
>>> y = x
>>> x[:] = [10, 20, 30, 40]
>>> y
???
>>> x = [10, 20, 30]
>>> x[1] = [99, 98, 97]
>>> x
???
>>> def func(x):
... x[0] = 99
...
>>> x = [10, 20, 30]
>>> func(x)
>>> x
???
>>> def func(x):
... x = [99, 98, 97]
...
>>> x = [10, 20, 30]
>>> func(x)
>>> x
???
4.6. docstring を書く¶
hello_pygame.py
の開発を例とした Python の基本事項の確認は以上で終わりです.最後に,docstring について紹介します.
docstring は,ファイルの先頭,関数の先頭などに記載する説明用コメントです.VSCode で関数名の上でマウスホバーしたとき,説明がポップアップしてくることに気づいた人もいると思います.自分で作った関数でもこれが出てくると便利ですし,より一般に,後からコードを読む人 (それには数か月後の自分も含まれます) の助けになります.
関数 init_screen
と draw
の場合の簡単な記載例を示します.
def init_screen():
"""Initialize pygame screen.
Library pygame is initialized and screen size is set to 600x400.
Returns
-------
Surface
Screen surface
"""
pygame.init()
width, height = 600, 400
screen = pygame.display.set_mode((width, height))
return screen
def draw(screen, player_image, text_image, mouse_pos):
"""Draw images onto screen.
player_image is drawn at mouse_pos on screen, and text_image,
unless it is None, is drawn to the right of player_image.
Parameters
----------
screen : Surface
Screen onto which images are drawn.
player_image : Surface
Player image to be blit'ed.
text_image : Surface
Text image to be blit'ed. If None is passed, nothing is drawn.
mouse_pos : tuple[int, int]
Position where player_image is blit'ed.
"""
screen.fill(pygame.Color("black"))
screen.blit(player_image, mouse_pos)
if text_image is not None:
mouse_x, mouse_y = mouse_pos
text_offset_x = 100
screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
pygame.display.update()
関数定義の場合,def
行の直後に二つの """
で挟んで記載します.正確には """..."""
はコメントではなく,改行を含むことのできる文字列です.文字列だけの文は実行に何も影響しないので,事実上コメントとして扱えます.しかしインデントレベルは守る必要があることに注意してください.
docstring の書き方には,Google スタイル,numpy スタイルなどいくつかの流儀がありますが,共通するのは,最初に1行で簡潔な要約を書き,1行空けてから,詳しい説明を書くことです.詳しい説明のところには,動作,引数の型と説明,返り値の型と説明などをそれぞれのスタイルで記載します.上の例は numpy スタイルです.
それよりも,各行が何をしているのか,コメントが無くても自然に読めるように変数名や関数名を工夫する方が後々有益です.
その上で,それでもわかりづらいところ,なぜそのように書く必要があったのか説明が必要なところ,どういう意図でそのような処理をしているのか説明が必要なところなどに限定してコメントを入れていく方が読みやすいプログラムになります.
4.7. 演習¶
例題4-3
前の章の演習で,キーが押されている間文字が変わるような修正をしました.この章ではマウスボタンが押されたときだけ表示するようになったので,マウス左ボタンが押下されるたびに文字の内容や色が変わるようにしてください.
(ヒント: Surface
のリスト text_images
を作成し,
player_images
を参考にして表示することを考えるとよいです.ボタン押下を検出するには MOUSEBUTTONDOWN
イベントを使います)
例題4-4
プレイヤが動いていないときは足踏みを止めるようにしてください.
(ヒント: pygame.mouse.get_rel
関数を調べてください.マウスの相対移動量が得られます.その量に応じて player_images
に与えるインデックスの計算を考えるとよいでしょう)
例題4-5
プレイヤの動きの速さによって足踏みの速さを変えるようにしてください.
(ヒント: pygame.mouse.get_rel
から得た移動距離を積分すれば累積移動距離が得られます.距離の算出には math.sqrt
が使えます)
例題4-6
assert
文は,assert 式
と書くことで,式
が True
なら何もせず,False
ならエラーを発生させます.プログラムが想定通りの挙動をしているかを確認する際に便利です.以下の assert
が通るように関数を修正してください:
def set_to_zero_vector(vec, n_elements):
vec = [0] * n_elements
x = []
set_to_zero_vector(x, 3)
assert x == [0, 0, 0]
y = [10, 20]
set_to_zero_vector(y, 5)
assert y == [0, 0, 0, 0, 0]