ゼロからのPython入門講座演習 スイカ割りゲームのリファクタリング

スイカ割りゲームスイカ割りゲームのリファクタリング

スイカ割りゲーム はとりあえず動き、ゲームを実行できるようになりました。

しかし、プログラムの書き方としては今一つな点があります。本格的な改善はこの講座の範囲を超えますが、いくつか簡単なポイントを改善してみましょう。

このように、プログラムの機能はそのままで、書き方を改善することを リファクタリング といいます。

座標の持ち方

元のソースコードでは、プレイヤーとスイカの位置を、x座標とy座標それぞれ別々の変数として保持していました。

suika_x = random.randrange(0, 5)  # スイカのx座標
suika_y = random.randrange(0, 5)  # スイカのy座標

player_x = random.randrange(0, 5) # プレイヤーのx座標
player_y = random.randrange(0, 5) # プレイヤーのy座標

しかし、スイカやプレイヤーの位置は本来一つの情報ですので、2つの変数に分ける意味はありません。ここでは、

(x座標, y座標)

という形式の、2つの整数からなる タプル として保持するようにしましょう。そうすると、上のコードは次のように書き換えられます。

In [21]:
suika_pos = (random.randrange(0, 5), random.randrange(0, 5))  # スイカの位置
player_pos = (random.randrange(0, 5), random.randrange(0, 5)) # プレイヤーの位置

変数名 suika_posplayer_pospos は、英語の 位置(position) から取っています。

生成した値を表示してみましょう。

In [22]:
print("スイカの位置は", suika_pos)
print("プレイヤーの位置は", player_pos)
スイカの位置は (4, 3)
プレイヤーの位置は (0, 2)

初期位置の生成

スイカの位置とプレイヤーの位置は、どちらも

(random.randrange(0, 5), random.randrange(0, 5))

という、全く同じ式でタプルを作成しています。同じことを2度書くのは無駄ですし、ミスの元にもなります。また、(random.randrange(0, 5), random.randrange(0, 5)) のような式の羅列だけでは、どんなことをやっているのか、わかりにくいです。

そこで、この処理は、指定した範囲で座標を生成する 関数 として定義してしまいましょう。

In [12]:
def generate_position(size):
    # 0以上size未満の範囲で、x座標とy座標を生成する
    x = random.randrange(0, size)  # x座標
    y = random.randrange(0, size)  # y座標
    
    return (x, y)

関数 generate_position(size) を呼び出すと、指定した 0 から size-1 までの範囲でx座標とy座標を生成し、結果をタプルとして返します。いろいろな値を size に指定して、実際に呼び出してみましょう。

In [13]:
generate_position(3)  # (0, 0)~(2, 2) の範囲で座標を生成
Out[13]:
(2, 1)
In [20]:
generate_position(100)  # (0, 0)~(99, 99) の範囲で座標を生成
Out[20]:
(31, 81)

generate_position(size) を使うと、さきほどの

suika_pos = (random.randrange(0, 5), random.randrange(0, 5))  # スイカの位置
player_pos = (random.randrange(0, 5), random.randrange(0, 5)) # プレイヤーの位置

という処理は、次のように書き換えられます。

suika_pos = generate_position(5)  # スイカの位置
player_pos = generate_position(5) # プレイヤーの位置

関数を利用して処理を行うことで、スイカとプレイヤーで同じ処理を2度書くのではなく、どちらの座標も generate_position(size) を利用して設定できるようになりました。このように関数を利用すると、

  • 無駄な繰り返しを避ける
  • generate_position というわかりやすい関数名がついているので、何を行っているのかひと目で理解できる

などのメソットがあります。

マジックナンバーを避ける

このプログラムでは、スイカとプレイヤーの初期位置は 5×5 の範囲内としています。この範囲は、generate_position(5) のように、引数として指定しています。

suika_pos = generate_position(5)  # スイカの位置
player_pos = generate_position(5) # プレイヤーの位置

しかし、あとからこのプログラムを読むとき、いきなり 「5」という数字だけが書いてあった場合、この数値がどんな意味の値なのか、覚えていられるでしょうか?

運良く覚えていられるかもしれないし、忘れているかもしれませんが、忘れていても問題ないように、ちゃんと意味がわかるようにしておきましょう。こういう場合、直接 5 という数字を書くのではなく、わかりやすい名前をつけた変数に代入してして使用するようにします。

この場合だと、5 はこのゲームのボードのサイズですから、BOARD_SIZE という名前にしましょう。Pythonの慣例で、このような定数値の変数名は小文字のboard_size ではなく、BOARD_SIZE のようにすべて大文字にします。

BOARD_SIZE = 5  # ボードの初期サイズ

suika_pos = generate_position(BOARD_SIZE)  # スイカの位置
player_pos = generate_position(BOARD_SIZE) # プレイヤーの位置

こうしておけば、BOARD_SIZE という名前を手がかりに、どんな意味を持つ値なのか、きっと思い出すことができるでしょう。また、サイズを変更する場合は BOARD_SIZE の値を修正するだけで済むようになります。

