3. 基本的なプログラミング (制御構造編)

この章と次の章では,前回から引き続いて hello_pygame.py を拡張しながら, Python の基本事項を確認していきます.

Python について既によく知っている人にとっては,多くの事項が復習になります.文法説明はいったん読み飛ばして実践と演習から見ていき,わからなかったら説明に戻るのでもよいかも知れません.

逆に,ここの文法説明が簡潔過ぎる場合は付録A のミニ・チュートリアルを参照してください.それでも難しい場合は,(Python の初心者向けではなく) プログラミングの初心者向けの Python 入門書を読む方がよさそうです.

3.1. 基本的な計算

3.1.1. 文の実行

  • 基本的に 1 行が 1 文です.勝手に改行してはいけません.
  • 行が長くなりすぎて困るときは,以下のルールを適用して短くできます.
    • 行末に \ を置くと,その行は次の行に継続されます.
    • かっこ類 () [] {} の中では自由に改行して構いません.
  • 字下げ (インデント) にも意味があるので,行の先頭に勝手に空白を入れてはいけません.
  • # から行末まではコメントです.実行結果に影響しません.
1 行の長さは何文字くらいに収めるのがよいですか?
配布環境では 100 文字を超えると Line too long という改善提案 (青い下線) が出るようになっています.

伝統的には,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 に変換される)
Python にも型はあるんですか? C 言語みたいに int x = 0; などと型名を書く必要がないので,型なんてないのだと思っていたのですが.
Python のあらゆるデータには型があります.変数に型を指定する必要がないだけです.

まず,変数とそこに割り当てられているデータ (値) を区別して考えるようにしてください.Python で x = 30 と書くと変数 x に整数型のデータ 30 が割り当てられます.30 には整数型という型がありますが,x にはそのような指定がありません.その直後に x = "abc" と文字列型データを割り当てても構いません.

C 言語では,プログラムの実行が開始される前に,すべての変数の型 (つまり,どの型のデータを割り当ててよいか) が決まります.この性質を静的型付け (static typing) といいます.一方 Python のように実行時に決まる場合を動的型付け (dynamic typing) と呼びます.

前回出てきた Surface 型も型の一種ですよね? intfloat に型変換できるんですか?
型変換は,変換が定義されている型の間でしかできません. Surface から intfloat への変換は定義されていません.

3.1.4. 実践編

変数は,同じ値を何度も使う際に便利なのは言うまでもないですが,たとえ 1 回しか使わないときでもコードを読みやすくするのに役立ちます.

例えば hello_pygame.py では screen = pygame.display.set_mode((600, 400)) のようにしてウィンドウを作成しましたが,後から見たときに「この 600 って何だっけ? 400 って何だっけ?」とわからなくなりがちです.変数を導入して適切な名前をつけておくと読みやすくなります.(説明的な変数 (explanatory variable, explaining variable) と呼ばれます)

この例のように,プログラムの機能を変えることなく,プログラムをより読みやすく,よりメンテナンスしやすく修正することをリファクタする (refactor) といいます.

hello_pygame.py (ver 8.0)
 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()
変数名のつけかたには何かルールがありますか?
文法上のルールは,英字またはアンダースコアで始まり,英数字またはアンダースコアが 0 個以上続くものです.さらに,慣習として守るべき約束があります.

変数や関数の名前のように,プログラマが各自で導入する名前を識別子といいます (これに対して,ifwhile のようにあらかじめ決められているものをキーワードまたは予約語といいます).

識別子には,大文字と小文字のアルファベット,アンダースコア _,数字が使えますが,ただし最初の文字を数字にしてはダメ,と覚えておきましょう.(実は日本語の文字も使えたりしますが,やめておく方がよいです)

アンダースコア 2 個で始まる識別子は特別な用途のものが定義されているため,自分で勝手に使ってはいけません.(例: __init__ とか __name__ とか)

Python 業界の慣習として,変数や関数名は snake_case のようにアルファベット小文字で,単語区切りをアンダースコア _ で表現する方法が好まれます.ただし,定数は大文字で UPPER_CASE のように表現することが推奨されています.

