[Python] 環境変数PYTHONPATHを使用する際にハマりそうな注意点
pythonを動かす時、PYTHONPATH
という環境変数が参照されます。
これはsys.path
に影響を与える大事な環境変数なのですが、
ちょっと使い方を間違えると意図しないパスが追加されてしまう可能性があるということに気づいたので、
備忘録ですがご紹介します。
検証環境
- Python3.10
(一応Python2.7でも動かしてみましたが、同じ結果でした)
基本の動き
環境変数PYTHONPATH
に値が入っていると、それがsys.path
に反映されます。
例えばこんな感じです。
(結果は見やすいように整形しています)
$ pwd /Users/hirano.shigetoshi $ PYTHONPATH="/aaa/bbb" python ccc/ddd.py [ '/Users/hirano.shigetoshi/ccc', '/aaa/bbb', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python310.zip', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload', '/opt/homebrew/lib/python3.10/site-packages' ]
2つ目に指定したPYTHONPATH
が入っていることがわかります。
先頭に入っているのはスクリプトディレクトリで、
今回の場合起動スクリプトはccc/ddd.py
を指定しているので、
そのディレクトリであるccc
が入っています。
PYTHONPATH
は(PATH
などと同じように):
で区切って複数指定することも可能です。
PYTHONPATH
指定時の注意
基本の動きがわかったところで、今回気づいた注意点です。
PYTHONPATH
が未定義or空文字列の場合
環境変数PYTHONPATH
が未定義だったり、定義されていても空文字列が入っている場合、
sys.path
には何も変化を及ぼしません。
$ PYTHONPATH="" python ccc/ddd.py [ '/Users/hirano.shigetoshi/ccc', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python310.zip', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload', '/opt/homebrew/lib/python3.10/site-packages' ]
空文字列なんだから当たり前じゃね?
そう、当たり前なんですが、これはsys.path
の
「空文字列はカレントディレクトリを表す」という性質と突き合わせると違和感があります。
「空文字列はカレントディレクトリを表す」について一応見てみましょう。
$ ls -l ccc total 4 -rw-r--r-- 1 hirano.shigetoshi staff 0 3 24 12:08 __init__.py -rw-r--r-- 1 hirano.shigetoshi staff 27 3 24 11:45 ddd.py $ python Python 3.10.6 (main, Aug 11 2022, 13:36:31) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.path ['', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python310.zip', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload', '/opt/homebrew/lib/python3.10/site-packages'] >>> sys.path = sys.path[1:] >>> sys.path ['/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python310.zip', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload', '/opt/homebrew/lib/python3.10/site-packages'] >>> import ccc Traceback (most recent call last): File "<stdin>", line 1, in <module> ModuleNotFoundError: No module named 'ccc' >>> sys.path.insert(0, "") >>> sys.path ['', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python310.zip', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload', '/opt/homebrew/lib/python3.10/site-packages'] >>> import ccc >>> ccc.__file__ '/Users/hirano.shigetoshi/ccc/__init__.py'
sys.path = sys.path[1:]
で ''
をsys.path
から消した状態で import ccc
すると「そんなものはない!」と言われます。
importはsys.path
からファイルを探す仕組みですので当たり前です。
次にsys.path.insert(0, "")
でsys.path
に''
を追加してimport ccc
するとパッケージがimportできました。
確かに''
がカレントディレクトリを指していることがわかります。
空文字列は立派な値ですし、 もしかしたらユーザがカレントディレクトリを追加したい意思を持って空文字列にしているという可能性もゼロじゃない(本当?)ので、 無視されてしまうのはちょっと乱暴は気もしますが、 さすがに意図せず空文字列になっている可能性の方が圧倒的に高いと思いますので、 この挙動の方が弊害は少ないという判断なのかなと思います。
PYTHONPATH
は :
で区切って複数書ける
さて、今度は複数のPYTHONPATH
を:
で区切った場合の話です。
環境変数PATH
と同じように:
で結合できますので、PATH
の慣例に従ってこんなふうに書けます。
PYTHONPATH="/aaa/bbb:$PYTHONPATH"
PYTHONPATH
が未定義の状態でこれを実行するとどうなるでしょうか?
$ unset PYTHONPATH $ PYTHONPATH="/aaa/bbb:$PYTHONPATH" python ccc/ddd.py [ '/Users/hirano.shigetoshi/ccc', '/aaa/bbb', '/Users/hirano.shigetoshi', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python310.zip', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload', '/opt/homebrew/lib/python3.10/site-packages' ]
3つ目にご注目。カレントディレクトリが追加されました! 実際環境変数として渡された文字列は
/aaa/bbb:
です。
未定義のPYTHONPATH
は空文字列として展開されます。
ここまでの話の流れだと、この:
の後ろの空文字列はsys.path
に影響を及ぼさないで欲しいと思うのですが、
実際は、この空文字列はカレントディレクトリを指していると解釈されてしまいます。
PYTHONPATH="/aaa/bbb:$PYTHONPATH"
という書き方は、
「すでに設定されているものを採用しつつ左側に追加する」という意図で書きました。
しかし残念ながらこの書き方はPYTHONPATH
では意図通りではない動きになっています。
元のPYTHONPATH
が定義済みの空文字列ならまだしも、
未定義であった時でもカレントディレクトリとして追加されてしまうのでかなり怖いです。
回避方法
とはいえ、 「すでに設定されているものを採用しつつ追加をする」 という発想での書き方は普通に行いたいですので、 その場合は、以下のように書く必要があります。
$ unset PYTHONPATH $ PYTHONPATH="/aaa/bbb${PYTHONPATH:+:}$PYTHONPATH" python ccc/ddd.py [ '/Users/hirano.shigetoshi/ccc', '/aaa/bbb', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python310.zip', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10', '/opt/homebrew/Cellar/python@3.10/3.10.6_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload', '/opt/homebrew/lib/python3.10/site-packages' ]
${PYTHONPATH:+:}
はめっちゃ読みづらいですが、
:+
は「左辺の変数が未定義や空文字列でない時は右辺を出力する」です。
これでPYTHONPATH
が未定義や空文字列の時は:
も出力しないという書き方が実現できます。
ただこれを常用するかっていうと、ちょっと読みづらいしつらいところですね。 スクリプトの中で厳密にやりたいときに使用するかもってくらいが現実かなと思います。
まとめ
3.5行まとめ。
PYTHONPATH
が未定義or空文字列のときはsys.path
に影響を及ぼさないPYTHONPATH
が未定義でも空文字列でもない時はsys.path
に何らかの影響を及ぼす:
で区切った結果に空文字列があれば、それはカレントディレクトリとしてsys.path
に登録するPATH
と同じ気分で繋げる時は特に注意
特に触っていてハマったとかではなく、調べ物をしている中でたまたま気づいたのですが、
正直この事情を全部気にしながら環境作りをするのはかなり大変なので、
PYTHONPATH
は使いたくないなぁというのが感想です。
以上、どなたかの理解に繋がれば幸いです。