Python のおまじない `__main__` を読み解いてみよう

記事タイトルとURLをコピーする

こんにちは。自称ソフトウェアエンジニアの橋本 (@hassaku_63)です。

今回は Python の話をします。今回の話はすでに色々記事が出回っている N 番煎じの話ではありますが、初学者なら一度は素通りしがちな頻出トピックとも思いますので、自社のブログ媒体としても何かしら書いておこうと思いました。

私個人の近況はと言うと、Python はめっきり使わなくなりました。既存実装のメンテはだいたい Python 製なので読むことはあるのですが、コード改修が必要な類のタスク以外が割と多い感じです。最近だと CDK (TypeScript) の実装をレビューしたり、Serverless Framework 製の社内ツールを Runtime EoL の対応のために軽く触ったくらいです。

個人では Go や TypeScript を使うものが多いです。Go に関してだと社内配布するローカル MCP Server の開発業務効率化ツールの開発言語として Go の採用をしていたりしますが、とはいえあくまで個人として使っている感じです。

Python をやっていると良く次のようなコードに遭遇すると思います。

def main():
  pass # do something...
 
if __name__ == "__main__":
  main()

この if 文、中級者くらいであればなんのためにこの記述が必要なのか、その意図は理解できると思います。

ビギナーの方にとってはおそらく「おまじない」に見えることでしょう。

これは一体なんなのか、結局なんのために必要なのか?...本記事では、それらの疑問に一次情報の調査を踏まえて回答します。

前提

CPython のバージョンは 3.12.11 を前提とします。

本記事の想定読者はビギナーから中級者です。

この記事の記述はモジュール分割の知識があることをある程度前提としています。自作のコードをモジュール分割した経験があることと、 __init__.py というファイルの存在を知っており、実装者として最低限の使い方を知っているものと想定します。

結論

トップレベルに処理を書くと、ファイル名指定して実行した場合以外にも、外部から当該モジュールが import された場合にもそれが実行されてしまいます。後者の話は通常モジュール分割を進めるうえで不都合な場合がほとんどだと思います。分離されたモジュールとしての機能実装以外にも、そのモジュールをエントリポイントとして何かしら処理を実行したいのであれば、それは if __name__ == "__main__" を記述し、そのブロック内にロジックを収めましょう。

実際にこの書き方を使って嬉しいシーンがなんなのかは、「使用例」のセクションで例示しています。

そもそも if __name__ == "__main__" とはなんなのか?なぜメインロジックはこの if 文の中で呼ぶ必要があるのか?

イメージとしては「Python スクリプトとして *.py ファイルを実行するときに、インタプリタの引数に渡すことがあるファイルにはこれを書いておく」くらいのイメージを持っている方が多いんではないでしょうか。

このおまじないを書かずにロジックをトップレベルで書いてしまうと、そのファイルが外から import された場合でも、トップレベルに書かれた処理が実行されてしまいます。きちんとモジュール分割することを考えようとしたときに、これでは困ります。

ここで、そのファイルがどう呼び出されているのかのコンテキストを意識する必要があります。

メインのエントリポイントとして実行されている場合(インタプリタから直接呼ばれる場合)はそれほど意識することはありません。しかし、そうでない場合(別のファイルから import される場合)を考慮するのであれば、本記事のテーマである「おまじない」を理解する必要があります。

要素分解

構文要素に分解すると、掲題の記述は __name__ という変数と__main__ という値に分けることができます。これらが何なのかを知る必要があります。

__name__ という変数は、この文脈だとモジュールのメンバーということになります(Python 的にはこれもオブジェクトです)。以下のドキュメントにこの変数についての記載があります。

https://docs.python.org/3.12/reference/datamodel.html#module.__name__

要するに、この特殊変数は Python がモジュールを import するにあたって、Python ランタイムから対象ファイル(モジュール)がどういう名前で認識されているのかを示すものです。また、モジュールが直接実行されている場合はこの変数に __main__ がセットされることも明言されています。

The name used to uniquely identify the module in the import system. For a directly executed module, this will be set to "__main__".

__main__ という値に関しては次のドキュメントでも説明されています。公式の表現に倣うと、この if 文は「"top-level environment" が __main__ である場合」を判定していることになります。

see: __main__ — Top-level code environment

...とはいえ、上記の説明だけでは腑に落ちない方もあると思います。実例を示しましょう。

__name__ 変数の挙動を確かめる簡易実験

次のようなモジュール構成のプロジェクトがあるとします。

project_root/
├── calculator.py
├── string_utils.py ... このモジュールから、他のすべてのモジュールを import している
└── utils/
    ├── __init__.py
    └── math_operations.py

string_utils が他のすべてのモジュールを import しており、かつエントリポイントとして実行される想定であるとします。

# string_utils.py
 
# 自分自身の __name__ の値をチェックする debug print
# モジュール内のトップレベルで宣言されているため、自身が import された場合にはこの文が実行されてしまう
print("[module name] string_utils.py", __name__)
 
# do something..
 
if __name__ == "__main__":
    import calculator
    import utils
    from utils import MathOperations

