7
10

More than 1 year has passed since last update.

Python で自作ライブラリの import を極めたいメモ

Last updated at Posted at 2022-10-25

背景

  • Python で自作ライブラリ(モジュール)を作っている, Python アプリのコードのモジュール化をしている.
  • 自作ライブラリのモジュールは他の自作モジュールを参照することもある
  • unittester とかではしらせずらい(GUI 使っていたりして, UI のコンポーネントをテストしたい), テスト .py つくる, なんかの test フレームワーク使うのもめんどいので, モジュールに単体テストコード書いてぱぱっと時短ですませたい..
    • テストファイルわけると, vscode とか使っている場合, working directory 設定とかめんどい...
    • from . import myui とかあるけど単体で動かす場合はエラーになってしまう...
  • 自作ライブラリをパッケージングしているときに, pip とかでインストールされた site-package のほうではなくて, 手元(e.g. git repo)の開発版のほうをみるようにしたい

まとめ

  • 基本相対パスは使わない. from mymod import mod1 とか import mymod.mod1 とかの記述にする.
  • 親階層の参照が必要なら python で呼ばれる .pysys.path.append("..") などでパスを通す
  • 開発 repo 側のソースコードの import を優先させたい場合は PYTHONPATH 環境変数指定が推奨

前提条件

main.py

myui/
  uimod.py
  uimod2.py

のような構成を考えます.
python 3.3 or later で, 階層とかはそんなに複雑ではないので, __init__.py は作らないとします.

main.py は普段通りです.

# main.py

import myui.uimod
# もしくは from myui import mymod

myui.uimod.myfunc()

情報

ありがとうございます.

問題. 相対インポート

モジュール側で相対インポートを使っていると,

# myui/uimod.py
from . import mymod2 # 同じモジュール(同じ階層)にある mymod2.py を使う


if __name__ == '__main__':
  testcode...

別ファイルからこの .py は import はできますが,
python uimod.py で直接起動する場合は相対 import はつかえないのでエラーになります :cry:

解決その 1

  • 相対は使わない
    • モジュール側で別モジュールを呼ぶときは import myui.uimod2 or from myui import ... のように記述する
  • メインファイル側(モジュールを呼び出す側)で必要に応じて sys.path.append でパスを通しておく

tests フォルダに, test code 作りたい場合, e.g.

main.py
tests/
  test_uimod.py
  test_uimod2.py
myui/
  uimod.py
  uimod2.py

の場合, テストコード .py では sys.path.append("..") でモジュールの基点(main.py のあるフォルダ)を追加して対応.

# tests/test_uimod.py
# 起動は `tests` に入り `python test_uimod.py` 

import sys
sys.path.append("..")
from myui import uimod

testcode...

https://note.nkmk.me/python-relative-import/ にありますように, sys.path.append(os.path.join(os.path.dirname(__file__), '..')) で, 絶対パス(__file__ 基準)にするとより確実でしょう

モジュール内で親階層側にある別モジュールを呼んでいる場合も同様で, メイン側(モジュールを呼び出す側)で各モジュールへのパスを通しておく.

myui/
  uimod.py
  uimod2.py
components/
  mycomp.py

とあって, myui.uimod.py から components/mycomp.py を呼んでいる場合は, モジュール側では特になにもしない.

# myui/uimod.py

# import sys
# sys.path.append("..") # components/ の親ディレクトリ

from components import mycomp

...

解決その 2

相対パスのよい点は, .py がローカルの手元で開発しているバージョンのを呼んでいるのが確実になることです
たとえば pip パッケージ化している場合, ローカルの開発版と pip で入っているものものが混在してかちあってしまうのを防げる可能性があります.

ちょっとコードはややこしくなりますが,
モジュール .py に一緒にテストコード記述したいばあい,

if __name__ == '__main__':
  sys.path.append("..")
  import myui.mymod2
else:
  from . import mymod2


if __name__ == '__main__':
  testcode...

か, __file__ 基準でより相対パス問題の少ない

if __name__ == '__main__':
  sys.path.append(os.path.join(os.path.dirname(__file__), '..')

from myui import mymod2

みたいにするのがよさそうでしょうか...

モジュール内で親階層側にある別モジュールを参照している場合は, この場合モジュール側に適宜 sys.path.append("..") あたりでパスを追加しておく必要があります.

# myui/uimod.py

if __name__ == '__main__':
  import sys
  sys.path.append("..") # or `components/` の親ディレクトリ

from components import mycomp

if __name__ == '__main__':
  sys.path.append("..")

  import myui.mymod2
else:
  from . import mymod2

if __name__ == '__main__':
  testcode...

ローカルのモジュールの探索を優先させる

もしくは, pip などで site-packages にインストールされているのと手元ローカル版がある場合に, 手元ローカル版を優先させる場合は PYTHONPATHを設定で対応がよいでしょう.
(メインで実行する .py がローカルモジュールの親ディレクトリに配置されていない場合)

ただ, .pyPYTHONPATH 環境変数を実行時指定

os.environ["PYTHONPATH"] = os.path.join(os.path.dirname(__file__), "..")` 

のようにしてもうまく探索されない(環境変数指定の場合は, python 起動時でないと反映されないぽい)です.

PYTHONPATH 環境変数設定して起動がめんどうで, .py で完結させたい場合, sys.path で探索がよいでしょう.

ただ, sys.path でのパス解決は昇順(リストの最初から)なので, append() で最後につけると, pip とかの site-packages パスが優先になってしまいます.

Python list には prepend がありませんので, insert(0, ...) あたりでリストの最初に追加で対応します!

sys.path.insert(0, os.path.join(os.path.basedir(__file__), ".."))
7
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
10