Python実践開発入門 関数やモジュールの分割

ここから前回のスクリプトをブラッシュアップしていきます。使用が1回きりの場合はさきほどのような1ファイルのスクリプトでも問題ないのですが、実務で日々使うようなものは短いコードであっても保守性や信頼性、テストのし易さを上げるために適切な単位でモジュール、関数等で分割します。

今回は以下の分割について考えてみます。

  • エントリーポイント
  • ビジネスロジック
  • 入出力
  • 定数・設定

エントリーポイント

エントリーポイントとは実行を開始するモジュールを指します。特に決まった決まりはありませんがmain、run、appといったモジュール名がよく使用されます。

入出力とビジネスロジック

最初に作成したmainの関数を再度見てみましょう。以下3つの処理から成り立っています。

  • 入力データファイルの読み込み
  • 集計処理
  • 集計結果の出力

1つの関数で複数の処理を行うと関数の役割が不明確になり以下のようなデメリットがあります。

  • 部分的な再利用が難しくなる
  • 機能単位でテストができない
  • トラブルが起きた際に原因究明が難しくなる

このため機能や役割ごとに関数やモジュールを分割することが望ましいと言えるでしょう。今回は入力・集計処理・出力とで関数を分けてみることにしましょう。

入出力

今回作成するスクリプトのメイン処理は、指定されたテキストファイルを入力データとし、コンソールへの出力が出力先となります。

  • 入力:テキストファイル
  • 出力:コンソール

案件によっては入力データをDBに変えたい、出力をファイルにしたい、といった入出力の変更が起こり得るのですが、分割することで変更が発生した際の影響範囲を限定的にすることができます。

集計処理(ビジネスロジック)

「ビジネスロジック」という単語には明確な定義があるわけではないのですが、一般的には業務処理の部分を指します。これらのロジックは別の案件で使いまわしたり、不具合が発生した場合に調査したりするため、他から分離されていることが望ましく、通常、業務要件ごとや機能ごと等でモジュール単位で適切に分割します。今回の例で言うと、集計処理がビジネスロジックに該当します。

定数

Pythonには一般的な不変な値を指す定数はありませんが、業務上の固定値のことを便宜上定数と呼ぶことにします。通常このような値をロジックの中に直接記述してしまうと、その値に変更が発生した場合、ロジック内部をくまなく探して修正する、という手間が発生します。こういったことを避けるため、通常定数値は別のファイルにまとめて記述します。今回の要件では以下の項目が定数として挙げられます。

  • 業務上の閾値:100

ファイル名は任意ですが、const、constants、setting、settings、configといったファイル名がよく使用されます。本書ではsettings.pyという名前を使用することにします。また、慣習として定数は全て大文字の変数名を使用します。また、本解説では省略しますがiniファイルやyamlファイルなどの外部ファイルなどの設定ファイルに定数を指定する場合もあります。

モジュールの分割例

では、前回のコードを実際に分割してみましょう。最終的には以下のような構成となります。

.
├ settings.py
├ aggregate.py
├ input_data.py
├ output_data.py
└ main.py

定数の実装

まずは定数ファイルからです。モジュール名はsettingsとします。

# settings.py
THRESHOLD = 100

ビジネスロジックの実装

次にビジネスロジックです。引数で指定されたデータリスト、しきい値に基づいて集計を行い、結果件数を返す関数を実装します。モジュール名はaggregateとします。

# aggregate.py

def aggregate(data_list, threshold):
    result_count = 0
    for value in data_list:
        # 閾値を超えた場合、集計対象
        if value > threshold:
            result_count += 1

    return result_count

入出力モジュールの実装

入出力をそれぞれ実装します。まず、入力データの取得処理は指定されたファイルパスを取得し、リストに変換して返します。モジュール名はinput_dataとします。

# input_data.py

def file2list(input_file_path):
    with open(input_file_path, "r") as f1:
        text = f1.read()

    # 改行区切りで行ごとのテキストのリストを構築する
    data_text_list = text.split("\n")
    if "" in data_text_list:
        data_text_list.remove("")

    # テキストのリストを数値のリストに変換する
    return [float(x) for x in data_text_list]

結果の出力処理は、引数で指定された結果をprintで出力します。モジュール名はoutput_dataとします。

# output_data.py

def output_result(result_count: int):
    print("集計結果:{}".format(result_count))

エントリーポイント

最後にエントリーポイントとしてmain.pyを実装します。

# main.py
from settings import THRESHOLD
from input_data import file2list
from aggregate import aggregate
from output_data import output_result
from sys import argv

def main():
    """ メイン処理 """

    print("処理を開始します。")

    # 入力データ検証・取得
    data_list = file2list(argv[1])

    # 集計処理
    result_count = aggregate(data_list, THRESHOLD)

    # 出力処理
    output_result(result_count)

if __name__ == '__main__':
    main()