Python スクリをバイナリ化するビルドツール cx_Freeze を使っててよく遭遇するエラーとその対処についてまとめてみました。
前提
- Python 3.6
- cx_Freeze 5.1.1
(エラー1) NameError: name '__file__'
is not defined
__file__
は使えない。
(対策) sys.argv[0]
などで補う。
(エラー2) NameError: name 'exit' is not defined
exit()
は使えない。
(対策) sys.exit()
を使う。
(エラー3) AttributeError: 'NoneType' object has no attribute 'write'
argparse を例にする。
argparse は Usage や「コマンドライン引数が不正ですよ」といった旨を標準出力するが、cx_Freeze で Win32GUI でビルドしている場合は標準出力担当が消えてなくなる(標準出力を行う機能を持ったオブジェクトが None になる) ため、このようなエラーが出る。
決定的な対策方法はなく、工夫して凌ぐしかない。以下に二つほど挙げる。
対策1: try-except でラップする
たとえば拙作のプログラムランチャ incl では 例外時のエラーメッセージをシンプルにした上で、winapi の MessageBox で表示させる という対応をしている。素のエラーメッセージ(スタックトレース)全文を表示するよりは全然優しい。
対策2: 標準出力先を nul(null) に置き換える
トリッキーな方法として「標準出力自体を無効化する」やり方もある。
import os
import sys
# 標準出力先を nul(null) に置き換える
null_fp = open(os.devnull, 'w')
sys.stdout = null_fp
詳しくは Redirecting stdout to "nothing" in python - Stack Overflow に書いてあるが、sys.stdout に nul や null に相当するディスクリプタを指定させることで何も出力させないようにするというもの。
ただしこれを行うと 開発時(たとえばコンソールから実行する時)も標準出力が一切見えなくなってしまう ため不便だ。開発時は普段通り表示し、リリース時にのみ nul 指定させる、などさらに一工夫必要だろう。
(エラー4) ImportError: can't find module 'XXXXXXXX__main__'
実行ファイル名を変更した後で起動すると起きる。
詳しく
cx_Freeze では cx_Freeze.Executable('main.py')
という形でエントリーポイントを指定するが、この時の実行ファイル名は main.exe 固定となる。
この時、内部的にはおそらく以下のようになっていて、
-
(実行ファイル名)__main__
という名前のメインモジュールが定義されている-
(実行ファイル名)
部分は ビルド時点のものがハードコードされる
-
- 実行ファイルを実行すると、まず
(実行ファイル名)__main__
を開こうとする - この時の
(実行ファイル名)
部分は 実行時の実行ファイル名から動的に取り出す
したがって 「ビルド時点のハードコードされた実行ファイル名」と「実行ファイル名」が違っているとメインモジュールにヒットしない という挙動になってしまう。
対策: 実行ファイル名は変更しない
実行ファイル名を変更したい場合は、エントリーポイントとなる py ファイル名自体を変更すること。
上記例で言えば、main.py をビルドして出来た main.exe を mytool.exe に変更するのではなく、main.py を mytool.py に変更した上でビルドする(すると mytool.exe ができる)。
(エラー5) ImportError: DLL load failed: 指定されたモジュールが見つかりません。
実行ファイル実行時のカレントディレクトリが「実行ファイルのあるディレクトリ以外」になっている時に起きる。たとえば TKinter モジュールに対して起きる。
原因
DLL ファイルの同梱先が間違っており DLL 探索に失敗している。
対策
DLLは lib フォルダの中に入れる。
これも 拙作プログラムランチャ incl の話 になるが、以下のように修正した。
正しくない例:
+ incl
+ lib ★ 正しくはこっちに入れるべきなのに
+ tcl
+ tk
- incl.exe
- tcl86t.dll ★ DLL を実行ファイル直下に置いていた
- tk86t.dll ★
正しい例:
+ incl
+ lib
- tcl86t.dll ★ DLL を lib 配下に移した
- tk86t.dll ★
+ tcl
+ tk
- incl.exe
ちなみに、cx_Freeze でビルドしただけだと DLL は上記「正しくない例」の位置に配置されてしまう ので、「正しい例」の位置に配置させるためには追加手順(DLLファイルの move をビルドスクリに追記する等)が必要。