#はじめに
2019年10月にリリースを予定しているPython3.8で新たに加わる変更をPython3.8の新機能 (まとめ)という記事でまとめ始めました。その中で比較的分量のある項目を別記事に切り出すことにしました。その第一弾がこの記事になります。
#バイトコンパイルとコンパイル結果のキャッシュについて
Pythonはインタプリター言語ですが、実行時にまずはソースコードをバイトコードに変換(バイトコンパイル)してから実行します。その際にバイトコードを同じ名前で拡張子が.pyc
というファイルにキャッシュとして格納します。ソースコードが変わっていない場合に再度コンパイルするのではなくそのファイルから読み込むことにより実行開始時間を短縮することができるからです。
Python 2 の時代はpycファイルはpyファイルと同じディレクトリに置かれる事になっていました。なのでpython を実行するとバラバラとファイルが増えて、まあそういうモノだと思ってましたが、あまりスマートなやり方では無かったですね。
例えばこのような構成のモジュールを考えてみましょう
my_module/
├── __init__.py
├── one.py
└── two.py
このモジュールを利用するプログラムをPython2で実行するとこのようになります。
my_module/
├── __init__.py
├── __init__.pyc
├── one.py
├── one.pyc
├── two.py
└── two.pyc
./my_module
の下のソースに横並びでキャッシュファイルが生成されていてちょっととっ散らかった印象を受けます。
この問題を解決するために、PEP-3147で「__pycache__というディレクトリを作ってそこに入れようよ」という提案がされ、Python 3.2で実装されました。その際にpycファイル中にpythonバージョン名が入るようになったので、複数のバージョンのpythonを混在させても無駄なコンパイルが発生しなくなりました。例えば、上の例を3.6と3.7で実行してみると以下のようになります。
my_module/
├── __init__.py
├── one.py
├── two.py
└── __pycache__
├── __init__.cpython-36.pyc
├── one.cpython-36.pyc
├── two.cpython-36.pyc
├── __init__.cpython-37.pyc
├── one.cpython-37.pyc
└── two.cpython-37.pyc
自動的に __pycache__というディレクトリが作られ、その下にバージョン名をファイル名に含むキャッシュファイルが作成されていることがわかります。
3.2でこの変更が加えられてから、pycファイルの作り直し判定をタイムスタンプ比較からハッシュ比較にできる変更(PEP-552: 3.7で実装)などがありましたが、__pycache__の位置はソースファイル(.py)直下であり続けました。
#変更の理由
ほとんどの場合、ソースコード直下で困らないのですが、Dockerなどのコンテナで実行する際に不都合がある場合があります。例えば __pycache__がroot権限でできてしまうとか、セキュリティ強化のためにルートファイルシステムをread onlyで実行するとキャッシュファイルが作られない(よって起動が遅くなる)とか。
この問題を避けるためにここで「環境変数設定で__pycache__の場所を指定できるようにする」という変更提案がされ、3.8で実装されることになりました。
なお調べていて気がついたのですが、これに似た変更はPEP-304として2003年に提案されていた様です。セキュリティ的な懸念などによりその時は提案取り下げになったのですが、その後のpythonの変更によりその懸念がなくなったため今回16年の時を経てリベンジされたわけです。
#変更の内容
以下の方法のいずれかで__pycache__の置き場所を指定できます。
-
PYTHONPYCACHEPREFIX
環境変数 -
-X
pycache_prefix コマンドラインオプション
変数名からもわかるように、これで指定されるのはprefix(トップディレクトリ)で、その配下にモジュール構成と同じディレクトリ構造でキャッシュファイルが格納される。
例えば、以下のような多段の階層構造を持つモジュールを考えてみます。
my_module/
├── __init__.py
├── one.py
├── two.py
└── sub_module
├── __init__.py
├── three.py
└── four.py
これを普通に実行した時には、以下のように各ディレクトリ毎に__pycache__ディレクトリができます。今度は3.8の開発版で実行してみます。
my_module/
├── __init__.py
├── one.py
├── two.py
├── __pycache__
│ ├── __init__.cpython-38.pyc
│ ├── one.cpython-38.pyc
│ └── two.cpython-38.pyc
└── sub_module
├── __init__.py
├── three.py
├── four.py
└── __pycache__
├── __init__.cpython-38.pyc
├── four.cpython-38.pyc
└── three.cpython-38.pyc
これを、export PYTHONPYCACHEPREFIX=/tmp/pycache
としてから実行すると、ソースコードのディレクトリツリーには何も生成されず、そのかわりに/tmp/pycache
に以下にのようなファイルができます。
.../my_module
├── __init__.cpython-38.pyc
├── one.cpython-38.pyc
├── two.cpython-38.pyc
└── sub_module
├── __init__.cpython-38.pyc
├── four.cpython-38.pyc
└── three.cpython-38.pyc
つまり、バイトコンパイルしたキャッシュファイル(.pyc)がソースコードのツリーと同様の構成で別ディレクトリに保管されるということです。
#どんな時に便利なのか?
一つはセキュリティ理由などでソースと同じ場所にpycファイルを置きたくない場合に使えると思います。例えば、Dockerでは以下のようにして実行時オプションでルートファイルシステムを read onlyで起動できます。
$ docker container run -it --rm --read-only <docker_image>
これまでのバージョンのpythonだとpycファイルは生成されませんが、例えば以下のような形で別の場所を指定し(ちなみにpythonの3.8のimageファイルはまだ公開されていないので仮にです(^^;)、
FROM python:alpine3.8
WORKDIR /usr/src/app
ENV PYTHONPYCACHEPREFIX=/usr/src/pycache
COPY . .
CMD python ./test.py
実行時にこのような形でVolume指定すれば、そこにpycファイルが書き込まれることになります。
$ docker container run -it --rm -v <volume_name>:/usr/src/pycache --read-only <docker_image>
こうすることにより、ルートファイルシステムは書き込み不可にしておきながら、キャッシュの恩恵を受けることができます。
#まとめ
Python3.8の新機能の一つである「コンパイルしたバイトコードファイルの置き場所を指定できる」について試してみました。もう少し有用な使い道があるよ、という方は教えていただけると助かります。