widthheight とか font_size とかは定数だと思うのですが,なぜ大文字にしないのですか?
これは Python の慣習には反しているかもしれませんが,大文字にしない理由があります.

このバージョンのプログラムでは,確かにこれらは定数です.しかし今後プログラムを発展させていく中で,定数ではなくなるかもしれません (例えば将来ウィンドウサイズやフォントサイズを動的に変更する機能を追加するかもしれません).そしてその頃にはこれらの変数名をプログラム中のあちこちで使用しているかも知れません.

現在のプログラムで一見「定数」のように振舞うからといって大文字にしてしまうと,そのような変更の必要が生じたときに,すべての出現箇所を小文字に変換して回る必要があります.これは全くもってナンセンスなことです.

以上のことから,「定数名は大文字にする」という Python の慣習は,将来にわたって定数であり続けると確信できるときにのみ適用するべきだというのが執筆者の個人的な考えです.

3.2. 関数

3.2.1. 関数定義と関数呼び出し

関数定義は以下の形式で書きます:

def 関数名(引数1, 引数2, 引数3):
    関数本体の処理
    ...
    return 返り値
  • 引数が 0 個の場合は丸かっこ内は空にします.
  • return 文は,返り値がない場合は return とだけ書きます.
  • return 文に到達せずに関数を抜けた場合は,返り値のない return 文を実行したのと同じことになります.
  • 関数の中で定義された変数は,その関数の中だけで使えるローカル変数になります.

関数呼び出しは,関数名の後ろに丸かっこをつけると行えます:

関数名(引数1, 引数2, 引数3)
関数って数学でいう \(z = f(x, y)\) みたいなものですよね? x, y が引数で,z が返り値ってことですよね? それなのに「引数を取らない」とか「値を返さない」場合があるというのが理解できないのですが.
Python や C 言語などの「関数」は,数学の「関数」とは別物だと考えてください.

数学でいう「関数」は,引数を定めると,返り値が一意に定まるものです.また,値を返す以外の副作用 (例えば外部の変数の値が変わるとか,ファイルの内容が書き換わるとか) は起きません.このような関数をプログラミング言語の分野ではわざわざ「純粋関数」と呼びます.

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 としましょう.

hello_pygame.py (ver 9.0)
 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_imageplayer_image などが main 関数の中だけで使えるローカル変数だからです. draw 関数の中では使えません.

そこで,必要なものは引数として渡してやることにします.

hello_pygame.py (ver 10.0)
 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 を初期化してウィンドウを作る処理と,文字画像を作る処理をそれぞれ関数に切り出しましょう.

hello_pygame.py (ver 11.0)
 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_screencreate_text には引数はありません.一方,それぞれの関数定義の最後には,結果 (いずれも Surface 型の画像データ) を返すための return 文を付け加えています (8行目と17行目).

3.3. 制御文

3.3.1. 条件文

if 文は以下の形式です (elifelse は適宜省略可):

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")

cond1cond2 などの条件のところには真偽値型の値を指定します:

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 文 (ループのその回をスキップする) を使えます.
  • whilefor などのブロックを有効範囲とするローカル変数は作れません.関数ローカルしかありません.

3.3.3. 実践編

これまでの hello_pygame.py では,ループの頭で pygame.event.clear を呼び出すことでイベントの発生を無視していました.そのためウィンドウ右上の × ボタンも効きませんでした.

イベントの情報を入手するには,代わりに pygame.event.get 関数を使います.この関数は,その時点までに発生したイベントを単にクリアするのではなく,発生順に一列に並んだシーケンスとして返します.シーケンスなので for 文で 1 つずつ処理することができます.

pygame.event.get が返すシーケンスの要素は pygame が定義する Event 型 (正確には pygame.event.Event 型) のオブジェクトです.詳細はリファレンスマニュアル に記載されているのですが,具体例を見る方が早いです.

hello_pygame.py (ver 12.0)
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_quitTrue にします.

39-41行目

イベントのタイプが pygame.KEYDOWN だったなら,何らかのキーが押されたことを示すイベントだということです.この場合,Event 型オブジェクトの key という属性を調べるとキーの種類がわかります.定数 pygame.K_ESCAPE と等しいなら should_quitTrue にします.