モジュールのトップレベルでデバッグプリントを差し込んで、 __name__ の値をチェックしています。他のモジュールでも同様に記載します。

この状況で string_utils を実行します。次のような出力があるはずです。

$ python -m string_utils
[module name] string_utils.py __main__  # ← エントリポイント
[module name] calculator.py calculator
[module name] utils/math_operations.py utils.math_operations
[module name] utils/__init__.py utils

エントリポイントとして直接実行された string_utils__name__ の値が __main__ になっていることがわかると思います。

今度は calculator.py でも同じことをしてみましょう。

# calculator.py
 
print("[module name] calculator.py", __name__)
 
if __name__ == "__main__":
    import string_utils
    import utils
    from utils import MathOperations

calculator をエントリポイントとして実行してみましょう。

$ python -m calculator
[module name] calculator.py __main__  # ← エントリポイント
[module name] string_utils.py string_utils
[module name] utils/math_operations.py utils.math_operations
[module name] utils/__init__.py utils

今回はエントリポイントである calculator が __main__ になっていることがわかります。先ほどの例とはエントリポイントとなったファイルが異なるからです。

上述した、

この特殊変数は Python がモジュールを import するにあたって、対象ファイル(モジュール)がどういう名前で認識されているのかを示すものです。また、モジュールが直接実行されている場合はこの変数に __main__ がセットされることも明言されています

この記述の意味が、なんとなくでもイメージできたでしょうか?

本記事のお題に立ち戻ると、例の if 文の意味は「自分自身がエントリポイントとして直接実行された場合だけ、この if 文が True になる = if の中身が実行される」ということになります。

実際の使用例

イメージしやすい例を挙げるとするなら、特定の自作モジュールを開発している際に、そのモジュールの動作をテスト用に確認したい場合が挙げられます。boto3 など何かしらの SDK をラップするモジュールを自作しているとしましょう。

外から呼ばれる想定のモジュールなので、import 時に変なコードを実行してほしくはないですよね。でも、開発時だけ一時的にそのモジュール単体の機能がうまいこと動くかどうかを実際に動かしてみたいケースがあると思います。そんなときに、このおまじないを使うことができます。以下の要領です。詳しくは該当箇所のコメントを見てください。

# libs/aws/s3.py
# S3 Clinet をラップする自作モジュール
# 実装は適当なのであくまで擬似的な例示目的として。
import boto3
 
_client = None
 
 
def get_client():
  global _client
  if not _client:
    _client = boto3.client('s3')
  return _client
 
 
def get_object(client, bucket: str, key: str):
  res = client.get_object(Bucket=bucket, Key=key)
  return res['Body'].read()
 
 
# 上の機能がちゃんと動くかどうか、一時的に自分のローカル環境で試したい。
# このモジュールは外部から import して使う想定なので、import 時に検証用のコードが実行されたくない。
# このファイル (libs/aws/s3.py) がインタプリタから直接呼ばれている場合だけ、検証用コードが実行されるようにしたい。
#
# >>> python -m libs.aws.s3
if __name__ == "__main__":
  c = get_client()
  body = get_object(c, "my-bucket", "my-object.json")
  print(body.decode('utf-8'))

もう一つ、実用的な例としてはユニットテストの記述なんかも該当します。Basic Exampleif __name__ ... の記述を確認できます。

# 出典: https://docs.python.org/3.12/library/unittest.html#basic-example
import unittest

class TestStringMethods(unittest.TestCase):
    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')
 
    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())
 
    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)
 
if __name__ == '__main__':
    unittest.main()

こう書くことで、このモジュールを実行するとそのモジュール内で定義されたユニットテストを実行できます。

通常だと以下のように書くところ、

$ python -m unittest path.to.your.test_module

次のように書けます。

$ python -m path.to.your.test_module

「だから何?」って思われるかもしれませんが、両者は実質それほど変わりません。本記事の主旨でもありませんのでここの詳しい違いについては掘り下げないこととします*1

テストコードと if __name__ ... の利用方法という観点から述べるなら、特定の機能グループあるいは Test Size のものだけをまとめた「Test Suite」を作って、その単位に対してテスト適用を行ったり、追加で verbose フラグやカバレッジレポート出力などのテスト実行に関する追加要件をひとまとめにしたテスト実行のエントリポイントとして if __name__ == __main__: ... の書き方を使用すると思います。いわゆるタスクランナー的な用途で、そのタスクの詳細を記述する場所、というイメージが近いと思います。

詳しくは unittest 標準モジュールのドキュメントから "TestSuite" や "discovery" といったキーワードを探してみてください。

3.12 のドキュメント内だと Organizing test code セクションで提示されている事例が私の利用イメージに近いです。

-m オプション使用時のエントリポイント

モジュールシステムとエントリポイントの話をするなら __main__.py に関しても言及する必要があります。また、1つ前のセクションでも触れた -m オプションの有無で、実際に何が変わるのかに関する解説もあった方がよいです。・・・あった方がよいのですが、このへんの解説は別の記事で解説できればと思います。ほんの触りのだけ、ここで解説しておきます。

Python スクリプトの実行をするとき、だいたい次の2通りのやりかたをします。

