需要のないページ

プログラミングや趣味や。

Pythonのモジュールキャッシュ作成/使用条件を確認してみる

前書き

Pythonをいじっているとにょきにょき発生する__pychache__。
内部にはコンパイル済みモジュール.pycが格納される。

リファレンスには、次のような記述がある。

Python は2つの場合にキャッシュのチェックを行いません。ひとつは、コマンドラインから直接モジュールが読み込まれた場合で、常に再コンパイルされ、結果を保存することはありません。2つめは、ソース・モジュールのない場合で、キャッシュの確認を行いません。ソースのない (コンパイル済みのもののみの) 配布をサポートするには、コンパイル済みモジュールはソース・ディレクトリになくてはならず、ソース・ディレクトリにソース・モジュールがあってはいけません。

ちょっと変形すると、次のような感じだ。(引用表記してるけどかなり改変してるよ)

Pythonがキャッシュをチェックしない場合

  1. コマンドラインから直接モジュールが読み込まれた場合
    常に再コンパイルされ、結果を保存することはない。
  2. ソース・モジュールのない場合
    コンパイル済みモジュールはソース・ディレクトリになくてはならない。

これを実際に試してみるだけの記事。

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モジュールをインポートすることは出来ない。
ここで、先ほど生成されたキャッシュを次の手順で利用してみよう。

  1. キャッシュファイルをhoge_pyに移動する。
  2. キャッシュファイルの名前をhoge.pycにする。

このように、モジュールをインポート出来るようになる。


    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
        
12436288584_94d6bc46d2_b.jpg
コマンドプロンプトに慣れているなら当然のように使っていると思います。いまさら何の説明がいるのかというくらい基本的なものですけど、このコマンドが備えているほんの少しの便利な機能と、それを帳消しにするクソ仕様を知っていますか?

 

おまけ - ソースを置き換えるスクリプト

配布目的なら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使うべき