__file__ が使えないケースと代替手段(frozen, zipimport)

__file__ はスクリプトのパスを取得する便利な変数だが、すべての状況で使えるわけではない。使えないケースと代替手段を理解しておく必要がある。

__file__ が使えないケース

インタラクティブシェル(Python REPL)
exec()eval() で実行されたコード
-c オプションでコマンドラインから実行した場合
PyInstaller などで exe 化した場合(frozen)
zip ファイルからインポートされたモジュール

インタラクティブシェルでの問題

# Python REPL で実行
>>> __file__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '__file__' is not defined

exec() での問題

code = '''
print(__file__)
'''
exec(code)  # NameError: name '__file__' is not defined

PyInstaller で exe 化した場合

PyInstaller でパッケージングすると、__file__ は一時ディレクトリを指すことがある。

# exe 化されたプログラムでは
print(__file__)  # C:\Users\...\AppData\Local\Temp\_MEI12345\script.py

実行ファイルの場所を取得するには sys.executable を使う。

import sys
import os

def get_app_path():
    if getattr(sys, 'frozen', False):
        # PyInstaller で frozen されている
        return os.path.dirname(sys.executable)
    else:
        # 通常の Python スクリプト
        return os.path.dirname(os.path.abspath(__file__))

sys.frozen 属性は PyInstaller によって設定される。

zipimport の場合

Python は zip ファイル内のモジュールをインポートできるが、その場合 __file__ は zip 内のパスを指す。

# module.py が archive.zip 内にある場合
import sys
sys.path.insert(0, 'archive.zip')

import module
print(module.__file__)  # archive.zip/module.py

このパスは通常のファイル操作には使えない。

代替手段

inspect モジュールを使う

inspect モジュールは呼び出し元の情報を取得できる。

import inspect

def get_script_path():
    frame = inspect.currentframe()
    if frame is None:
        return None
    
    # 呼び出し元のフレームを取得
    caller_frame = frame.f_back
    if caller_frame is None:
        return None
    
    return caller_frame.f_code.co_filename

sys.argv[0] を使う

コマンドラインから実行された場合、sys.argv[0] にスクリプトのパスが入る。

import sys
import os

script_path = os.path.abspath(sys.argv[0])
script_dir = os.path.dirname(script_path)

ただし、python -m module で実行した場合や、インポートされた場合は期待通りに動かない。

importlib.resources を使う

パッケージ内のリソースファイルにアクセスする場合は importlib.resources が最も信頼性が高い。

# Python 3.9 以降
from importlib.resources import files

# パッケージ内のファイルを取得
config_text = files('mypackage').joinpath('config.json').read_text()

# パスが必要な場合
from importlib.resources import as_file

with as_file(files('mypackage').joinpath('data.bin')) as path:
    # path は一時ファイルのパス(zip 内でも動作)
    with open(path, 'rb') as f:
        data = f.read()

堅牢な実装例

様々な状況に対応する関数の例を示す。

import sys
import os

def get_base_path():
    """スクリプトまたは実行ファイルのディレクトリを取得"""
    
    # PyInstaller で frozen されている場合
    if getattr(sys, 'frozen', False):
        return os.path.dirname(sys.executable)
    
    # __file__ が利用可能な場合
    if '__file__' in globals():
        return os.path.dirname(os.path.abspath(__file__))
    
    # sys.argv[0] にフォールバック
    if sys.argv[0]:
        return os.path.dirname(os.path.abspath(sys.argv[0]))
    
    # 最終手段:カレントディレクトリ
    return os.getcwd()