Pythonスクリプトの書き方(4パターン)

結果が同じでも、プログラムは何通りもの書き方があるのが普通です。

そのため、学びはじめの頃は「どの書き方が良いのか」悩んでしまうことがよくあります。そこで、今回はPythonスクリプトを対象にどのようなパターンで書き分けたら良いのかを考えてみます。

一般に用いられているPythonスクリプトの書き方のパターンは概ね以下の4つに分類できます。

パターン 摘要
1 逐次型 即興用
2 関数+実行文 パターン1を関数にまとめて書き換え
3 main()関数 パターン2を安全化
4 インポート時の実行回避あり 再利用可能化、if __name__ == ‘__main__’:の使用

はじめからパターンを決めている場合もあれば、パターンが遷移することもあります。よくあるのが、即興でパターン1で書き、書き換えているうちにパターン2,3となり、他のスクリプトからも利用したくなりパターン4に改良した、というケースです。

どのパターンを採用するかは、状況により異なりますので、各パターンの特性を理解しておく必要があります。そこで、今回は各パターンの特徴や利点について以下で説明します。

本記事の目次
「スクリプト」という言葉は文脈で意味が変わりますが、ここでは「1つのファイルからなるPythonプログラム」のことを示します。インタプリタ型言語という意味合いもあります。

サンプルプログラム

具体的なプログラムがあった方がわかりやすいので、「JPEGファイルを更新年月日でフォルダに振り分けるプログラム」を例に用います。

このプログラムは、カレントディレクトリにあるJPEGファイルを、年月日名のフォルダに振り分けてコピーします。コピー先はjpeg_datesフォルダとして、その中に年月日のフォルダがない場合は新規に作成します。

sample python script

パターン1:逐次型

単純に上から下に逐次実行するパターンです。処理と同じ順序でコードを読めるのでわかりやすいです。単に今回の処理だけなら、このパターンで十分です。

# file_to_dir_p1.py
import os
import datetime
import shutil
from pathlib import Path

current_dir = Path(".")

jpeg_dates = Path("./jpeg_dates")
jpeg_dates.mkdir(exist_ok=True)

for file in current_dir.glob("*.jpg"):
    # 更新日時の取得
    mod_ts = os.path.getmtime(str(file))
    mod_dt = datetime.datetime.fromtimestamp(mod_ts)

    # 年月日フォルダ作成
    dir_name = mod_dt.strftime("%Y_%m_%d")
    date_dir = jpeg_dates / dir_name
    date_dir.mkdir(exist_ok=True)

    # ファイルコピー
    shutil.copy2(str(file), str(date_dir))

パターン2:関数+実行文

パターン1のコードの一部を関数にまとめたパターンです。関数は先に定義しておき、実行部分はその後に書きます。Pythonは上から下にプログラム文を解釈するので、先に定義しないと関数を呼び出しできません。

パターン1を、機能そのままにコードの意味が理解しやすくなるように書き換えたパターンです。このようなコードの再構築を「リファクタリング」と言います。以下のように、意味のわかる名前の関数にまとめることで、コードの理解だけでなく修正も簡単になります。

# file_to_dir_p2.py
import os
import datetime
import shutil
from pathlib import Path


def datetime_to_y_m_d(dt):
    """日時を「年_月_日」に変換"""
    return dt.strftime("%Y_%m_%d")


def get_file_mod_datetime(file_path):
    """ ファイルの更新日時を取得"""
    mod_dt = os.path.getmtime(str(file_path))
    return datetime.datetime.fromtimestamp(mod_dt)


def copy_file_to_dir(dir_path, file_path):
    """ファイルをディレクトリにコピー"""
    dir_path.mkdir(exist_ok=True)
    shutil.copy2(str(file_path), str(dir_path))


current_dir = Path(".")

jpeg_dates = Path("./jpeg_dates")
jpeg_dates.mkdir(exist_ok=True)

