45
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Python exe化]実行ファイル化したPythonプログラムでは__file__を使わない方が良い。[path バグる原因]

Last updated at Posted at 2021-04-26

はじめに

Pythonでプログラムを作っていく中で、他ファイル/フォルダを参照するという場面は一般的なものです。例えば

  • エクセルファイルを参照してユーザーの入力を反映する
  • 画像ファイルを参照して描画する
  • データ出力先のディレクトリを探す

といったような場面です。このような場合、参照先のディレクトリやファイルのパスを皆さんはどのように管理するでしょうか?

  • カレントディレクトリからの相対パス
  • main.pyからの相対パス
  • 絶対パスに変換して管理する

のような方法がまず思いつきます。

__file__

翻って、pythonでパスの管理をする際、よくお目にかかるであろう変数があります。__file__です。
__file__は、そのコード自身のパスを保持しています。python3.8まではカレントディレクトリからの相対パスでしたが、python3.9以降は絶対パスを保持しています。以下のように使ったりします。便利です。


# main.pyからの相対パスを絶対パスで管理
PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file.txt') # python3.8以前
PATH = os.path.join(os.path.dirname(__file__), 'file.txt') # python3.9以降
PATH = Path(__file__).parent // 'file.txt' # pathlibを使用した場合

さて、この__file__。通常はエラーの原因になりにくい変数です。
しかし、pythonファイルのexe化では、__file__には注意しなければなりません。
exe化したファイルの__file__変数に格納されているのは何でしょうか。

  1. exe化したファイル自身のパス
  2. もととなった.pyファイルのパス
  3. 存在しないファイルのパス
  4. 空文字列
  5. exe化の手法によってバラバラ

1.だったらうれしいですよね。
実は5.が正解となります。
これが結構厄介で、exe化したら動かなくなった!、の原因となることもあります。

ということで、いろいろな手法でexe化して、__file__の挙動を詳しく調べてみました。
結果と対策方法を見たい方 => 結果まとめ&対策方法

検証

実行ファイルにおける__file__, os.getcwd(), sys.executableの挙動を確認しました。

検証内容

以下の内容を検証します。

  • __file__: main.pyの相対パス(python < 3.9)、絶対パス(python >= 3.9)
  • os.getcwd(): カレントディレクトリの絶対パス
  • os.path.abspath(__file__): main.pyの絶対パス
  • Path(__file__).parent: main.pyがあるディレクトリ
  • sys.executable: Python インタプリタの実行ファイル(python.exe)の絶対パスを示す

検証環境

  • Windows10
  • Python 3.9.4
  • Poetry version 1.1.6
pyproject.toml
python = "^3.9,<3.10" 
pyinstaller = "^4.3"
cx-Freeze = "^6.6"
py2exe = "^0.10.4"

pythonのバージョンは、デフォルト指定で"^3.9"で、これは3.8<=pythonのバージョン<4.0を示しています。しかしpy2exeが"^3.9,<3.10"という指定のため上限も指定しています。

その他

デフォルトのカレントディレクトリC:\test
ターゲットファイルC:\test\test.py
実行ファイル名: test.exe
としています。

検証結果

pyinstaller, cx_Freeze, py2txtそれぞれでexe作成を行い、実行ファイルを実行します。
実行の仕方として、

  1. C:\testからコンソール実行
  2. test.exeのあるディレクトリに移動してコンソール実行
  3. エクスプローラーから本体をダブルクリックで実行
  4. デスクトップ作成したショートカットをダブルクリックで実行

の3種類試しています。

ソースコードは

test.py
import os
import sys
from pathlib import Path

print(f"1. __file__ =>                  {__file__}")
print(f"2. os.getcwd() =>               {os.getcwd()}")
print(f"3. os.path.abspath(__file__) => {os.path.abspath(__file__)}")
print(f"4. Path(__file__).parent =>     {Path(__file__).parent}")
print(f"5. sys.executable =>            {sys.executable}")

0. pythonでそのまま実行

コントロールとして、exe化せずにそのまま実行しました。

# poetry run pytnon test.py で実行

1. __file__ =>                  C:\test\test.py
2. os.getcwd() =>               C:\test
3. os.path.abspath(__file__) => C:\test\test.py
4. Path(__file__).parent =>     C:\test
5. sys.executable =>            C:\test\.venv\Scripts\python.exe

1~5で不審な点は見当たりません。poetryの仮想環境で検証しているため5.のパスは仮想環境内のpythonのパスを示しています。

1. pyinstallerの場合

以下のコマンドでexeの作成を行いました。