この 5 のような定数値は、一般に マジックナンバー といいます。基本的には、マジックナンバーは、値を直接書くのではなく、 常に 変数に代入してから利用するようにします。

マジックナンバーを変数に代入せず、あっちこっちで使ってしまうと、あとでその値の意味がわからなくなってしまったり、値を変更するときに修正漏れが発生したりします。

ゲームのループ処理

修正前のプログラム では、ゲームのループ処理は次のようになっていました。

# スイカとプレイヤーの位置が異なる間、処理を繰り返す
while (suika_x != player_x) or (suika_y != player_y):

    # スイカとプレイヤーの距離を表示する
    distance = calc_distance(player_x, player_y, suika_x, suika_y)
    print("スイカへの距離:", distance)

    # キー入力に応じて、プレイヤーを移動する
    c = input("n:北に移動 s:南に移動 e:東に移動 w:西に移動")
    if c == "n":
        player_y = player_y - 1
    elif c == "s":
        player_y = player_y + 1
    elif c == "w":
        player_x = player_x - 1
    elif c == "e":
        player_x = player_x + 1

スイカとプレイヤーの位置は、x座標とy座標別々の変数ではなく、一つのタプルにまとめてしまいましたので、まず while文を修正しましょう。

while 文は、スイカとプレイヤーの位置が異なる間、処理を繰り返しますから、条件式は

# 修正前
while (suika_x != player_x) or (suika_y != player_y):
    ...

から、

# 修正後
while suika_pos != player_pos:
    ...

と修正します。x座標とy座標を別々に比較するのに比べて、すっきりとしていますね。

スイカからプレイヤーまでの距離を求める処理

スイカからプレイヤーまでの距離を求める処理 も、座標をタプルで受け取るように修正します。

# 修正前
def calc_distance(x1, y1, x2, y2):
    # 2点間の距離を求める
    diff_x = x1 - x2
    diff_y = y1 - y2

    return math.sqrt(diff_x**2 + diff_y**2)

という関数を、次のように修正しましょう。

# 修正後
def calc_distance(pos1, pos2):
    # 2点間の距離を求める
    diff_x = pos1[0] - pos2[0]
    diff_y = pos1[1] - pos2[1]

    return math.sqrt(diff_x**2 + diff_y**2)

calc_distance() を呼び出す部分

# 修正前
distance = calc_distance(player_x, player_y, suika_x, suika_y)

も、新しい変数を使うように変更します。

# 修正後
distance = calc_distance(player_pos, suika_pos)

プレイヤーの移動処理

ゲームのループ処理の中では、次のようにキー入力を受け取り、入力に応じてプレイヤーの位置を移動しています。

# キー入力に応じて、プレイヤーを移動する
c = input("n:北に移動 s:南に移動 e:東に移動 w:西に移動")
if c == "n":
    player_y = player_y - 1
elif c == "s":
    player_y = player_y + 1
elif c == "w":
    player_x = player_x - 1
elif c == "e":
    player_x = player_x + 1

この、入力文字に応じてプレイヤーを移動させる部分、けっこう処理が長くて、ループの中に入ってると読みにくくて邪魔ですね。ここも、関数にしてしまいましょう。

プレイヤーの座標を移動させる関数は、次のように書けます。

In [23]:
def move_position(direction, pos):
    # direction にしたがって、posを移動する
    
    current_x, current_y = pos
    
    if direction == "n":
        current_y = current_y - 1
    elif direction == "s":
        current_y = current_y + 1
    elif direction == "w":
        current_x = current_x - 1
    elif direction == "e":
        current_x = current_x + 1

    return (current_x, current_y)

move_position(direction, pos) は引数として directionpos を指定します。direction は、移動方向を指示する文字を指定し、pos には現在の位置を示すタプルを指定します。

pos から direction にしたがって移動した座標のタプルが戻り値となります。

引数と現在位置を指定して、実際に呼び出してみましょう。

In [25]:
print(move_position('n', (1, 1))) # (1, 1) から、北に移動
(1, 0)
In [26]:
print(move_position('e', (3, 4))) # (3, 4) から、東に移動
(4, 4)

タプルのアンパック

move_position(direction, pos)では、次のように代入文の左辺に2つの変数名を指定しています。

current_x, current_y = pos

これは、コレクションのアンパック代入 で紹介したタプルをアンパックした代入で、x座標とy座標を別々の変数に代入しています。

ゲーム全体を関数に

プレイヤーの移動処理を move_positoin() 関数にまとめてしまいましたから、ゲームのループ処理は次のようになります。

suika_pos = generate_position(BOARD_SIZE)  # スイカの座標
player_pos = generate_position(BOARD_SIZE) # プレイヤーの座標

# スイカとプレイヤーの位置が異なる間、処理を繰り返す
while (suika_pos != player_pos):

    # スイカとプレイヤーの距離を表示する
    distance = calc_distance(player_pos, suika_pos)
    print("スイカへの距離:", distance)

    # キー入力に応じて、プレイヤーを移動する
    c = input("n:北に移動 s:南に移動 e:東に移動 w:西に移動")
    player_pos = move_position(c, player_pos)

