site.py ってどうやってパスを設定しているの?

なんとなくしか理解していなくて、どうでも良いことなんだけど、よ〜く考えてみるとなんか不思議になってきて気になって仕方が無くなるシリーズ。略して「なんよ」。第一弾はpythonのsite.pyがどうやってsite-localな追加モジュールなどを追加しているのか編。

python起動時の処理

pythonを対話的に起動するとmain() -> Py_Main() -> Py_Initilize()というように処理が進み、初期設定が行われる。正確にはPy_InitializeEx()関数にて、

  1. frameを初期化
  2. smallint を確保・初期化
  3. ByteArray 初期化
  4. floatのlittle/big判定*1
  5. 予約語の登録
  6. sys, builtins, modulesの確保
  7. 例外オブジェクトの登録
  8. import(というかImport_FileTab)の初期化
  9. meta_path, path_importer_cache, path_hookの確保・登録。zipimportのインポート
  10. mainモジュールの確保

とか結構いろんなことをやってる。python起動時に-vをつけて実行すると、zipimport当たりからの動作がエラー出力に表示される*2python立ち上げる度にこれだけのことがなだれのように行われるわけで、そりゃmod_pythonとかのありがたさが、、、ってそれはまた違う話。
今回は、この後に(-Sで抑制していなければ)呼び出されるinitsite()関数について。

initsite()

と、言ってもこの関数でやっていること自体は site.py(c)を呼び出すだけ。呼び出し終わったら、エラーチェックして、問題なければDECREFしている。

site.py

で、呼び出されるpythonモジュールsite.pyはどうなっているかというと、、、
前半はユーティリティ関数を延々と宣言していて、最後のほうのmain()関数にて全処理を羅列している。そのあとscript()というのが宣言してあるけど、これはimportじゃなく直接実行された場合で今回は関係なし。
ちなみにsite.pyを直接実行すると、実際どういうパス構成になっているかなどの情報がプリントアウトされる。
で、main()は、、、短いので抜粋


site.py - main()

def main():
    global ENABLE_USER_SITE

    abs__file__()
    known_paths = removeduppaths()
    if (os.name == "posix" and sys.path and
        os.path.basename(sys.path[-1]) == "Modules"):
        addbuilddir()
    if ENABLE_USER_SITE is None:
        ENABLE_USER_SITE = check_enableusersite()
    known_paths = addusersitepackages(known_paths)
    known_paths = addsitepackages(known_paths)
    if sys.platform == 'os2emx':
        setBEGINLIBPATH()
    setquit()
    setcopyright()
    sethelper()
    aliasmbcs()
    setencoding()
    execsitecustomize()
    if ENABLE_USER_SITE:
        execusercustomize()
    # Remove sys.setdefaultencoding() so that users cannot change the
    # encoding after initialization.  The test for presence is needed when
    # this module is run as a script, because this code is executed twice.
    if hasattr(sys, "setdefaultencoding"):
        del sys.setdefaultencoding

main()の各行について

abs__file__()は現在sys.pathにあるアイテムをフルパスに変換しているだけ(どうもPEP-302というimport-hookの代替技術があるらしく、これをスキップしているんだけど、、、これはまた次の「なんよ」ネタかな)。
removeduppaths()は、またabspath()してnormcase()で大文字小文字を整合(posix系では変化なし)した後、set()で重複パスを外したuniqなパスリストを得てる(どうも無駄が多い気が・・・)。
次のifはpythonをmake installせずに直接buildpath上で実行する、という開発状態の場合にパスを追加する処理(どうも無駄が多い気が・・・)。
その次のifはというと、ユーザーディレクトリも見るかどうかのチェック。check_enableusersite()で調べていて、これは単に現プロセスがsetuid/gidプロセスでないことを確認しているだけ。このチェックがOKなら各ユーザディレクトリもsys.pathに追加され、更にusercustomizeが自動的にimportされる(無ければスルー)。
さて、次のaddusersitepackages()とaddsitepackages()が今回の「なんよ」の主題。両方とも候補パスをknown_hostsにaddsitedir()関数で追加したリストを返す。

ちょっとややこしいaddsitedir()

addsitedir()は単純に指定されたsitedirを追加するだけでなく、そのディレクトリに*.pthなファイルがあった場合これを解析して、内容に応じたパスを追加する。実際にpthの解析を行うのが addpackage()関数。と、いってもそう複雑でもなくて、頭がimport だったら内部をexecで実行、そうでなければファイルの各行の内情自体をパスとみなして追加するだけ。

これを使って、add(user)?sitepackages()は、以下のように動作する(Python 2.6.4 (r264:75706, Dec 7 2009, 18:43:55) [GCC 4.4.1] on linux2の場合)。

まずaddusersitepackages()はユーザーごとのパッケージディレクトリを追加する。デフォルトでは ~/.local/lib/python${VERSION}/site-packagesのような感じ。~/.localの部分はPYTHONUSERBASE環境変数で変更することが出来る。さらに {lib,local/lib}/dist-packagesを探して、存在するようであればそこも追加する。

次にaddsitepackages()で追加されるのはサイトレベルのパッケージディレクトリ。

  • ${prefix}/lib/python${VERSION}/dist-packages
  • ${prefix}/local/lib/python${VERSION}/dist-packages
  • ${prefix}/lib/dist-python

ディレクトリについて addsitedir()する。

これが今回の「なんよ」の答え。
なんであんなにどばどば、と入るのかと思ったらdist-packagesの中の*.pthファイルに書いてある内容からということね。

残りの処理

あとは

setquit()
quit()とexit()を定義している(組込みだと思ってた!知らんかった!)
setcopyright()
"Type "help", "copyright", "credits" or "license" for more information.

"の行を表示して、copyright, credits, licenseを定義。

sethelper()
help()をHelperクラスから作ってる(組込みだと思ってた!知らんかった!)
aliasmbcs()
MSのマルチバイトのための黒い関数
setencoding()
エンコーディング関係
execsitecustomize()
サイトカスタマイズモジュール sitecustomizeをimport
execusercustomize()
ユーザカスタマイズモジュール usercustomizeを可能ならimport

という感じ。

まとめ

  1. 追加パッケージに必要なパスは site.py で追加される
  2. site.py は 決められたディレクトリをsys.pathに追加する
  3. 更にsite.py は追加パッケージ情報をdist-packages/*.pthファイル等から読み出し、必要であれば実行、またはパス情報を sys.pathに追加する。
  4. その他、site.pyはhelp()やexit(), quit()などの便利機能を追加している。

逆に言えば普段はこれらの機能は全く必要ない場合も少なくないはずで、ダイエットしようと思ったら-S使うとかを検討したほうがよさそうと。
とりあえずabs__file__は削除してもいいんじゃ?とか思った。PEP-302が謎だけど。

*1:正直H8みたいにバスごとにエンディアンが変わるってんで無ければ動的に検出する意味が無いと思うんだけど・・・

*2:余談ですが、僕は良くpythonのバージョンを調べようとしてつい-vしてしまって「のわっ」っとなりますorz