poetry run pyinstaller test.py

C:\test\dist\test\test.exeが実行ファイルのパスです。

結果

# dist\test\test.exe での実行
1. __file__ =>                  C:\test\dist\test\test.py
2. os.getcwd() =>               C:\test
3. os.path.abspath(__file__) => C:\test\dist\test\test.py
4. Path(__file__).parent =>     C:\test\dist\test
5. sys.executable =>            C:\test\dist\test\test.exe

# cd dist\test && test.exe での実行
1. __file__ =>                  C:\test\dist\test\test.py
2. os.getcwd() =>               C:\test\dist\test
3. os.path.abspath(__file__) => C:\test\dist\test\test.py
4. Path(__file__).parent =>     C:\test\dist\test
5. sys.executable =>            C:\test\dist\test\test.exe

# 本体をダブルクリックで実行
1. __file__ =>                  C:\test\dist\test\test.py
2. os.getcwd() =>               C:\test\dist\test
3. os.path.abspath(__file__) => C:\test\dist\test\test.py
4. Path(__file__).parent =>     C:\test\dist\test
5. sys.executable =>            C:\test\dist\test\test.exe

# ショートカットをダブルクリックで実行
__file__ =>                  C:\test\dist\test\test.py
os.getcwd() =>               C:\test\dist\test
os.path.abspath(__file__) => C:\test\dist\test\test.py
Path(__file__).parent =>     C:\test\dist\test
sys.executable =>            C:\test\dist\test\test.exe
  1. の結果は変です。4通りともC:\test\dist\test\test.pyというパスを表示しましたが、C:\test\dist\testにあるのはtest.exeでありtest.pyは存在しません。
  2. の結果で不審な点はありません。ダブルクリックの際は本体があるディレクトリを示しています。
  3. の結果で不審な点はありません(python3.9で実行しているので当たり前です)。
  4. の結果で不審な点はありません(1.のファイル名はおかしいですが親ディレクトリまでのパスはtest.exeのパスと等しいので使えるっちゃあ使える?)
  5. これはびっくり。python.exeでもNoneでもなく、作成した実行ファイルのパスが格納されています。このパスをもとに相対パスなどを指定してやるのが安全で便利そうです。

2. cx_Freezeの場合

cx_Freezeでexeを作成する場合、setup.pyという設定ファイルを記述する必要があります。今回はpy2txtのsetup.pyと差別化するため、setup_cx_freeze.pyという名前にしました。
ソースコードは以下のようになります。

setup_cx_freeze.py
import sys
from cx_Freeze import setup, Executable

build_exe_options = {"packages": ["os", "sys", "pathlib"], "excludes": ["tkinter"]}

setup(
    name = "test",
    version = "0.1",
    description = "My GUI application!",
    options = {"build_exe": build_exe_options},
    executables = [Executable("test.py")]
)

見よう見まねで作成しました。

exeの作成は以下のコマンドにより行いました。

poetry run python setup_cx_freeze.py build

C:\test\build\exe.win-amd64-3.9\test.exeが実行ファイルのパスです。

結果

# build\exe.win-amd64-3.9\test.exe での実行
1. __file__ =>                  test.py
2. os.getcwd() =>               C:\test
3. os.path.abspath(__file__) => C:\test\test.py
4. Path(__file__).parent =>     .
5. sys.executable =>            C:\test\build\exe.win-amd64-3.9\test.exe

# cd build\exe.win-amd64-3.9 && test.exe での実行
1. __file__ =>                  test.py
2. os.getcwd() =>               C:\test\build\exe.win-amd64-3.9
3. os.path.abspath(__file__) => C:\test\build\exe.win-amd64-3.9\test.py
4. Path(__file__).parent =>     .
5. sys.executable =>            C:\test\build\exe.win-amd64-3.9\test.exe

# 本体をダブルクリックで実行
1. __file__ =>                  test.py
2. os.getcwd() =>               C:\test\build\exe.win-amd64-3.9
3. os.path.abspath(__file__) => C:\test\build\exe.win-amd64-3.9\test.py
4. Path(__file__).parent =>     .
5. sys.executable =>            C:\test\build\exe.win-amd64-3.9\test.exe