たいぶんスッキリしましたね。

最後に、この処理もすべて suika_wari という名前の関数にしてしまいましょう。

def suika_wari():
    suika_pos = generate_position(BOARD_SIZE)  # スイカの座標
    player_pos = generate_position(BOARD_SIZE) # プレイヤーの座標

    # スイカとプレイヤーの位置が異なる間、処理を繰り返す
    while (suika_pos != player_pos):

        # スイカとプレイヤーの距離を表示する
        distance = calc_distance(player_pos, suika_pos)
        print("スイカへの距離:", distance)

        # キー入力に応じて、プレイヤーを移動する
        c = input("n:北に移動 s:南に移動 e:東に移動 w:西に移動")
        player_pos = move_position(c, player_pos)

まとめると

ここまでの修正をまとめると、次のようになります。

In [27]:
import random
import math

BOARD_SIZE = 5

def generate_position(size):
    # 0以上size未満の範囲で、x座標とy座標を生成する
    x = random.randrange(0, BOARD_SIZE)  # x座標
    y = random.randrange(0, BOARD_SIZE)  # y座標
    
    return (x, y)


def calc_distance(pos1, pos2):
    # 2点間の距離を求める
    diff_x = pos1[0] - pos2[0]
    diff_y = pos1[1] - pos2[1]
    
    return math.sqrt(diff_x**2 + diff_y**2)


def move_position(direction, pos):
    # direction にしたがって、posを移動する
    
    current_x, current_y = pos
    
    if direction == "n":
        current_y = current_y - 1
    elif direction == "s":
        current_y = current_y + 1
    elif direction == "w":
        current_x = current_x - 1
    elif direction == "e":
        current_x = current_x + 1

    return (current_x, current_y)


def suika_wari():
    # スイカ割りを実行する
    
    suika_pos = generate_position(BOARD_SIZE)  # スイカの座標
    player_pos = generate_position(BOARD_SIZE) # プレイヤーの座標

    # スイカとプレイヤーの位置が異なる間、処理を繰り返す
    while (suika_pos != player_pos):

        # スイカとプレイヤーの距離を表示する
        distance = calc_distance(player_pos, suika_pos)
        print("スイカへの距離:", distance)
    
        # キー入力に応じて、プレイヤーを移動する
        c = input("n:北に移動 s:南に移動 e:東に移動 w:西に移動")

        player_pos = move_position(c, player_pos)

    print("スイカを割りました!")

スイカ割りを実行するときは、suika_wari() 関数を呼び出します。

In [28]:
suika_wari()
スイカへの距離: 2.8284271247461903
n:北に移動 s:南に移動 e:東に移動 w:西に移動 n
スイカへの距離: 2.23606797749979
n:北に移動 s:南に移動 e:東に移動 w:西に移動 n
スイカへの距離: 2.0
n:北に移動 s:南に移動 e:東に移動 w:西に移動 e
スイカへの距離: 1.0
n:北に移動 s:南に移動 e:東に移動 w:西に移動 e
スイカを割りました!

改善された内容

もとのプログラムと比べて、修正したプログラムは以下のような点で異なっています。

  1. プレイヤーとスイカの位置を、x座標とy座標を別々の変数ではなく、タプルでまとめて管理するようにして、処理が見やすくなった。
  2. マジックナンバーを直接使わず、変数に代入して数値の意味をわかりやすくした。
  3. 機能をすべて関数に分割し、処理をわかりやすくした。

とくに、3.は重要です。プログラムはいろいろな機能を組み合わせて構成されますが、必要な機能を抽出し、適切に関数に分割するようにします。

ここでは、

  • 座標を生成する処理 (generate_position)
  • 2点間の距離を求める処理 (calc_distance)
  • 座標を移動する処理 (move_position)
  • ゲームを実行する処理 (suika_wari)

という、4つの関数に分割しました。

generate_position()calc_distance()move_position() の3つの関数は、スイカ割りに必要になる、個別の機能を関数化しています。それぞれの関数は、短い、独立した機能となっていて、どんなことをするのか、わかりやすくなっています。また、関数にすることで、簡単に呼び出してテストできるようになっています。たとえば、calc_distance() を呼び出して結果を確認したければ、次のように呼び出せます。

In [30]:
calc_distance((2, 2), (4, 5))  # 座標 (2, 2) から (4, 5) までの距離を求める
Out[30]:
3.605551275463989

そして、suika_wari() はこの3つの関数を利用し、スイカ割りゲーム全体を関数として実行できるようにしています。

このように、主となる関数(ここでは suika_wari() ) と、主となる関数が利用する補助的な関数 (ここではgenerate_position()calc_distance()move_position()) を組み合わせて全体を構成する手法は、プログラムの開発でもっとも基礎的なテクニックです。

プログラムを開発するときには、

  • プログラムの機能はなにか
  • その機能はどんな機能から構成されるのか

ということを考え、それぞれの機能を関数として定義するようにしてみましょう。



Amazon.co.jpアソシエイト:
Copyright © 2001-2020 python.jp Privacy Policy python_japan