💡

Python でインポートがうまくいかないとき

2022/03/06に公開

Learning Python, 5th edition の Chapter 23-24 の話を、自分がよく困ってググるトピックごとに書いてみました

🔰 ディレクトリの中にあるファイルがインポートできない

たとえばこういう構成でプログラムを作っているとして、

my_app
  |- main.py
  |- utils/
     |- foo.py
     |- bar.py

このままだと main.py から utils/ の中の foo.pybar.py はインポートできない。これは、Python はインポートするときに search path の中から該当のモジュールファイルを探すけど、main.py で作業しているときに utils/ はこの search path にいないからインポートできない。

search path とは

search path は、import 文があったときに Python がインポートするファイルを探してくれるパス。逆に、import foo と書いたときに search path の中に foo.py がなければインポートは失敗する。

search path は、

  1. プログラムのホームディレクトリ
  2. 環境変数 PYTHONPATH
  3. standard library のディレクトリ
  4. .pth ファイル
  5. site-packages (i.e. pip install したときのインストール先)

の全部で、import sys; print(sys.path) を走らせると確認できる。

1のホームディレクトリは、Python のアプリを立ち上げている場合は一番最初に実行されるファイル (e.g. main.py, app.py) で、ターミナルから pythonを打って起動している場合は今いるディレクトリ (i.e. pwd)になる。とりあえずflask runした場合とpython` した場合でインポートの結果が変わることがあるのを覚えておけば大丈夫。

my_app の例に戻ると、2か4の方法でパスの設定を変えていない場合、my_app/utils/search path に存在しない。なので、import utils.foo とか from utils import foo と書いても、Python が utils.py を見つけられないのでエラーになる。

my_app
  |- main.py
  |- utils/
     |- foo.py <-- 見つからない
     |- bar.py <-- 見つからない

逆に、my_app/ 直下にあるファイルは search path にあるので見つけられる。

my_app
  |- main.py
  |- hoge.py
  |- utils/
     |- foo.py 
     |- bar.py
# my_app/main.py

import hoge # OK
import utils.foo # error
from utils import foo # error
import bar # error

↑ のどの書き方でも、utils foo bar のインポートは失敗する。

ここでうっかり成功する場合は、

  • standard library や 3rd party library (pip install したやつ) に同じ名前のファイルがないか
  • PYTHONPATH 環境変数をいじってないか
  • .pth ファイルがないか

を確認してみてください。

じゃあどうするか

どうすればいいかというと、utils/をパッケージにする。パッケージとは、__init__.py という特別なファイルがある Python プログラムのディレクトリのことで、(ディレクトリ + __init__.py = パッケージ) utils/ をパッケージにするには utils/ の中に __init__.py という名前の空のファイルを作れば ok。

touch utils/__init__.py # 空のファイルを作成
my_app
  |- main.py
  |- utils/
     |- __init__.py
     |- foo.py 
     |- bar.py

ちなみに __init__.py は、そのパッケージが初めてインポートされたときに自動で実行されるファイル。何かしらの初期化処理が書かれることもあるけど、実際は空のことが多い。

こうすると、Python は utils/ をパッケージとして認識してくれて、かつこのパッケージが search path の中にいるので (1のプログラムのホームディレクトリに該当する) インポートできる。

# どの書き方でも ok
import foo
import foo.my_func  # foo の中に def my_func(): している前提
from utils import foo
from utils.foo import my_func

パッケージの中から同じパッケージの別のファイルをインポートしたい

__init__.py のあるディレクトリ (=パッケージ) の中で、同じパッケージある別のファイルをインポートするとき、こういう書き方だとうまくいきません

# my_app/utils/foo.py

import bar
my_app
  |- main.py
  |- utils/
     |- __init__.py
     |- foo.py 
     |- bar.py

Python 3系では、パッケージ内のファイルで import bar と書いた場合パッケージの中のディレクトリは探索されません。パッケージの中の通常のインポート文では、モジュール探索パスのみを探すので my_app/**.py はインポートできますが、 my_app/utils/**.py はインポートできません。

背景として、Python 2系は utils/foo.pyimport foo と書けば、utils/bar.py がインポートできていました。これは、2系はパッケージ内のインポートを「相対→絶対インポート」でしていたためです。相対インポートで、まず同じパッケージの中の bar.py を探した後に、なければ sys.path にある探索パスを探しに行きます(絶対インポート)。この方法の問題として、utils/foo.py から**bar というライブラリ**をインポートしたいとき、import barと書いてもまずutils/bar.pyがインポートされ、外部ライブラリのbar` がインポートできません。コードを書いている時点で存在するライブラリについては同じ名前を使わないことで避けられますが、将来追加されるライブラリに対しては安全ではないです。そこで、Python 3系では「パッケージ内のインポートは相対インポートのみ」となりました。

相対インポートとは

上の例で import bar を相対インポートに書き直すと

from . import bar
from .bar import some_func # some_func が bar.py で定義されている前提

の2つの方法がとれます。

インポート文のモジュール/パッケージの名前の前に1つ以上の . がつくと、Python が「これは相対インポートだな」と認識してくれます。相対インポートは、現在のファイルが含まれるディレクトリの中を探索します。

from bar import some_func # 絶対インポート
from .bar import some_func # 相対インポート

注意点として、相対インポートを使う場合 from が必須で、import だけだと動きません。

import .bar # これはだめ

じゃあどうするか

以上をまとめると、パッケージの中から同じパッケージの別のファイルをインポートしたい場合、

my_app
  |- main.py
  |- utils/
     |- __init__.py
     |- foo.py <-- bar をインポートしたい
     |- bar.py

相対インポートを使うとできます。

# my_app/utils/foo.py

# 書き方 1
from . import bar

# 書き方 2
from .bar import some_func, another_func, some_const

Discussion

ログインするとコメントできます