for file in current_dir.glob("*.jpg"):
    mod_dt = get_file_mod_datetime(file)
    date_dir = jpeg_dates / datetime_to_y_m_d(mod_dt)
    copy_file_to_dir(date_dir, file)

関数にしておくことで、機能追加などの修正にも対応しやすくなります。例えば、PNGファイルを振り分ける処理も追加したい場合は、以下のコードを加えるだけで対応できます。

......

# PNGファイルの振り分け
png_dates = Path("./png_dates")
png_dates.mkdir(exist_ok=True)

for file in current_dir.glob("*.png"):
    mod_dt = get_file_mod_datetime(file)
    date_dir = png_dates / datetime_to_y_m_d(mod_dt)
    copy_file_to_dir(date_dir, file)
バグに変わる可能性のあるコード

この後のパターン3の効用を説明するために、バグに変わる可能性のあるコードを入れてあります。それは、同じ名前の変数mod_dtに、関数の中と外の両方で値を代入していることです。

.....

def get_file_mod_datetime(file_path):
    """ ファイルの更新日時を取得"""
    mod_dt = os.path.getmtime(str(file_path))
    return datetime.datetime.fromtimestamp(mod_dt)

.....

for file in current_dir.glob("*.jpg"):
    mod_dt = get_file_mod_datetime(file)
    .....

現状では、同じ名前であってもスコープが異なるので干渉せず、プログラムは問題なく動作します。しかし、プログラムを改良してゆく過程で、関数内の代入文が削除されてしまうと、不具合が発生します。試しに、関数内の代入文をコメントアウトして実行してみると、NameError: name 'mod_dt' is not definedが発生します。

.....

def get_file_mod_datetime(file_path):
    """ ファイルの更新日時を取得"""
    # mod_dt = os.path.getmtime(str(file_path)) # コメントアウト
    return datetime.datetime.fromtimestamp(mod_dt)

.....

関数内ではmod_dtは定義されなくなったので、グローバルから参照しようとしますが、まだ未定義なので例外を投げます。例外ならデバッガが教えてくれますが、mod_dtにグローバルのどこかで代入して実行できてしまうと、バグを気付かないまま残置してしまう可能性があります。

このような潜在的な危険性を回避するためには、日頃から「関数の中と外では同じ変数名を定義しない」ようにしますが、確実なのは次のパターン3を用いる方法です。

PyCharmではこのようなコードを検知すると、以下のように「Shadows name ‘xxx’ from outer scope」と警告を表示します。

pycharm inspection info

パターン3:main()関数

実行部分のコードをmain()関数で括るパターンです。こうすることで、変数の定義はすべて関数の中で行われるので、上記の潜在的な危険性は回避されます。

プログラムの処理はすべて関数にまとめ、main()関数必ずプログラムの最後で呼び出します

# file_to_dir_p3.py
import os
import datetime
import shutil
from pathlib import Path


def datetime_to_y_m_d(dt):
    """日時を「年_月_日」に変換
    """
    return dt.strftime("%Y_%m_%d")


def get_file_mod_datetime(file_path):
    """ ファイルの更新日時を取得
    """
    mod_dt = os.path.getmtime(str(file_path))
    return datetime.datetime.fromtimestamp(mod_dt)


def copy_file_to_dir(dir_path, file_path):
    """ファイルをディレクトリにコピー
    """
    dir_path.mkdir(exist_ok=True)
    shutil.copy2(str(file_path), str(dir_path))


def main():
    current_dir = Path(".")

    jpeg_date = Path("./jpeg_dates")
    jpeg_date.mkdir(exist_ok=True)

    for file in current_dir.glob("*.jpg"):
        mod_dt = get_file_mod_datetime(file)
        date_dir = jpeg_date / datetime_to_y_m_d(mod_dt)
        copy_file_to_dir(date_dir, file)

# 最後に呼び出す
main()
main()関数の定義は先頭でもOK

上記では、main()関数は最後に配置していますが、先頭に置いても大丈夫です。main()関数は編集する機会が多いので、先頭にある方がスクロールを省けて便利な場合もあります。

