3. 基本的なプログラミング (制御構造編)¶
この章と次の章では,前回から引き続いて hello_pygame.py
を拡張しながら,
Python の基本事項を確認していきます.
Python について既によく知っている人にとっては,多くの事項が復習になります.文法説明はいったん読み飛ばして実践と演習から見ていき,わからなかったら説明に戻るのでもよいかも知れません.
逆に,ここの文法説明が簡潔過ぎる場合は付録A のミニ・チュートリアルを参照してください.それでも難しい場合は,(Python の初心者向けではなく) プログラミングの初心者向けの Python 入門書を読む方がよさそうです.
3.1. 基本的な計算¶
3.1.1. 文の実行¶
- 基本的に 1 行が 1 文です.勝手に改行してはいけません.
- 行が長くなりすぎて困るときは,以下のルールを適用して短くできます.
- 行末に
\
を置くと,その行は次の行に継続されます. - かっこ類
()
[]
{}
の中では自由に改行して構いません.
- 行末に
- 字下げ (インデント) にも意味があるので,行の先頭に勝手に空白を入れてはいけません.
#
から行末まではコメントです.実行結果に影響しません.
伝統的には,1 行は 80 文字に収めるべきであるとよく言われていて, Python でもそうするべきだと考える人が多いようです.しかしこの 80 という数がどこから来たかというと,大昔のコンピュータの画面が表示できる最大文字数だったり,さらに遡るとパンチカードの桁数だったりするので,実のところあまり強い根拠のある数ではありません.現代では 80 文字はちょっと少なすぎるように思われます.
一方で,人間の視野の広さや視線移動能力には限界があるので,やはり長すぎるのもよくありません.
100 文字というのは,VSCode の中で文法チェック・スタイルチェックを行っている pylint の標準設定です.配布環境ではこの設定のままにしています.
また,エディタ領域には 80 文字と 100 文字のところに縦線が表示されるようにしています.目安として活用してください.
3.1.2. 変数と演算¶
多くのプログラミング言語と同様に算術演算子と丸かっこで数式を表せます:
(2 + 3) * 4 # (2 + 3) × 4 = 20
10 / 4 # 2.5
10 // 4 # 2 (整数除算)
10 % 3 # 剰余
10 ** 3 # べき乗
代入文により左辺の変数に右辺の値を割り当てることができます:
pi = 3.14
radius = 5
area = pi * radius ** 2
C言語のような累積代入演算子も使えます:
x += 2
x *= 5
並列代入が特徴的です:
x, y = (100, 200)
x, y = y, x
3.1.3. 型と型変換¶
値にはそれぞれが属する型があります:
5 # 整数型 (int 型) の値
5.0, 2e-3 # 実数型 (float 型) の値
True, False # 真偽値型 (bool 型) の値
型名を関数のように使うことでその型に型変換できます:
float(5) # 5.0
int(2.5) # 2
bool(1) # True (0 以外は True に変換される)
int x = 0;
などと型名を書く必要がないので,型なんてないのだと思っていたのですが.まず,変数とそこに割り当てられているデータ (値) を区別して考えるようにしてください.Python で x = 30
と書くと変数 x
に整数型のデータ 30 が割り当てられます.30 には整数型という型がありますが,x
にはそのような指定がありません.その直後に x =
"abc"
と文字列型データを割り当てても構いません.
C 言語では,プログラムの実行が開始される前に,すべての変数の型 (つまり,どの型のデータを割り当ててよいか) が決まります.この性質を静的型付け (static typing) といいます.一方 Python のように実行時に決まる場合を動的型付け (dynamic typing) と呼びます.
Surface
型も型の一種ですよね? int
や
float
に型変換できるんですか?Surface
から int
や float
への変換は定義されていません.3.1.4. 実践編¶
変数は,同じ値を何度も使う際に便利なのは言うまでもないですが,たとえ 1 回しか使わないときでもコードを読みやすくするのに役立ちます.
例えば hello_pygame.py
では screen =
pygame.display.set_mode((600, 400))
のようにしてウィンドウを作成しましたが,後から見たときに「この 600 って何だっけ? 400 って何だっけ?」とわからなくなりがちです.変数を導入して適切な名前をつけておくと読みやすくなります.(説明的な変数 (explanatory variable, explaining
variable) と呼ばれます)
この例のように,プログラムの機能を変えることなく,プログラムをより読みやすく,よりメンテナンスしやすく修正することをリファクタする (refactor) といいます.
1import pygame
2
3pygame.init()
4width, height = 600, 400
5screen = pygame.display.set_mode((width, height))
6font_size = 50
7font_file = None
8antialias = True
9font = pygame.font.Font(font_file, font_size)
10text_image = font.render("hello, pygame", antialias, pygame.Color("green"))
11player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
12
13while True:
14 pygame.event.clear()
15 key_pressed = pygame.key.get_pressed()
16 if key_pressed[pygame.K_ESCAPE]:
17 break
18 mouse_pos = pygame.mouse.get_pos()
19
20 screen.fill(pygame.Color("black"))
21 screen.blit(player_image, mouse_pos)
22 mouse_x, mouse_y = mouse_pos
23 text_offset_x = 100
24 screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
25 pygame.display.update()
26
27pygame.quit()
変数や関数の名前のように,プログラマが各自で導入する名前を識別子といいます (これに対して,if
や while
のようにあらかじめ決められているものをキーワードまたは予約語といいます).
識別子には,大文字と小文字のアルファベット,アンダースコア
_
,数字が使えますが,ただし最初の文字を数字にしてはダメ,と覚えておきましょう.(実は日本語の文字も使えたりしますが,やめておく方がよいです)
アンダースコア 2 個で始まる識別子は特別な用途のものが定義されているため,自分で勝手に使ってはいけません.(例: __init__
とか
__name__
とか)
Python 業界の慣習として,変数や関数名は snake_case
のようにアルファベット小文字で,単語区切りをアンダースコア _
で表現する方法が好まれます.ただし,定数は大文字で UPPER_CASE
のように表現することが推奨されています.
width
や height
とか font_size
とかは定数だと思うのですが,なぜ大文字にしないのですか?このバージョンのプログラムでは,確かにこれらは定数です.しかし今後プログラムを発展させていく中で,定数ではなくなるかもしれません (例えば将来ウィンドウサイズやフォントサイズを動的に変更する機能を追加するかもしれません).そしてその頃にはこれらの変数名をプログラム中のあちこちで使用しているかも知れません.
現在のプログラムで一見「定数」のように振舞うからといって大文字にしてしまうと,そのような変更の必要が生じたときに,すべての出現箇所を小文字に変換して回る必要があります.これは全くもってナンセンスなことです.
以上のことから,「定数名は大文字にする」という Python の慣習は,将来にわたって定数であり続けると確信できるときにのみ適用するべきだというのが執筆者の個人的な考えです.
3.2. 関数¶
3.2.1. 関数定義と関数呼び出し¶
関数定義は以下の形式で書きます:
def 関数名(引数1, 引数2, 引数3):
関数本体の処理
...
return 返り値
- 引数が 0 個の場合は丸かっこ内は空にします.
return
文は,返り値がない場合はreturn
とだけ書きます.return
文に到達せずに関数を抜けた場合は,返り値のないreturn
文を実行したのと同じことになります.- 関数の中で定義された変数は,その関数の中だけで使えるローカル変数になります.
関数呼び出しは,関数名の後ろに丸かっこをつけると行えます:
関数名(引数1, 引数2, 引数3)
数学でいう「関数」は,引数を定めると,返り値が一意に定まるものです.また,値を返す以外の副作用 (例えば外部の変数の値が変わるとか,ファイルの内容が書き換わるとか) は起きません.このような関数をプログラミング言語の分野ではわざわざ「純粋関数」と呼びます.
Python や C の関数は,あくまで一連の処理に名前を付けて呼び出せるようにしたものであり,純粋関数を表すのに使うこともできますが,そうでない用途にも使えます.例えば引数としてファイル名を受け取り,そのファイルの内容に書かれた数値を読み出して値として返す関数があったとします.この関数は,同じ引数を渡して呼び出してもファイルの内容によって返り値が変わりますので,明らかに純粋関数ではありません.
純粋かどうかは別としても,値を返さないものを関数と呼ぶのに抵抗があるのはよくわかります.実際,プログラミング言語によっては,特に歴史の古い言語では,値を返さないものは手続き (procedure), サブルーティーン (subroutine),サブプログラム (subprogram) などと呼んで関数とは別の文法を用意しているものもあります.
関数定義の際に,引数にデフォルト値を指定できます:
def func_abc(a, b=20, c=10):
return (a + b) * c
y = func_abc(2, 3, 5) # (2 + 3) * 5 = 25
z = func_abc(2, 3) # (2 + 3) * 10 = 50
関数呼び出しの際に,引数をキーワード指定できる場合があります:
y = func_abc(2, b=3, c=10) # (2 + 3) * 10 = 50
z = func_abc(a=2, c=3) # (2 + 20) * 3 = 66
これらをうまく組み合わせると,関数呼び出し時の引数指定が簡潔になります.
まず,関数を定義する際,デフォルト値のある引数は,そうでない引数よりも後ろにする必要があります.これは考えてみると当たり前ですね.省略できるものの後ろに省略できないものがあると困ります.
次に,関数を呼び出す際,キーワード引数は,そうでない引数より後ろに置く必要があります.キーワード引数は順不同で指定できるので,その後ろに普通の引数があると混乱するからです.
以上の基本事項のほかに,関数定義時に「これはキーワードでしか指定できない引数」「これはキーワードでは指定できない引数」といった制限をつける方法があります.ある関数にこの制限があるかないかは,マニュアルを見てわかる場合もあればわからない場合もあります.pygame のリファレンスマニュアルは残念ながらわからないタイプです.試してみて初めて「あー,この関数はキーワード引数使えないんだ」とわかる場合がありますが,しかたないです.
3.2.2. 実践編¶
これまでの hello_pygame.py
では,関数に分けずにすべての処理をベタ書きして来ましたが,だんだん規模が大きくなったので分けていくことにします.
プログラム中の複数個所で同じ処理が行われているときにそれを関数にまとめるべきなのはもちろんですが,関数が有用なのはその場合に限りません.
まず,たとえ1回しか実行されないとしても,一連の処理に名前をつけることで,何をしようとしているのかが明確になります.
また,関数の中で定義した変数は,その関数の中でしか使えないローカル変数 (局所変数, local variable) になります.変数の影響を局所化することで,プログラムの挙動を把握しやすくなり,結果として読みやすくミスの入りにくいプログラムになります.
これに対して現状のプログラムでは,すべての変数がプログラム全体から見えるグローバル変数 (大域変数, global variable) になっていて,ある変数がプログラムのどの部分で使われているのか,プログラムのどこで変更され得るのか,常に気にしておく必要があります.
この実習では,以降の章でも,規模が拡大するとともに複雑になりがちなプログラムをうまくマネージするための考え方を紹介していきますが,すべてに共通するのは,長時間にわたって同時に把握しておくべき要素の数を減らすことです.グローバル変数の回避はそのための基本中の基本です.
関数に分割していく第一歩として,これまでの処理をすべて 1 つの関数に移します.関数名は自由に決めてよいのですが,ここでは (C言語に倣って) main
としましょう.
1import pygame
2
3
4def main():
5 pygame.init()
6 width, height = 600, 400
7 screen = pygame.display.set_mode((width, height))
8 font_size = 50
9 font_file = None
10 antialias = True
11 font = pygame.font.Font(font_file, font_size)
12 text_image = font.render("hello, pygame", antialias, pygame.Color("green"))
13 player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
14
15 while True:
16 pygame.event.clear()
17 key_pressed = pygame.key.get_pressed()
18 if key_pressed[pygame.K_ESCAPE]:
19 break
20 mouse_pos = pygame.mouse.get_pos()
21
22 screen.fill(pygame.Color("black"))
23 screen.blit(player_image, mouse_pos)
24 mouse_x, mouse_y = mouse_pos
25 text_offset_x = 100
26 screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
27 pygame.display.update()
28
29 pygame.quit()
30
31
32main()
- 1行目
import
文は通常は関数の中には入れませんのでそのまま残します.- 4行目
def main():
で関数定義を始めます.引数はないので丸かっこの中は空にします.- 5-29行目
これまでのコードをそっくり移します.ただし,単にペーストするだけではなく,先頭にインデントを入れる必要があります.
1 行ずつちまちまと Tab キーで揃えるのはとてもやってられないので,一括でインデントを変えるショートカットキーを覚えておきましょう.対象となるコード部分をドラッグして選択してから,以下のキーを押します.
- Ctrl + ]: 一括でインデントを 1 レベル追加
- Ctrl + [: 一括でインデントを 1 レベル削除
値を返す必要もないので,
return
文は省略しています.return
とだけ書いた行を最後に入れても構いません.- 32行目
- 定義した
main
関数を呼び出します.引数がない場合も,空の()
をつける必要があります.
関数1個だけだと切り分けた甲斐がないので,さらに切り出して行きます.22-27行目の描画処理の部分を draw
という関数に分けてみます.さっきの main
と同様に:
def draw():
screen.fill(pygame.Color("black"))
screen.blit(player_image, mouse_pos)
mouse_x, mouse_y = mouse_pos
text_offset_x = 100
screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
pygame.display.update()
を定義して,元の場所に draw()
と書いて呼び出す…だけでは済みません.やってみるとエラーが発生して動かないはずです.
なぜならば,text_image
や player_image
などが main
関数の中だけで使えるローカル変数だからです.
draw
関数の中では使えません.
そこで,必要なものは引数として渡してやることにします.
1import pygame
2
3
4def draw(screen, player_image, text_image, mouse_pos):
5 screen.fill(pygame.Color("black"))
6 screen.blit(player_image, mouse_pos)
7 mouse_x, mouse_y = mouse_pos
8 text_offset_x = 100
9 screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
10 pygame.display.update()
11
12
13def main():
14 pygame.init()
15 width, height = 600, 400
16 screen = pygame.display.set_mode((width, height))
17 font_size = 50
18 font_file = None
19 antialias = True
20 font = pygame.font.Font(font_file, font_size)
21 text_image = font.render("hello, pygame", antialias, pygame.Color("green"))
22 player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
23
24 while True:
25 pygame.event.clear()
26 key_pressed = pygame.key.get_pressed()
27 if key_pressed[pygame.K_ESCAPE]:
28 break
29 mouse_pos = pygame.mouse.get_pos()
30 draw(screen, player_image, text_image, mouse_pos)
31
32 pygame.quit()
33
34
35main()
繰り返しになりますが,グローバル変数を避けることで,関数の中だけを考えてプログラムすることができるようになり,同時に考えなくてはならない要素が減ります.引数や返り値を導入して…という作業は面倒に思えるかも知れませんが,一度やってしまえば,長期的には楽になるはずです.
処理を関数に切り分けた場合でも関数の外側で変数を定義することは依然として可能で,それらはグローバル変数になります.この実習ではプログラムの大規模化に備える方法論を学ぶため,できるだけグローバル変数を避ける方針で進めます.
どうしてもグローバル変数を使いたい場合は,読み取り専用での使用に限定しましょう.実行中に値が書き換わらないのであれば,混乱は最小限度で済むことが期待できるからです.
同じ調子で,pygame を初期化してウィンドウを作る処理と,文字画像を作る処理をそれぞれ関数に切り出しましょう.
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 draw(screen, player_image, text_image, mouse_pos):
21 screen.fill(pygame.Color("black"))
22 screen.blit(player_image, mouse_pos)
23 mouse_x, mouse_y = mouse_pos
24 text_offset_x = 100
25 screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
26 pygame.display.update()
27
28
29def main():
30 screen = init_screen()
31 text_image = create_text()
32 player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
33
34 while True:
35 pygame.event.clear()
36 key_pressed = pygame.key.get_pressed()
37 if key_pressed[pygame.K_ESCAPE]:
38 break
39 mouse_pos = pygame.mouse.get_pos()
40 draw(screen, player_image, text_image, mouse_pos)
41
42 pygame.quit()
43
44
45main()
関数 init_screen
と create_text
には引数はありません.一方,それぞれの関数定義の最後には,結果 (いずれも Surface
型の画像データ) を返すための return
文を付け加えています (8行目と17行目).
3.3. 制御文¶
3.3.1. 条件文¶
if
文は以下の形式です (elif
や else
は適宜省略可):
if cond1:
print("cond1 is True")
elif cond2:
print("cond1 is False; cond2 is True")
elif cond3:
print("cond1 and cond2 are False; cond3 is True")
else:
print("cond1, cond2, and cond3 are all False")
cond1
や cond2
などの条件のところには真偽値型の値を指定します:
a == 10
a != 10
(not 2 > a) or (not a >= 8)
C言語等に慣れている人は,else if
の代わりに elif
と書かなくてはならないことに注意してください.
3.3.2. 繰り返し文¶
while
文と for
文は以下の形式です:
while x > 0:
print(x)
x -= 1
for x in (10, 20, 30):
print(x + 3)
C言語などに慣れた人が見ると for
文は奇異に見えるかもしれません.上の
for
文を実行すると:
13
23
33
が表示されます.for x in
の後ろのタプルの中の値が変数 x
に順に割り当てられて print(x + 3)
が実行されます.タプルの部分には,タプル以外にも同様に何らかのデータが直列に並んだもの
(Python ではシーケンス (sequence) と総称します) を置くことができます.
- 繰り返し文の中では
break
文 (ループから抜ける) やcontinue
文 (ループのその回をスキップする) を使えます. while
やfor
などのブロックを有効範囲とするローカル変数は作れません.関数ローカルしかありません.
3.3.3. 実践編¶
これまでの hello_pygame.py
では,ループの頭で
pygame.event.clear
を呼び出すことでイベントの発生を無視していました.そのためウィンドウ右上の × ボタンも効きませんでした.
イベントの情報を入手するには,代わりに pygame.event.get
関数を使います.この関数は,その時点までに発生したイベントを単にクリアするのではなく,発生順に一列に並んだシーケンスとして返します.シーケンスなので
for
文で 1 つずつ処理することができます.
pygame.event.get
が返すシーケンスの要素は pygame が定義する Event
型 (正確には pygame.event.Event
型) のオブジェクトです.詳細はリファレンスマニュアル
に記載されているのですが,具体例を見る方が早いです.
29def main():
30 screen = init_screen()
31 text_image = create_text()
32 player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
33
34 while True:
35 should_quit = False
36 for event in pygame.event.get():
37 if event.type == pygame.QUIT:
38 should_quit = True
39 elif event.type == pygame.KEYDOWN:
40 if event.key == pygame.K_ESCAPE:
41 should_quit = True
42 if should_quit:
43 break
44 mouse_pos = pygame.mouse.get_pos()
45 draw(screen, player_image, text_image, mouse_pos)
46
47 pygame.quit()
48
49
50main()
この例では,ウィンドウ右上の × ボタン等による実行終了イベントと,Esc キーの押下イベントを検出して,どちらの場合もプログラムを終了します.キーの押下はこれまでは pygame.key.get_pressed
関数で検出してきましたが,今回の方法の方が取りこぼしのおそれがなく確実です.
- 35行目
- 終了するべきかどうかを表す変数
should_quit
を用意します.注目しているイベントが来なかったら「終了しない」ので,最初はFalse
にしておきます. - 36行目
- イベントのシーケンスから 1 つずつ変数
event
に取り出します. - 37-38行目
Event
型のオブジェクトにはtype
という変数が所属しています (Surface
型のオブジェクトにfill
とかblit
というメソッドが所属しているのと同じです).Python では,このようにオブジェクトに属する変数を属性 (attribute) と呼びます.取り出したイベントの種類 (
type
) がpygame.QUIT
という pygame が用意する定数と等しければ,ウィンドウ右上の × ボタンが押されたイベントだということを意味します.その場合はshould_quit
をTrue
にします.- 39-41行目
イベントのタイプが
pygame.KEYDOWN
だったなら,何らかのキーが押されたことを示すイベントだということです.この場合,Event
型オブジェクトのkey
という属性を調べるとキーの種類がわかります.定数pygame.K_ESCAPE
と等しいならshould_quit
をTrue
にします.37行目に始まった
if
文にelse
はありませんので,QUIT
とKEYDOWN
以外のイベントは単に無視することになります.- 42-43行目
- この時点で
should_quit
がTrue
なら,break
文によってwhile
ループを抜け出します.この場合,47行目でpygame.quit
を呼んだ後main
関数から戻り,プログラムは終了します.
should_quit
なんて変数を使わずにその場で break
するのではダメなんですか?should_quit = True
のところで break
しても,for
ループから抜けるだけで while
ループからは抜けられません.pygame.event.get
で取得するように変えなくてよいんですか?ちょっと極端な例で考えましょう.while
ループが 1 秒に 1 回実行されるとします (本当はもっと速いです).キー押下を検出する場合,ある時刻の pygame.key.get_pressed
呼び出し時にはキーは押されておらず,次の時刻 (1秒後) の pygame.key.get_pressed
呼び出し時にもキーは押されておらず,しかしこの 2 時刻の間でキーが押されていたというケースがあり得て,これを検出することができません.これが取りこぼしです.
一方,マウス位置取得については,プレイヤの描画位置を毎時刻に更新するには各時刻の最新のマウス位置がわかれば十分で,時刻間のことを考える必要がありません.
どうしてもイベント処理として扱いたい場合は MOUSEMOTION
というタイプのイベントを使うことになりますが,マウスポインタが動いていないときやウィンドウの外に出ている場合はイベントが発生しないので,かえって使いにくいです.
3.3.4. if __name__ == "__main__"¶
if
文の別の使用例を示します.これまで,main
関数を呼ぶために単にプログラムの一番最後に main()
とだけ書いていましたが,ここは Python の場合は以下のように書くことが推奨されています.
47 pygame.quit()
48
49
50if __name__ == "__main__":
51 main()
これは,このファイルが python コマンドで直接起動されるのではなく他から
import
された場合に備えるためのものです.
直接起動された場合,変数 __name__
には __main__
という文字列が自動的にセットされています.一方 import
された場合はモジュール名 (この場合は hello_pygame
という文字列) がセットされます.結果として,上のような if
文によって,直接起動されたときだけ main
関数が呼び出されるようになります.
python
コマンドから直接実行し,またあるときは他所から import
するという状況が想像できません.こんな対策本当に必要なんですか?例えば,対話的実行によって以下のように実行することを考えます.
hello_pygame.py
に上の修正を加えていたら,以下のように出力されるはずです:
>>> import pygame
pygame 2.0.1 (SDL 2.0.14, Python 3.9.5)
Hello from the pygame community. https://www.pygame.org/contribute.html
>>> pygame.init()
(7, 0)
>>> from hello_pygame import create_text
>>> screen = create_text()
>>> screen
<Surface(230x36x32 SW)>
ここでは,まず pygame を初期化した後,hello_pygame.py
から
create_text
を import
して関数として使えるようにしています.このようにして開発中の関数の動作をチェックしたりすることはよくあります.
もしさっきの修正を行わず hello_pygame.py
の最後に main()
とだけ書いていた場合,from hello_pygame import create_text
の行を実行した時点で main
関数が呼ばれてウィンドウが開き,× ボタンや Esc キーで停止するまで先に進みません.
3.4. デバッグのコツ¶
3.4.1. エラーメッセージを理解する¶
だんだん複雑になってきたので,思った通りに動かない場合も出てくるかと思います.
まず,コードを入力中に赤色のアンダーラインが現れた場合は,かなり高い確率で何かがおかしいです.放置せずに速やかに修正するようにしてください.
実行して,エラーが発生した場合は,エラーメッセージをよく読むようにしてください.エラーメッセージの読み方は前の章の Q&A でも紹介しました.プログラムが複雑になってくると,メッセージの行数も増えて一見難しそうになります.例えば mouse_pos = pygame.mouse.get_pos()
の最後の ()
をつけ忘れたとすると,以下のようなメッセージが現れると思います:
Traceback (most recent call last):
File "c:\cs1\projects\hello_pygame\hello_pygame.py", line 51, in <module>
main()
File "c:\cs1\projects\hello_pygame\hello_pygame.py", line 45, in main
draw(screen, player_image, text_image, mouse_pos)
File "c:\cs1\projects\hello_pygame\hello_pygame.py", line 22, in draw
screen.blit(player_image, mouse_pos)
TypeError: invalid destination position for blit
main
から呼び出された draw
から呼び出された screen.blit
でエラーが起きたことを示しています.こうやって呼び出し履歴の情報が含まれているためメッセージが長くなるのですが,これがあるから,いつ何が起きたかを究明しやすくなります.
最後に書かれているのが発生したエラーです.invalid destination
position for blit
とのことなので,mouse_pos
が何かおかしいということです.重要なのは,このエラーが発生した screen.blit
の行自体が間違っているとは限らないということです.mouse_pos
を定義したところや更新したところまで遡ってチェックしていく必要があることが多いです.そのため,必要に応じて呼び出し履歴を逆順にたどっていく (エラーメッセージを下から上に見ていく) ことになります.
Google でエラーメッセージ (例えば SyntaxError: invalid
syntax
) をそのまま入力して問い合わせると,メッセージを構成する各単語(SyntaxError
, invalid
, syntax
) のうち多くを含むページが検索されます.
単語ごとではなくこのフレーズそのものが含まれるページを見つけたい場合は,
"SyntaxError: invalid syntax"
のようにダブルクォートで囲んで問い合わせます.目当てのメッセージをより確実に見つけられます.
その際,やみくもにメッセージ全体をフレーズ検索すればよいわけではないことに注意してください.例えば:
>>> int("hello, world")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'hello, world'
というエラーが出たとして,この最後の行全体をフレーズ検索すると一致する結果が得られないと思います.最後の 'hello, world'
の部分は今回書いた特定のプログラムの一部であって,一般性がないからです.この部分を除いて "ValueError: invalid literal for int()
with base 10"
で検索すると有用な情報が見つかります.
本当に怖いのはエラーが出ないのに動作がおかしいことで,さらに怖いのは,エラーが出ず,一見動いているように見えるのに,実は間違った結果が出ていることです.だから,エラーが出たら「よかった,間違いを見つけてくれた」と喜ぶべきです.
上級者は,自分が間違いを起こすことを前提として,間違えたらできるだけエラーが起きるように予防的なプログラミングをします.エラーは道具なのです.
assert
文の利用,2 つめは型ヒントです.例としてさっきの mouse_pos = pygame.mouse.get_pos()
のところを考えます.この行の次に:
assert len(mouse_pos) == 2
という行を挿入してみましょう.assert
文は,assert 式
と書くことで,その式が True
なら何もせず,False
ならエラーを発生させるものです.len
は引数として与えられたシーケンスの長さを返す関数です.
この例では,mouse_pos
が長さ 2 のシーケンスであることを期待しています.シーケンスでなければ len
関数がエラーを起こしますし,シーケンスだったとしても長さが 2 でなければ assert
文がエラーを起こします.
このように,プログラムの途中で「当然満たしているべき条件」を assert (表明) しておくと,想定外の挙動を早期に見つけることができます.
もう 1 つの例が型ヒントです.Python では変数に型を指定する必要はありませんが,敢えて指定しておいて,型の整合性を自動でチェックしようというものです.
型の自動チェックを有効にするには,VSCode の設定ファイル
settings.json
を開き,以下の off
の部分を basic
に書き換えて保存します
(settings.json
を開くには,メニューバーから
を開き,
Preferences: Open User Settings (JSON)
を選びます):
"python.analysis.typeCheckingMode": "off",
hello_pygame.py
の編集画面に戻り,さっきの mouse_pos = ...
の行の前に以下の行を挿入してください:
mouse_pos: tuple[int, int]
あるいは mouse_pos = ...
の行自体を以下のようにしてもよいです:
mouse_pos: tuple[int, int] = pygame.mouse.get_pos()
これにより,変数 mouse_pos
には整数 2 つからなるタプル型のデータを割り当て可能であると明示します.
このように書いても,python
コマンドは特に何もしません.しかし外部ツールによって型の整合性をチェックすることができるようになります.試しにさっきのように get_pos
の後ろの ()
をつけ忘れてみると,エディタ領域で青いアンダーラインが現れるはずです.
assert
文によるチェックがプログラムを実行してみないと走らないのに対して,型チェックは実行前に行えて,結果がエディタ領域で即座に確認できることに注目してください.型というのがいかに強力な仕組みかが感じられると思います.Python の「変数に型を指定しなくてよい」という特徴は小規模なプログラムを書くときは手軽で便利ですが,大規模なプログラムを開発する際は,敢えて型を指定することが有用な場合も多いのです.
引き続き型チェックを有効にしたい場合は settings.json
をこのままにしてください.邪魔だと感じる場合は先ほど basic
に書き換えた箇所を off
に戻せばよいです.
3.4.2. デバッガを活用する¶
エラーメッセージを見てすぐに間違いを突き止められれば話は早いのですが,原因がわからない場合,あるいはエラーが起きないのに動作がおかしい場合は,関係しそうな変数などが期待通りの値になっているかを確認する作業が必要です.
例えばさっきの mouse_pos
のエラーの場合なら,pygame.blit
の前の行に print("mouse_pos = ", mouse_pos)
という行を挿入してから実行して出力を見ると,値がおかしいことがわかるはずです:
mouse_pos = <built-in function get_pos>
mouse_pos
には,get_pos
関数が返した結果ではなく,get_pos
関数自身が割り当てられてしまっていたことが読み取れます.
このように,問題がありそうな変数に目星がついているならよいのですが,そうでない場合は,デバッガを活用するのが得策です.今の例なら,
pygame.blit
の行にブレイクポイントを仕掛けて (行番号の左側を左クリックして赤い丸をつける),デバッグ実行 ( )
してください.pygame.blit
が呼ばれる直前で一時停止するので,エディタで変数上にマウスホバーするなり,Run サイドバーで変数を一覧表示するなりして,想定外の値を持っている変数を探すことができます.
エラーが出ない場合は,もっと広範囲を調べる必要があります.今のようなプログラムの場合,main
関数の while
ループ内のどこかにブレイクポイントを仕掛けて毎時刻の各変数の値を調べる…ということをしたくなるのですが,マウス操作との干渉が問題です.開発中のプログラムのウィンドウ上でマウスを動かしていると,VSCode のデバッガ操作ボタン (Continue や Step Over など)
を押せません.
こういう場合は,特定の条件でのみ実行される文をわざと作って,そこにブレイクポイントを仕掛けるとよいです.
34 while True:
35 should_quit = False
36 for event in pygame.event.get():
37 if event.type == pygame.QUIT:
38 should_quit = True
39 elif event.type == pygame.KEYDOWN:
40 if event.key == pygame.K_ESCAPE:
41 should_quit = True
42 elif event.key == pygame.K_b:
43 pass
44 if should_quit:
45 break
pygame.K_b
は b キーを表します.つまり,b が押されたときのみ43行目の pass 文が実行されるのですが,pass 文とは何もしない文です.そのため普段は何も起きません.デバッグしたいときにはここにブレイクポイントを仕掛けて,b を押したときに一時停止するようにします.するとその時点での変数の状態を調べることができます.
pdb
コマンドを使ってみてください.使い方? 自分で調べなさい.3.5. 演習¶
例題3-1
pass 文にブレイクポイントを仕掛けて,実行中の適当な時点で b
キーで一時停止した状態から,draw
関数の中の
screen.blit(player_image, mouse_pos)
の時点まで実行を進めて,その直前での mouse_pos
の値を確認し,確かに現在のマウスポインタの位置と一致してそうだと確認してください.
1行進むには Step Over ボタンを押しますが,関数の中には入りません.関数の中に入るには Step Into を使います.関数から抜けるには Step Out を使います.
例題3-2
ブレイクポイントには発動条件を設けることができます.行番号の左側を左クリックする代わりに,右クリックして Add Conditional Breakpoint
を選びます.draw(screen, player_image, text_image, mouse_pos)
の行に,マウスの x 座標が 300 以上になったときに発動するブレイクポイントを仕掛けて,その時点の各変数の値を確認してください.
タプル mouse_pos
の 1 つめの値 (x 座標値) は,mouse_pos[0]
として表せます.この式が 300 以上であることを表す条件式を指定してください.
例題3-3
(この例題に取り組む前に Git でコミットするのを忘れないようにしてください.次の章ではこの例題の前の時点から再開します)
関数 create_text
にデフォルト値つき引数 text
と color
を作り,引数が省略されたときは今まで通りの動作で,指定されたときは text
で指定された文字列を color
で指定された色で表示するように変更してください.これを利用して text_image
のほかに text_image2
を作り,Space キーが押されている間は text_image
の代わりに
text_image2
を表示するようにしてください.
Space キーを指定するには,pygame.K_ESCAPE
の代わりに
pygame.K_SPACE
を使います.すべてのキーの一覧はリファレンスマニュアルの以下のページにあります.