Python の勉強 スロットゲーム編 〜その15:ライブラリ、パッケージ、モジュール〜

プログラミング

正規表現による入力検証(バリデーション)をスロットゲームに組み込む際に「ライブラリ」や「モジュール」という単語が出てきました。Python の公式ページを見ていると「パッケージ」という単語も出てきます。どれも似たような使われ方をしていて区別しづらいかと思いますので、今後のため整理したいと思います。

「モジュール」 == 「ファイル」、「パッケージ」 == 「フォルダ」みたいな感じ

モジュール

Python の機能の最小単位は関数です。モジュールは復数の関数を 1 つのファイルにまとめたものになります。実際、スロットゲームで使用している 2 つのモジュール「random」「re」は以下のフォルダ内で存在を確認できます。※ Windows で標準インストールした場合

「random」「re」モジュールを Windows で確認

import re」と書くことで、Python はこのファイルを読み込んでくれるわけですね。

パッケージ

同じフォルダ内には「urllib」といったフォルダも確認できます。こちらが “パッケージ” と呼ばれる単位になります。パッケージには「__init__.py」ファイルが必要になります。

「urllib」パッケージ

「urllib」パッケージがまさにそうなのですが、「__init__.py」ファイルに特別な何かを書く必要はありません。そのファイルが存在しているだけで、そのフォルダがパッケージという扱いになります。

ファイルを含むディレクトリをパッケージとしてPython に扱わせるには、ファイル init.py が必要です。 これにより、 string のようなよくある名前のディレクトリにより、モジュール検索パスの後の方で見つかる正しいモジュールが意図せず隠蔽されてしまうのを防ぐためです。 最も簡単なケースでは init.py はただの空ファイルで構いませんが、 init.py ではパッケージのための初期化コードを実行したり、後述の all 変数を設定してもかまいません。

6.4. パッケージ – Python 3.9.4 ドキュメント

また Widows のファイルシステムと同様、パッケージの中にパッケージ(サブパッケージ)を含めることが可能です。サブパッケージを指定してモジュールを読み込み、使用する際には

import パッケージ.サブパッケージ.モジュール

パッケージ.サブパッケージ.モジュール.関数(...)

または

from パッケージ.サブパッケージ import モジュール

モジュール.関数(...)

のようにします。上記 2 つ以外にも as を使ったり、関数を指定して読み込む方法もあります。

ところで Python は自動でサブパッケージを読み込みません。復数のサブパッケージを読み込むためにはサブパッケージ名を羅列します。それ以外に __all__ という特別な変数を定義しておくと、対象のサブパッケージを「*」で読み込むことができるようになります。
以下は xml パッケージと、それに含まれるサブパッケージを確認している様子です。各サブパッケージにも「__init__.py」が含まれていることが分かります。また xml/__init__.py を確認すると __all__ が定義されていました。この変数を定義してあるおかげで、「from xml import *」とするだけで「dom」「parsers」「sax」「etree」を読み込むことができるようになります。

ライブラリ

「ライブラリ」は説明の都合で作成された単語のようで、「パッケージ」と「モジュール」の総称のようです。公式ドキュメント中に登場する「標準ライブラリ」とはつまり「標準でインストールされるパッケージとモジュール」と同義になります。
公式ドキュメント中で「ライブラリとは〜」という説明が見つからないため、あくまで推測になります。また Python での用語の使い方になりますので、他のプログラミング言語ではまた違った定義になることをご承知おきください。

ライブラリの種類

“標準” ライブラリという言葉から想像できるように、標準じゃないライブラリも存在します。前回の記事で触れた「PEP8」には以下 3 つが定義されています。(PEP8 としては、import 文を記載する場合には、この順番で記載するように…との指定があります)

  • 標準ライブラリ
  • サードパーティライブラリ(外部ライブラリ)
  • ローカル(自作)ライブラリ

標準ライブラリ

Python のインストール時に、自動でインストールされるライブラリです。すぐ使えて、どこでも使えます。「random」や「re」がそれです。「import」によりライブラリを読み込むことで使えるようになります。
では「print()」や「format()」等はどこからきたのでしょうか?これらはビルトインモジュール(builtins)といって、特に import しなくても使えるようになっている関数です。※ builtins を import して使うこともできます。意味は無いです

サードパーティーライブラリ(外部ライブラリ)

これが Python の真骨頂と言ってもいいでしょう。標準ライブラリのように、Python と一緒にインストールされるものではなく、後追で自分で追加していくライブラリです。とにかく膨大な数のサードパーティーライブラリが存在していて誰でも使うことができます。特に PyPI(Python Package Index)に登録されているライブラリであれば、「pip」という Python のパッケージ管理ツールを使用して簡単にインストールできます。作りたいものがある時、解決しなければいけない問題がある時、全てのプログラムを自分で作成するのはとても大変ですが、サードパーティーライブラリの力を借りれば数行で済むかも知れません。

