Pythonのモジュールキャッシュ作成/使用条件を確認してみる
前書き
Pythonをいじっているとにょきにょき発生する__pychache__。
内部にはコンパイル済みモジュール.pycが格納される。
リファレンスには、次のような記述がある。
ちょっと変形すると、次のような感じだ。(引用表記してるけどかなり改変してるよ)
Pythonがキャッシュをチェックしない場合
- コマンドラインから直接モジュールが読み込まれた場合
常に再コンパイルされ、結果を保存することはない。- ソース・モジュールのない場合
コンパイル済みモジュールはソース・ディレクトリになくてはならない。
これを実際に試してみるだけの記事。
Windows前提の記事だけど、Macユーザもなんとなくわかるよね?
(逆のことをMacユーザに言われたらキレる自信がある)
本文
次のように意味のないコードを書いた。これでも立派なモジュールである。
print('hogee')
そしてディレクトリ階層は次のとおり。
hoge_py/
└ hoge.py
階層と呼ぶのもおこがましいが、ここらへんは明確にしないといけない。
コマンドラインから直接モジュールが読み込まれた場合
つまりこういうこと。もちろん見やすいように成形はしてあるが。
C:\...\hoge_py>dir
2017/10/30 17:09 <dir> .
2017/10/30 17:09 <dir> ..
2017/10/30 17:20 14 hoge.py
C:\...\hoge_py>python hoge.py
hogee
C:\...\hoge_py>dir
2017/10/30 17:09 <dir> .
2017/10/30 17:09 <dir> ..
2017/10/30 17:20 14 hoge.py
見てわかるように、実行前後での変化はない。
逆に、コマンドラインから直接モジュールが読み込まれない場合とは?
こういうことである。もちろんこちらも成形済み。
C:\...\hoge_py>dir
2017/10/30 17:09 <dir> .
2017/10/30 17:09 <dir> ..
2017/10/30 17:20 14 hoge.py
C:\...\hoge_py>python
>>> import hoge
hogee
>>> exit()
C:\...\hoge_py>dir
2017/10/30 17:41 <dir> .
2017/10/30 17:41 <dir> ..
2017/10/30 17:20 14 hoge.py
2017/10/30 17:41 <dir> __pycache__
C:\Users\...\hoge_py>dir __pycache__
2017/10/30 17:41 <dir> .
2017/10/30 17:41 <dir> ..
2017/10/30 17:41 151 hoge.cpython-36.pyc
確かにキャッシュファイルが作られているのがわかる。
また、ファイル名からコンパイル環境がわかるのも便利だ。
ソース・モジュールのない場合
さきほどの実験の続き。hoge.pyを消してやると...
C:\...\hoge_py>del hoge.py
C:\...\hoge_py>dir
2017/10/30 17:41 <dir> .
2017/10/30 17:41 <dir> ..
2017/10/30 17:41 <dir> __pycache__
C:\...\hoge_py>python
>>> import hoge
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'hoge'
>>> exit()
C:\...\hoge_py>dir
2017/10/30 17:41 <dir> .
2017/10/30 17:41 <dir> ..
2017/10/30 17:41 <dir> __pycache__
当然ながら、hogeモジュールをインポートすることは出来ない。
ここで、先ほど生成されたキャッシュを次の手順で利用してみよう。
このように、モジュールをインポート出来るようになる。
C:\...\hoge_py>python
>>> import hoge
hogee
バージョンがファイル名に含まれる利点が失われているのが少々残念である。
なお、このときのディレクトリ階層は次のようになっている。
hoge_py/
└ hoge.pyc
...うん。階層と呼ぶのも難だが、説明を明確にするには必要なのだ。
おまけ - キャッシュを利用可能にするバッチ
最後は簡単な説明で済ませてしまったが、これをCUIで書くと次のようになる。
rem rm *.py
move .\__pycache__\* .\
rd .\__pycache__ /q
ren *.pyc ????????????????.pyc
remはコメントアウトの意味だ。
一行目は完全に実装を隠したいデストロイヤー向けなのである。
なお、リネームに随分回りくどい方法を用いているのは理由がある。
説明するのが面倒なので、サンプルとリンクだけ置いておく。
上手くいきそうでいかない例
C:\...\hoge_py\__pycache__>dir
2017/10/30 20:03 <dir> .
2017/10/30 20:03 <dir> ..
2017/10/30 19:55 151 hoge.cpython-36.pyc
C:\...\hoge_py\__pycache__>dir *.cpython-36.pyc
2017/10/30 19:55 151 hoge.cpython-36.pyc
C:\...\hoge_py\__pycache__>ren *.cpython-36.pyc *.pyc
C:\...\hoge_py\__pycache__>dir
2017/10/30 20:03 <dir> .
2017/10/30 20:03 <dir> ..
2017/10/30 19:55 151 hoge.cpython-36.pyc
とりあえず思い通りの結果が出る例
C:\...\hoge_py>dir
2017/10/30 20:03 <dir> .
2017/10/30 20:03 <dir> ..
2017/10/30 19:55 132 hoge.cpython-36.pyc
C:\...\hoge_py>ren *.cpython-36.pyc ????????????????.pyc
C:\...\hoge_py>dir
2017/10/30 21:56 <dir> .
2017/10/30 21:56 <dir> ..
2017/10/30 19:55 132 hoge.pyc
おまけ - ソースを置き換えるスクリプト
配布目的ならsetup.pyを書けばよいので、これはただの嫌がらせである。
むしゃくしゃしたときに実装をすべて隠蔽してやろう。
import os, sys, glob, importlib, shutil
from distutils import dir_util
# 自分以外の.pyファイルをすべて取得
py_files = glob.glob('*.py')
py_files.remove(sys.argv[0])
# 片っ端からインポートして消す
for py_file in py_files:
importlib.__import__(
os.path.splitext(os.path.basename(py_file))[0]
)
os.remove(py_file)
# pycacheの中身を今のディレクトリに移す
dir_util.copy_tree('./__pycache__', './')
shutil.rmtree('./__pycache__')
# pycの中身をリネーム
pyc_files = glob.glob('*.pyc')
for pyc_file in pyc_files:
basename, ext = os.path.splitext(os.path.basename(pyc_file))
os.rename(pyc_file, basename.split('.')[0] + ext)
書き捨てなのでぐちゃぐちゃなコードだ。特にインポートの多さ。*1
とにかく、これを使うと次のように嫌がらせが出来る。
C:\...\hoge_py>dir
2017/10/30 21:46 <dir> .
2017/10/30 21:46 <dir> ..
2017/10/30 21:39 0 fuga.py
2017/10/30 21:39 0 hoge.py
2017/10/30 21:39 0 piyo.py
2017/10/30 21:42 726 replace_source_to_cache.py
C:\...\hoge_py>python replace_source_to_cache.py
C:\...\hoge_py>dir
2017/10/30 21:46 <dir> .
2017/10/30 21:46 <dir> ..
2017/10/30 21:46 132 fuga.pyc
2017/10/30 21:46 132 hoge.pyc
2017/10/30 21:46 132 piyo.pyc
2017/10/30 21:42 726 replace_source_to_cache.py
追記:組み込みモジュールpy_compileを使うと、もっと簡単に書ける。
import py_compile
py_compile.compile('hoge.py', cfile='hoge.pyc')
.pycファイルの移動をサボれるだけだが。
*1:Python3.4以降ならpathlib使うべき