37行目に始まった if 文に else はありませんので,QUITKEYDOWN 以外のイベントは単に無視することになります.

42-43行目
この時点で should_quitTrue なら,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 の場合は以下のように書くことが推奨されています.

hello_pygame.py (ver 13.0)
47    pygame.quit()
48
49
50if __name__ == "__main__":
51    main()

これは,このファイルが python コマンドで直接起動されるのではなく他から import された場合に備えるためのものです.

直接起動された場合,変数 __name__ には __main__ という文字列が自動的にセットされています.一方 import された場合はモジュール名 (この場合は hello_pygame という文字列) がセットされます.結果として,上のような if 文によって,直接起動されたときだけ main 関数が呼び出されるようになります.

同じ py ファイルを,あるときは 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_textimport して関数として使えるようにしています.このようにして開発中の関数の動作をチェックしたりすることはよくあります.

もしさっきの修正を行わず 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" で検索すると有用な情報が見つかります.

エラーが出るのが怖いです.怒られているような気がします.
エラーが出るのは悪いことではありません.誤りはどんどんコンピュータに見つけさせましょう.

本当に怖いのはエラーが出ないのに動作がおかしいことで,さらに怖いのは,エラーが出ず,一見動いているように見えるのに,実は間違った結果が出ていることです.だから,エラーが出たら「よかった,間違いを見つけてくれた」と喜ぶべきです.

上級者は,自分が間違いを起こすことを前提として,間違えたらできるだけエラーが起きるように予防的なプログラミングをします.エラーは道具なのです.

一つ前の回答にある「予防的なプログラミング」とは例えばどういうことですか?
2 つほど例を挙げます.1 つめは 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 を開くには,メニューバーから View ‣ Command Palette を開き, 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 の行にブレイクポイントを仕掛けて (行番号の左側を左クリックして赤い丸をつける),デバッグ実行 (Run ‣ Start Debugging) してください.pygame.blit が呼ばれる直前で一時停止するので,エディタで変数上にマウスホバーするなり,Run サイドバーで変数を一覧表示するなりして,想定外の値を持っている変数を探すことができます.

エラーが出ない場合は,もっと広範囲を調べる必要があります.今のようなプログラムの場合,main 関数の while ループ内のどこかにブレイクポイントを仕掛けて毎時刻の各変数の値を調べる…ということをしたくなるのですが,マウス操作との干渉が問題です.開発中のプログラムのウィンドウ上でマウスを動かしていると,VSCode のデバッガ操作ボタン (Continue や Step Over など) を押せません.

こういう場合は,特定の条件でのみ実行される文をわざと作って,そこにブレイクポイントを仕掛けるとよいです.

hello_pygame.py (ver 14.0)
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_bb キーを表します.つまり,b が押されたときのみ43行目の pass 文が実行されるのですが,pass 文とは何もしない文です.そのため普段は何も起きません.デバッグしたいときにはここにブレイクポイントを仕掛けて,b を押したときに一時停止するようにします.するとその時点での変数の状態を調べることができます.

デバッグ実行すると pygame のウィンドウが出てきません.デバッグ無しでの実行なら出てくるのですが.
pygame のウィンドウが VSCode の裏に隠れていませんか?
デバッグ実行すると pygame のウィンドウが「応答なし」状態になることがあるのですが.
ブレイクポイントで止まっている間はイベントが処理されないので,応答なしと判断されることがあります.まあしかたないです.
どうも久しぶりです,絶対にメモ帳でプログラミングするマンです.メモ帳でデバッガを使うことはできませんか?
メモ帳では無理ですが,コマンドプロンプト等から 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 にデフォルト値つき引数 textcolor を作り,引数が省略されたときは今まで通りの動作で,指定されたときは text で指定された文字列を color で指定された色で表示するように変更してください.これを利用して text_image のほかに text_image2 を作り,Space キーが押されている間は text_image の代わりに text_image2 を表示するようにしてください.

Space キーを指定するには,pygame.K_ESCAPE の代わりに pygame.K_SPACE を使います.すべてのキーの一覧はリファレンスマニュアルの以下のページにあります.