最近では機械学習の「Tensorflow」、科学計算用の「numpy」と、グラフ描画のための「matplotlib」等を目にする機会が増えたのではないでしょうか。

ローカル(自作)ライブラリ

自作のライブラリです。自分で再利用したり、社内やコミュニティー内で共有したりしましょう。しかも、特別なことはほとんど何もする必要がありません。Python ファイルを作成したら、それがそのままモジュールのように利用できます。フォルダを作成して、その中に Python のファイルと __init__.py ファイルを格納したらパッケージとして利用できます。

ただし

  • (変数や関数と同様)先頭(一文字目)に数字は使えない
    →スロットゲームのプログラムは ファイル名が数字で始まっているのでライブラリとして定義できない
  • Python は読み込んだライブラリをその場で実行する
    → 今のスロットゲームプログラムは、読み込んだら勝手にゲームがスタートしてしまう

ということを知っておく必要があります。
せっかくなので、実際に「my_slotgame」パッケージとして定義して使用するところまでやってみましょう。必要な手順は以下になります。

  1. 「my_slotgame」フォルダを作成する
  2. 「__init__.py」ファイルを作成する
    ファイルは中身が空っぽの、所謂、空ファイルとする
  3. 「014.py」をコピーし、「my_slotgame」フォルダ内に「game015.py」という名前で作成する
  4. 「015.py」を作成して、「my_slotgame/game015.py」をインポートする

「my_slotgame」フォルダを作成する

「__init__.py」ファイルを作成する

「014.py」をコピーし、「my_slotgame」フォルダ内に「game015.py」という名前で作成する

「015.py」を作成して、「my_slotgame/game015.py」をインポートする

ただし、このプログラム(my_slotgame/game01.py)は前述の通り、読み込むと即実行されてしまう…という致命的な欠点があります。最後にこれを回避して、今回の記事は終わりにしたいと思います。

if __name__ == ‘__main__’:

Python は実行中のモジュールを一意に識別するために「__name__」という変数(属性)を使用します。
001.py ~ 014.py で確認してきたようにファイル名を指定して実行(スクリプトとして実行)した場合は、「__name__ == '__main__'」となります。これとは別に、import を使用してファイルを読み込んだ場合は、修飾名(パッケージ名.モジュール名)つまり「__name__ == 'my_slotgame.game015'」となります。この違いを利用することで、読み込み→即実行を回避するとともに、ファイル名を指定した実行する場合は今まで同様、ゲームが即実行されるように修正可能です。具体的には

  1. 実行されていたプログラムの部分を関数として定義する
  2. __name__ == '__main__' を判断基準として、ファイル名を指定して実行した場合に「1」で定義した関数を実行する
  3. import で読み込む場合は、別途、「1」で定義した関数を実行する

となります。

修正した後は「015.py」「game015.py」がそれぞれ、想定通りに動くかどうか確認してみてください。

※015.py

from my_slotgame import game015


game015.main()

※my_slotgame/game015.py

"""スロットゲーム

簡単なスロットゲームです。ゲームスタート時に保有しているコインを増やしましょう。
掛けコイン数を入力すると、スロットの結果が表示されます。
絵柄が揃うと、そのパターンに応じた配当倍率でコインを獲得することができます。
"""


import random  # "乱数" を生成するために必要なライブラリをメモリ上に読み込む(標準ライブラリ)
import re  # "正規表現" を使用するために必要なライブラリをメモリ上に読み込む(標準ライブラリ)

# スロット(リール)の中身を "定数" で定義する
# 配列のインデックスは 0 から始まるので、0 〜 9 番目に格納されている
# 定数は全て大文字で記載する
REEL_MARK_LIST = ('♠', '♥', '♥', '◆', '◆', '◆', '♣', '♣', '♣', '♣')

# 特別なプレーヤーの名前と、その名前を使用した際のボーナスコイン数
BONUS_PLAYER_AND_COINS = {'king': 100, 'queen': 150}

# スペシャルサンクスの対象者名
# ボーナスコインは一律 50 枚とする
SPECIAL_THANKS = {'akira',      # 弊社社長。詳細は社長が作成最多ブログ記事のプロフィール欄をご確認下さい。
                  'hurry',      # 詳細はプロフィール欄から twitter をご確認下さい。
                  'shachi',     # https://shachi-web.com/
                  }
SPECIAL_THANKS_COIN = 50


# ============================================================
# ゲームに必要な関数を定義する
# ============================================================

def show_start_message():
    """ゲーム開始のメッセージを表示する
    (This function is to be called to show the start message.)
    """
    print('--------------')
    print('スロットゲーム')
    print('--------------')