.....

# 先頭で定義
def main():
    current_dir = Path(".")

    jpeg_date = Path("./jpeg_dates")
    jpeg_date.mkdir(exist_ok=True)

    for file in current_dir.glob("*.jpg"):
        mod_dt = get_file_mod_datetime(file)
        date_dir = jpeg_date / datetime_to_y_m_d(mod_dt)
        copy_file_to_dir(date_dir, file)

def datetime_to_y_m_d(dt):
    .....

def get_file_mod_datetime(file_path):
    .....

def copy_file_to_dir(dir_path, file_path):
    .....

# 最後に呼び出す
main()

先頭でmain()関数を定義した場合でも、呼び出しは必ず最後です。Pythonはプログラムを上から順に実行しますが、プログラムが作動する起点(エントリポイント)は最後のmain()です。それまでに関数がすべて定義済みならば、関数どうしの順序を入れ替えても動作に支障はありません

パターン4:インポート時の実行回避あり

スクリプト内の関数を、他のプログラムからも再利用したい場合にこのパターンを用います。パターン3のままで他のプログラムからインポートして読み込むと、最後のmain()が実行されてしまいます。それを回避するためにif __name__ == '__main__':のブロックにmain()を入れます。

# file_to_dir_p4.py
import os
import datetime
import shutil
from pathlib import Path


def datetime_to_y_m_d(dt):
    """日時を「年_月_日」に変換
    """
    return dt.strftime("%Y_%m_%d")


def get_file_mod_datetime(file_path):
    """ ファイルの更新日時を取得
    """
    mod_dt = os.path.getmtime(str(file_path))
    return datetime.datetime.fromtimestamp(mod_dt)


def copy_file_to_dir(dir_path, file_path):
    """ファイルをディレクトリにコピー
    """
    dir_path.mkdir(exist_ok=True)
    shutil.copy2(str(file_path), str(dir_path))


def main():
    current_dir = Path(".")

    jpeg_date = Path("./jpeg_dates")
    jpeg_date.mkdir(exist_ok=True)

    for file in current_dir.glob("*.jpg"):
        mod_dt = get_file_mod_datetime(file)
        date_dir = jpeg_date / datetime_to_y_m_d(mod_dt)
        copy_file_to_dir(date_dir, file)


if __name__ == '__main__':
    main()

Pythonファイルは、必ず__name__という属性を持ちます。そのファイルがプログラムとして起動されると'__main__'が値として代入されます。つまり、__name__ == '__main__'が成立する時は、プログラムとして起動しているので、main()を実行します。

なお、以下のようにif __name__ == '__main__':のブロックに実行部分を入れるパターンもよく用いられますが、前述の危険性もあるので、main()関数を用いる方が好ましいです。

.....

def datetime_to_y_m_d(dt):
    .....

def get_file_mod_datetime(file_path):
    .....


def copy_file_to_dir(dir_path, file_path):
    .....


if __name__ == '__main__':
    current_dir = Path(".")

    jpeg_date = Path("./jpeg_dates")
    jpeg_date.mkdir(exist_ok=True)

    for file in current_dir.glob("*.jpg"):
        mod_dt = get_file_mod_datetime(file)
        date_dir = jpeg_date / datetime_to_y_m_d(mod_dt)
        copy_file_to_dir(date_dir, file)

上記のスクリプト内の関数を利用するには、同じディレクトリ内のスクリプトであれば、以下のようにインポートして関数を呼び出します。

import datetime
import file_to_dir_p4

now = datetime.datetime.now()
today_string = file_to_dir_p4.datetime_to_y_m_d(now)

.....

最後に

スクリプトはその場限りで作成することもよくあるので、パターン1や2を採用することが多いかもしれませんが、最終的には「パターン4」を用いることをお勧めします。

コードはなるべく再利用しやすくして、同じコードを書かないようにすることでバグも少なくできます。そのためには、常日頃から小さなステップでリファクタリングを重ねることが大事です。