# 自前のコードをお試し実行するときに使われがち
$ python path/to/my_module.py
 
# -m オプションをつけるやり方。pip とか unittest を使う際に出てきがち
# pip ツールの実行時に新バージョンが出ているときに出力される、ログで提案されるバージョンアップのコマンドもこの形式
$ python -m path.to.my_module

前者はおそらく Python 経験者なら誰しも通る実行方法でしょうから、説明を省きます。

後者、 python -m <name> 形式で実行する場合の話ですが、ここで<name>に入りうるのは「実行対象のファイル名を示すパスのセパレータをドット区切りに変換したもの」とは限りませんディレクトリ名(のセパレータをドット区切りにしたもの)を指定することも可能です

この場合、対応するディレクトリ内のどこがエントリポイントになるべきなのかを Python インタプリタが判断できる必要があります。それを示すための仕様として、Python では __main__.py という特殊な位置付けのファイルを用意することになっています。-m オプションを付けてディレクトリ名に対応するインポートパスを指定するとき、Python はこの名前のファイルをエントリポイントと判断します。

__main__.py については以下のドキュメントで言及されています。文量も少ないので、興味があればぜひ一読ください。

https://docs.python.org/3.12/library/__main__.html#main-py-in-python-packages

__main__.pyを使うべきケースもこのドキュメントで明示されています。すなわち、以下の記述です。

Most commonly, the __main__.py file is used to provide a command-line interface for a package. Consider the following hypothetical package, “bandclass”:

通常、ディレクトリ名はその配下に存在する関連機能群をひとつにまとめた抽象概念としての命名をするはずです。公式ドキュメントの例示で言うなら bandclass がそれに当たります。

しかし、ディレクトリ内でさらに細分化して、ファイルが複数できる場合もあることでしょう。このとき、ユーザーに対して本機能に関する CLI ツールを提供したいとします。そうなると、ユーザーに見せるインタフェースとしては bandclass 以下の個別ファイルの存在は意識させたくないはずです。以下のように呼び出せるとスッキリしそうですね。

$ python -m bandclass  # 実在するファイル名には対応してないけど、こう呼び出したい

このような指定方法で呼び出せるようにするには bandclass/ 直下に __main__.py というファイルが必要で、このファイル内に python -m bandclass が実行された場合に実行されてほしい処理を記述する必要がある、という感じです。

余談ですが、標準で入っている unittest も、このドキュメントで紹介しているのと全く同じやり方で CLI 機能を提供しています。Lib/unittest/__main__.py に実装がありますので、興味があれば眺めてみてください。python -m unittest で実行される処理の実態がこのリンク先のコードです。

https://github.com/python/cpython/blob/v3.12.11/Lib/unittest/__main__.py

まとめ

この記事では import システムに関連するいくつかの話を見てきました。実験用のコードで眺めてみるのが一番わかりやすいと思いますので、よかったらぜひ実験してみてください。

ちなみに実験用コード自体も生成AI に作らせています。プロンプトを貼っておきます。

なお、これは「ユニットテストの記述したモジュール複数個と、ファイル指定によって直接実行した場合に(そのモジュール内の)テストコードが実行される状態」をゴールとしています。

3つの python ファイルを作成して。名前は python 推奨の命名規則に従うならなんでもいい。

うち2つはトップレベルディレクトリに、1つはサブディレクトリを作成し、配下のモジュールとして定義せよ。

それぞれで unittest.TestCase を使ったテストケースを定義して、かつ `if __name__ == "__main__" を併用したテスト実行のコードを埋め込むこと。

これだけだと本記事の主旨に対して不完全なので、ここからさらに手書きでアレンジしています。

例えば生成された各モジュールのトップレベルに print 文を埋め込んてみる、などです。他にも微修正を加えたりしています。そのへんは適宜調整してみてください。

私は手で修正したのでこれ以降 AI は使っていませんが、各モジュールの __name__ 変数がどうなるか実験できるコードを追加するならば、追加して以下のようなプロンプトを投げてみるとよいと思います。

xxx.py から、他のすべてのモジュールを import する実装を追加して。

また、すべてのモジュールのトップレベルに、自分自身のファイルパスと、`__name__` 変数の値をデバッグプリントする処理を追加して。

余談

本当は CPython の内部に立ち入って、__main__ の正体に迫る記事を書くつもりでした。

記事の後半戦は趣味全振りで臨むつもりでしたが、流石に文量が増えすぎたので、泣く泣くこの記事からは割愛しています。

生成 AI 駆使しながらコード読む体験についてもシェアしたかったのですが残念です。

CPython の読解記事は私個人の Zenn アカウント hassaku63で出そうと思いますので、乞うご期待ください。

zenn.dev

*1:ただし、実際のところ、私なら後者のスタイルでの実行は(基本的に)しません。個別のファイルがテスト対象なら前者の指定方法で十分です。それに、unittest モジュール経由でテスト対象を呼び出す場合は unittest が提供する豊富なオプションが使えます。後者はそれを自前の実装でコントロールする必要があります

橋本 拓弥(記事一覧)

マネージドサービス部

内製開発中心にやってます。普段はサーバーレス関連や CDK を触ることが多いです