# ショートカットをダブルクリックで実行
1. __file__ =>                  test.py
2. os.getcwd() =>               C:\test\build\exe.win-amd64-3.9
3. os.path.abspath(__file__) => C:\test\build\exe.win-amd64-3.9\test.py
4. Path(__file__).parent =>     .
5. sys.executable =>            C:\test\build\exe.win-amd64-3.9\test.exe
  1. の結果は変です。ファイル名でなく相対パスとして考えると、カレントディレクトリ中のtest.pyを指していますが、実際にそのようなファイルはありません。
  2. の結果で不審な点はありません。ダブルクリックの際は本体があるディレクトリを示しています。
  3. の結果は変です。os.path.join(os.getcwd(), __file__)という雰囲気です。
  4. 1.の結果の親を表すため、カレントディレクトリの相対パス.が表示されています。python.exeからの相対パスを知りたい場合にこのように書くのは危ないかな。
  5. pyinstallerと同様に、作成した実行ファイルのパスが格納されています。便利。

3. py2exeの場合

py2exeでexeを作成する場合、setup.pyという設定ファイルを記述する必要があります。今回はcx_Freezeのsetup.pyと差別化するため、setup_py2exe.pyという名前にしました。
ソースコードは以下のようになります。

setup_py2exe.py
from distutils.core import setup
import setuptools
import py2exe

setup(
    console = [{'script': 'test.py'}] # exe化するファイル
)

見よう見まねで作成しました。

exeの作成は以下のコマンドにより行いました。

poetry run python setup_py2txt.py py2txt

C:\test\dist\test.exeが実行ファイルのパスです。

結果

# dist\test.exe での実行
1. __file__ =>                  #
2. os.getcwd() =>               C:\test
3. os.path.abspath(__file__) => #
4. Path(__file__).parent =>     #
5. sys.executable =>            C:\test\dist\test.exe

# cd dist && test.exe での実行
1. __file__ =>                  #
2. os.getcwd() =>               C:\test\dist
3. os.path.abspath(__file__) => #
4. Path(__file__).parent =>     #
5. sys.executable =>            C:\test\dist\test.exe

# 本体をダブルクリックで実行
1. __file__ =>                  #
2. os.getcwd() =>               C:\test\dist
3. os.path.abspath(__file__) => #
4. Path(__file__).parent =>     #
5. sys.executable =>            C:\test\dist\test.exe  

# ショートカットをダブルクリックで実行
1. __file__ =>                  #
2. os.getcwd() =>               C:\test\dist
3. os.path.abspath(__file__) => #
4. Path(__file__).parent =>     #
5. sys.executable =>            C:\test\dist\test.exe                             

__file__は未定義でエラーとなりました。そのため__file__を用いる1. 3. 4. をコメントアウトしました。

  1. の結果で不審な点はありません。ダブルクリックの際は本体があるディレクトリを示しています。
  2. pyinstaller, cx_Freezeと同様に、作成した実行ファイルのパスが格納されています。便利。

まとめ

今回はpyinstaller, cx_Freeze, py2exe3種類の実行ファイル生成方法における、__file__, os.getcwd(), sys.executableの挙動を確認しました。
分かったこと

  1. __file__は実行ファイルの生成方法によって指し示す先が異なったり、相対・絶対パスが変わったりするので使わない方が良い。
  2. os.getcwd()は実行ファイルの生成方法に依らず、正しい挙動を示した。
  3. ダブルクリックの際の挙動はショートカット、本体で全く同じ挙動だった。
  4. sys.executableは、実行ファイルの場合は自分自身の絶対パスを返すという挙動だった。

ということで、カレントディレクトリからの相対パスでパスを表現したい場合は今まで通りで問題なさそうだが、自分自身の場所からの相対パスで表したい場合はsys.executableを使う必要がある。しかし、.pyのままだとsys.executableはpython.exeの場所を示してしまう。どうしよう。

実は

cx_freezeの公式ドキュメントに書いてありました。

Applications often need data files besides the code, such as icons. Using a setup script, you can list data files or directories in the include_files option to build_exe. They’ll be copied to the build directory alongside the executable. Then to find them, use code like this:

def find_data_file(filename):
   if getattr(sys, "frozen", False):
       # The application is frozen
       datadir = os.path.dirname(sys.executable)
   else:
       # The application is not frozen
       # Change this bit to match where you store your data files:
       datadir = os.path.dirname(__file__)
   return os.path.join(datadir, filename)

An alternative is to embed data in code, for example by using Qt’s resource system.

サンプルコードでは、任意のファイルにアクセスしたい場合に、実行ファイルからの相対パスを引数として、絶対パスを返してくれる関数です。
実行ファイル化された場合はif文のコードが実行され、Pythonのまま実行した場合はelse以降が実行されます。

cx_freezeの公式ドキュメントの内容ですが、pyinstaller, py2exeを用いた場合も同様の対処で解決可能だと考えられます。

45
28
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?