sec05 - 第4章:Pythonで責務を分離:言語管理ロジックを別モジュールに分ける方法(パッケージ設計)
スポンサーリンク

Pythonの多言語化を実現するパッケージ構成の作り方

この章では、これまで作成してきた「Fortune Cookie(フォーチュン・クッキー)」アプリをさらに整備していきます。

現時点では、fortune.pyはかなり構造化されたコードとなっています。しかし、対応言語が増えると、『言語コードに応じて読み込むモジュールを切り替える』処理を追加または修正する必要が生じます。

    # 言語コードに応じて読み込むモジュールを切り替える
    if lang_code == 'en':
        import message.language_en as lang
    else:  # 'jp' or else
        import message.language_jp as lang

そこで、この管理ロジックをmanager/language_manager.pyに分けます。ファイルを分けることで、変更の影響範囲を小さくし安全に修正できるようになります。。

  • 文言の修正があるとき:翻訳者や内容を変更したい人はmessage/language_**.pyを編集すればよいので、fortune.pyのコードロジックに誤って手を加えるリスクが減ります。
  • 新しい言語を追加したいとき:新しくlanguage_**.pyを作り、language_manager.pyに追加するだけで済みます。fortune.pyを編集する必要はありません。
  • テストがしやすい:言語ごとのデータや選択ロジックを個別に単体テストできるので、不具合の切り分けが容易です。

まとめると、分けることで読みやすさ・安全性・拡張性・チーム開発の効率が向上します。これがパッケージ化・責務の分離(Single Responsibility Principle)の本質です。

Pythonでのパッケージ構成とモジュール分割の最終形

最終的に、message/が「文言や運勢のデータを格納する場所」、manager/が「選択・設定などの管理ロジックをまとめる場所」という構成にします。

fortune_cookie/
├─ fortune.py
├─ message/
│   ├─ __init__.py
│   ├─ language_en.py
│   └─ language_jp.py
└─ manager/
    ├─ __init__.py
    └─ language_manager.py

新しく、次のディレクトリとファイルを作ってください。

  • manager/
  • __init__.py
    • Pythonにmanager/をパッケージと認識させるために必要なファイルです。
  • language_manager.py
スポンサーリンク

language_manager を使った言語管理のモジュール化と設計改善

「言語の選択と管理」を専門に行うモジュールを作ります。

# manager/language_manager.py

def get_language_module(lang_code):
    '''言語コードに応じてモジュールを返す'''
    if lang_code == 'en':
        from message import language_en
        return language_en
    elif lang_code == 'jp':
        from message import language_jp
        return language_jp
    else:
        print('対応していない言語コードです。日本語を使用します。')
        from message import language_jp
        return language_jp

language_managerモジュールには、言語モジュールを返すget_language_module()関数があります。この関数は、言語コードに対応したモジュールを動的に読み込み、そのモジュールをreturnで返します。

fortune.pyは次のように書き換えます。

# fortune.py
import random

import manager.language_manager as lang_manager


def main():
    # 言語を選択
    lang_code = input('言語を選んでください (jp/en): ')
    # 言語コードに対応したモジュールが返される
    lang_module = lang_manager.get_language_module(lang_code)
    messages = lang_module.MESSAGES
    fortunes = lang_module.FORTUNES

    # クッキーを開きますか?
    print(f"=== {messages['app_title']} ===")
    answer = input(messages['prompt_open'])

    if answer == 'y':
        print(f"{messages['fortune_prefix']} {random.choice(fortunes)}")
    else:
        print(messages['goodbye'])


if __name__ == '__main__':
    main()

fortune.pyの修正点は次の通りです。

  • 【4行目】import manager.language_manager as lang_manager
    • language_managerモジュールを読み込みます。
  • 【11行目】lang_module = lang_manager.get_language_module(lang_code)
    • get_language_module()関数から言語コードに対応したモジュールを受け取ります。
  • 【12, 13行目】言語データモジュールの、変数lang_module.MESSAGESlang_module.FORTUNESを、変数messagesfortunesに代入し直しています。
    • 改めて別の変数へ代入しなくても、print出力部分をlang_module.MESSAGES['app_title']のように書いても問題ありません。しかし、これだと可読性が低くなると判断したため、敢えて変数messages, fortunesに代入し直しています。短い記述(ローカル変数への代入)は、コードが簡潔になり、わずかでも可読性が上がるのであれば検討する価値があります。
  • 【25, 26行目】今回から、fortune.pymain()を、if __name__ == '__main__':のブロックの中に入れます。
    • こうすることで、他のモジュールからfortuneimportされた時に、main()が実行されるのを防ぐことができます。

Python多言語対応アプリ:モジュール化のメリットまとめ

  • 新しい言語を追加しても修正箇所はlanguage_manager.pyのみで済むようになった。
  • fortune.pyはアプリの処理ロジックに専念できる。
  • get_language_module()が単体でテスト可能になった。
    • これまで、fortune.pymain()関数を実行しなければ、言語データ(モジュール)が正しく読み込めているかどうかを確認することができませんでしたが、これからはlanguage_manager.pyを単体でテストすることができます。
# manager/language_manager.py

def get_language_module(lang_code):
    '''言語コードに応じてモジュールを返す'''
    if lang_code == 'en':
        from message import language_en
        return language_en
    elif lang_code == 'jp':
        from message import language_jp
        return language_jp
    else:
        print('対応していない言語コードです。日本語を使用します。')
        from message import language_jp
        return language_jp


if __name__ == '__main__':
    # テスト用の一時的なsys.pathの追加
    import sys
    sys.path.append(f'{__file__}/../../')

    from pprint import pprint

    lang_module = get_language_module('jp')
    pprint(lang_module.MESSAGES)
    pprint(lang_module.FORTUNES)
テスト用のコード
  • 各モジュール単体でテストしたい場合は、if __name__ == '__main__':ブロック内にテスト用コードを記述することで安全に開発を行うことができます。
  • sys.pathは、Pythonが「import するときに、どこを探すか」を記録した探索パスのlistです。上記コードの6行目, 9行目のfrom message import language_xxは、fortune_cookie/がルートディレクトリであることを前提にモジュールまでの経路を示しています。しかし、manager/language_manager.pyを直接実行すると、manager/をルートディレクトリとして認識してしまい、importが失敗します。そのため、import時にエラーにならないように、正しいルートディレクトリを設定するための処理を加えています。
    • __file__は特殊変数で、実行中のファイルのパスが格納されています。language_manager.pyまでのパスを基準に、/../../の表記を付け加えることで、現在地から相対的に2つ上の階層(fortune_cookie/)を指定しています。
      (相対パスについては専用のレクチャーで学習します。)

相対パス:

f'{__file__}/../../'
# "__file__"を実際のパスに置き換えてみた例
C:\Users\xxx\Documents\python101\fortune_cookie\manager\language_manager.py/../../
Pythonでは、パス区切り文字がバックスラッシュ\でもスラッシュ/でも、通常は正しく処理されます。(ただし、クロスプラットフォームで確実に動作させるには、osモジュールを使ってパスを操作することが推奨されます。)

絶対パス:

C:\Users\xxx\Documents\python101\fortune_cookie
スポンサーリンク