ゼロからのPython入門講座演習 関数を活用する

スイカ割りゲームを作ってみようリファクタリング - マジックナンバーを避ける適切なデータ型を使う関数を活用する

これまで、この講座のサンプルプログラムは、「スイカ割りゲーム」も含め、あまり 関数 を定義せず、単純にプログラムを記述していました。

本来、これはあまり望ましいことではありません。ここでは、スイカ割りゲームをさらに リファクタリング し、プログラムを関数に分割してみましょう。

現在、スイカ割りゲームはこんな感じになっています。

import random
import math

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

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)


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

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

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

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

    if c == "n":
        current_y = current_y - 1
    elif c == "s":
        current_y = current_y + 1
    elif c == "w":
        current_x = current_x - 1
    elif c == "e":
        current_x = current_x + 1

    player_pos = (current_x, current_y)

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

初期位置の作成

元のプログラムでは、スイカとプレイヤーの初期位置を次のように生成しています。

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

この処理では、

(random.randrange(0, BOARD_SIZE), random.randrange(0, BOARD_SIZE))

という、全く同じ式でタプルを作成しています。同じことを2度書くのは無駄ですし、将来、この部分を修正する時のミスの元にもなります。

また、(random.randrange(0, BOARD_SIZE), random.randrange(0, BOARD_SIZE)) のような式の羅列だけでは、この処理がどんな目的でどんなことをやっているのか、わかりにくいです。

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

座標を生成する関数は generate_position という名前にします。生成する座標の範囲は、size という名前の 引数 で指定できるようにしましょう。

In [14]:
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 [15]:
generate_position(3)  # (0, 0)~(2, 2) の範囲で座標を生成
Out[15]:
(1, 0)
In [16]:
generate_position(100)  # (0, 0)~(99, 99) の範囲で座標を生成
Out[16]:
(54, 87)

関数を定義しておけば、こんなふうに手軽に呼び出して、関数がちゃんと書けているか試せるのでとても便利です。

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

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

の部分は、次のように書き換えられます。

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

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

このように、関数を利用すると、

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

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

プレイヤーの移動処理

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

# キー入力に応じて、プレイヤーを移動する
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

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

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

In [17]:
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 [19]:
move_position('n', (1, 1)) # (1, 1) から、北に移動
Out[19]:
(1, 0)
In [20]:
move_position('e', (3, 4)) # (3, 4) から、東に移動
Out[20]:
(4, 4)

スイカ割りゲーム全体を関数化

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

import random
import math

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

def generate_position(size):
    # 0以上size未満の範囲で、x座標とy座標を生成する
    x = random.randrange(0, size)  # x座標
    y = random.randrange(0, 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)


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 [21]:
import random
import math

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

def generate_position(size):
    # 0以上size未満の範囲で、x座標とy座標を生成する
    x = random.randrange(0, size)  # x座標
    y = random.randrange(0, 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 [22]:
suika_wari()
スイカへの距離: 4.123105625617661
n:北に移動 s:南に移動 e:東に移動 w:西に移動 s
スイカへの距離: 4.0
n:北に移動 s:南に移動 e:東に移動 w:西に移動 e
スイカへの距離: 5.0
n:北に移動 s:南に移動 e:東に移動 w:西に移動 w
スイカへの距離: 4.0
n:北に移動 s:南に移動 e:東に移動 w:西に移動 w
スイカへの距離: 3.0
n:北に移動 s:南に移動 e:東に移動 w:西に移動 w
スイカへの距離: 2.0
n:北に移動 s:南に移動 e:東に移動 w:西に移動 w
スイカへの距離: 1.0
n:北に移動 s:南に移動 e:東に移動 w:西に移動 w
スイカを割りました!

まとめ

これで、スイカ割りゲームのプログラムを

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

という、機能ごとに分割し、すべて独立した関数としました。

generate_position()calc_distance()move_position() の3つの関数は、スイカ割りに必要になる、個別の機能を関数化しています。それぞれの関数は、短い、独立した機能となっていて、どんなことをするのか、わかりやすくなっています。

また、関数にすることで、簡単に呼び出してテストできるようになっています。たとえば、calc_distance() を呼び出して結果を確認したければ、次のように呼び出せます。

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

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

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

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

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

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

Copyright © 2001-2023 python.jp Privacy Policy python_japan
Amazon.co.jpアソシエイト
Amazonで他のPython書籍を検索