def ask_player_name():
    """プレーヤーの名前を尋ねる
    (This function is to be called to ask player's name.)

    プレーヤーの名前を入力(input)してもらい、挨拶を表示(print)する
    入力されたプレーヤーの名前を返す(return)

    Returns:
        str: プレーヤーの名前
    """
    player_name = input('あなたの名前を入力して下さい: ')
    print(f'こんにちは {player_name} さん')
    return player_name

def ask_bets(player_coin):
    """現在の所持コイン数を表示した後、掛けるコイン数(bets)を尋ねる
    (This function is to be called to ask bets.)

    所持コイン数(player_coin)は数値なので、文字列に含めたい場合は文字列(string)に変換する必要がある ==> str() 関数
    input() 関数で入力した値は文字列(string)になるので整数(integer)に変換する ==> int() 関数

    Args:
        player_coin (int): プレーヤーの所持コイン数

    Returns:
        int: 掛けコイン数
    """
    print('------------------------------')
    print(f'現在の所持コイン数は {player_coin:,} 枚です。')
    while True:
        str_bets = input('掛けコイン数を入力して下さい: ')
        (valid, error_message) = is_bets_valid(str_bets, player_coin)
        if valid:
            return int(str_bets)
        else:
            print(error_message)
            continue

def is_bets_valid(str_bets, player_coin):
    """掛けコイン数が有効(valid)な場合は True を返す(return)
    --> The function returns true if the bets is valid.

    Args:
        str_bets (str): 掛けコイン数
        player_coin (int): プレーヤーの所持コイン数

    Returns:
        tuple: [0] 掛けコイン数が有効な場合 True、それ以外は False
               [1] 検証結果が False だった場合のエラーメッセージ
    """
    if not re.search(r'^[0-9]+$', str_bets):
        return (False, '掛けコイン数は半角数字で入力して下さい。')

    bets = int(str_bets)
    if bets == 0:
        return (False, '掛けコイン数は 1 枚以上を指定して下さい。')
    elif bets > player_coin:
        return (False, '掛けコイン数は所持コイン数以下を指定して下さい。')
    else:
        return (True, None)

def show_and_get_result():
    """スロットの結果(絵柄)を表示し、結果を取得する
    (This function is to be called to show and get result.)

    Returns:
        str: スロットの結果(絵柄)
    """
    result_list = []
    for _ in range(3):
        index = random.randint(0, 9)
        result = REEL_MARK_LIST[index]
        result_list.append(result)
    result_all = ''.join(result_list)
    print(result_all)
    return result_all

def get_division(marks):
    """絵柄に応じた配当倍率を取得する
    (This function is to be called to get division.)

    Args:
        marks (str): スロットの結果(絵柄)

    Returns:
        int: 配当倍率
    """
    if marks == '♠♠♠':
        print('超大当たり!!')
        return 15
    elif marks == '♥♥♥':
        print('大当たり!')
        return 12
    elif marks == '◆◆◆':
        print('大当たり!')
        return 10
    elif marks == '♣♣♣':
        print('大当たり!')
        return 8
    elif marks[0:2] == '♠♠':
        print('当たり!')
        return 5
    else:
        print('ハズレ...')
        return -1

def calculate_coin(coin, bets, division):
    """掛けたコイン数(bets)と、配当倍率(division)を使用してプレーヤーの所持コイン数を精算する
    (This function is to be called to calculate (players's) coin.)

    Args:
        coin (int): プレーヤーの精算前の所持コイン数
        bets (int): 掛けコイン数
        division (int): 配当倍率

    Returns:
        int: 精算後のプレーヤーの所持コイン数
    """
    coin = coin + bets * division
    return coin

def show_gameover_message(player_name):
    """ゲーム終了のメッセージを表示する
    (This function is to be called to show a game-over message.)

    Args:
        player_name (string): プレーヤー名前
    """
    print('ゲームオーバーです')
    print(f'さようなら {player_name} さん')


# ============================================================
# 実際にゲームを実行する
# ============================================================

def main():
    # プレーヤーの所持コイン数
    player_coin = 123

    show_start_message()
    player_name = ask_player_name()

    if player_name in BONUS_PLAYER_AND_COINS:
        player_coin = player_coin + BONUS_PLAYER_AND_COINS[player_name]
    elif player_name in SPECIAL_THANKS:
        player_coin = player_coin + SPECIAL_THANKS_COIN

    while player_coin > 0:
        bets = ask_bets(player_coin)
        marks = show_and_get_result()
        division = get_division(marks)
        player_coin = calculate_coin(player_coin, bets, division)

    show_gameover_message(player_name)


if __name__ == '__main__':
    main()

コメント

タイトルとURLをコピーしました