ちょっと複雑なプログラムを書くときに躓くインポートとかシステムパス、ディレクトリの話。
pythonの基本がわかってきて、複数の.pyファイル間であれこれするプログラムを作ったり、ファイルの読み込み書き出しをしたりするときに躓いたので、わかったその勢いでメモ。
環境はVScode、Python3.12
Pythonファイル構造の理解
Pythonで何かを作るときは、ファイルの構造がこのようになるようにしましょう
myproject/
│
├── src/
│ ├── __init__.py
│ ├── source.py
│ └── tools/
│ ├── __init__.py
│ └── tool.py
│
└── data/
└── model.txt
source.pyがメインで動かすプログラム、tool.pyが、メインのプログラムで使う関数などを定義している。というイメージ。
カレントディレクトリの概念と重要性
カレントディレクトリは、Pythonスクリプトが実行されているディレクトリ
です。
source.pyを実行するときは、src/がカレントディレクトリになります。
カレントディレクトリは、相対パスの解釈に影響を与え、ファイル操作の基準点となります。
ファイルの操作というのは、これから説明するインポートと、ファイルへのアクセスです。
モジュールインポートの方法
もう一度さっきの例。
myproject/
│
├── src/
│ ├── __init__.py
│ ├── source.py
│ └── tools/
│ ├── __init__.py
│ └── tool.py
│
└── data/
└── model.txt
source.pyからtool.py内のreadinfo関数をインポートする場合を考えます。
これには相対インポートまたは絶対インポートを使用できます。
相対インポートでは
from .tools import tool
と記述します。
ここでの " . "は「現在のディレクトリ」、つまり source.py がある src ディレクトリを意味します。したがって、.tools は src 内の tools ディレクトリを指し、そこから tool モジュールをインポートします。
絶対インポートでは
# 絶対インポートを使用する例
import sys
import os
# tools モジュールをインポートするために必要なパスを追加
sys.path.append(os.path.join(os.path.dirname(__file__), 'myproject'))
from src.tools import tool
と書きます。(非推奨)
myproject をPythonのパスに追加し、追加したディレクトリの中にあるディレクトリから . でつなげてインポートします。
システムパスを設定するのはあまりよくないらしいです。
相対インポートと絶対インポートの違い
相対インポート(さっきの例:from .tools import tool)は現在のディレクトリに基づきますが、
絶対インポート(例:from src.tools import tool)は全体のインポートパスに基づきます。これは、事前に
sys.path.append(os.path.join(os.path.dirname(__file__), 'myproject'))
で設定しないといけないのでよくないです。
init.pyファイルの役割と配置
相対インポートをするにも準備があります。さっきの例をもういちど
myproject/
│
├── src/
│ ├── __init__.py
│ ├── source.py
│ └── tools/
│ ├── __init__.py
│ └── tool.py
│
└── data/
└── model.txt
srcディレクトリとそのサブディレクトリtoolsに__init__.pyファイルを配置することで、これらのディレクトリがパッケージとして認識され、相対インポートが可能になります。
__init__.py ファイルはパッケージとして扱いたいディレクトリのルート(それが入ってるディレクトリ)に配置します。tool.pyを使いたいだけなら、toolsに入れます。
その他 __init__.py について知っておくべきことメモ
役割の説明編:
パッケージ識別: このファイルが存在するディレクトリは Python パッケージとして識別されます。Python 3.3 以降では、名前空間パッケージと呼ばれる __init__.py ファイルが不要なパッケージを作成することもできますが、互換性や明示的な構造のために __init__.py を使用することが一般的です。
パッケージの初期化: __init__.py はパッケージが最初にインポートされる際に実行されるため、パッケージレベルで必要な初期化コードをここに配置できます。
名前空間の管理: このファイルを使用して、パッケージ内で利用可能なモジュールや変数、関数などを制御できます。
注意点編:
空の __init__.py は、単にディレクトリがパッケージであることを示すためだけに存在します。しかし、必要に応じて初期化コードやパッケージのメタデータを含めることもできます。
Python 3.3 以降では、暗黙的な名前空間パッケージが導入されたため、__init__.py ファイルは常に必要というわけではありませんが、従来のパッケージ構造を使用する方が一般的で互換性があります。
sys.path ことシステムパスについて
これを絶対インポートで使ってた。コードの中で変更するのはあまりよくないし、sys.path の変更は、その変更を行った Python プロセス内でのみ有効なので注意。プロセスが終了すると、変更は失われる。
システムパスはこんなふうに設定される:
Pythonが起動するとき、sys.path は以下の場所を基に自動的に設定される。
カレントディレクトリ: Python スクリプトを実行したディレクトリ(あるいはPythonインタープリタを起動したディレクトリ)が最初にリストに追加されます。これにより、スクリプトのあるディレクトリに配置されたモジュールやパッケージを直接インポートできます。
PYTHONPATH 環境変数: PYTHONPATH は環境変数で、Pythonモジュールが格納されている追加のディレクトリを指定できます。この変数に設定されたディレクトリが sys.path に追加されます。
標準ライブラリディレクトリ: Pythonのインストール時に設定された標準ライブラリが格納されているディレクトリが含まれます。
サイトパッケージディレクトリ: サードパーティのパッケージやモジュールがインストールされるディレクトリです。例えば、pip を使ってインストールされたパッケージは通常、ここに配置されます。
4番はつまり、仮想環境のこと。
Pythonの仮想環境とサイトパッケージディレクトリ
仮想環境内でpipを使ってインストールしたパッケージは、その仮想環境のサイトパッケージディレクトリに配置されます。これにより、システム全体のPython環境とは独立した環境が確保されます。
大事なのは、仮想環境に入ってるものは、相対インポートとか考えなくても、インポートできるという事。import numpyとか。
なぜなら、すでにシステムパスが仮想環境にインストールしたパッケージに設定されてるから。
さっきも書いたけど、4番の仮想環境のサイトパッケージディレクトリ
とは、仮想環境を作ってアクティベートしていたら、この仮想環境のことを指す。 その他の説明は以下
仮想環境をアクティベートすると、その環境専用のPythonインタープリタが使用されます。
pip でパッケージをインストールすると、それらは仮想環境のサイトパッケージディレクトリにインストールされます。これにより、システム全体のPython環境とは別に、その環境でのみ使用されるパッケージセットを持つことができます。
このディレクトリは、通常仮想環境内の lib/pythonX.X/site-packages に位置します(X.X はPythonのバージョンを表します)。
仮想環境のアクティベーションとディレクトリパス
仮想環境をアクティベートするときにも、実はこの話は少し出てくる。
myproject/
│
├──venv
├── src/
│ ├── __init__.py
│ ├── source.py
│ └── tools/
│ ├── __init__.py
│ └── tool.py
│
└── data/
└── model.txt
myprojectディレクトリ内で.\venv\Scripts\activateを実行することで、仮想環境がアクティベートされる。この操作は、プロジェクトのルートディレクトリ(ここではmyproject)から行う必要がある。
これまでの話を踏まえて考えてみるとわかるかも。
.が現在のディレクトリを指すので、その中のvenvを起動するためには、myprojectからやらないといけないって話。
ファイルの読み書き
これもインポートと同じ考え。
projects\
myproject\
src\
source.py
tools\
tool.py
data\
result.txt
data\
model.txt
model.txt と result.txt を開く:
model.txt は myproject/data ディレクトリにあり、source.py は myproject/src ディレクトリにあります。したがって、model.txt にアクセスするには、../data/model.txt のようにパスを指定します。
result.txt は source.py と同じディレクトリにある data ディレクトリ内にあります。したがって、data/result.txt のようにパスを指定します。
source.pyはこんなかんじ
# source.py
import sys
import os
# tools モジュールをインポートするために必要なパスを追加
sys.path.append(os.path.join(os.path.dirname(__file__), 'tools'))
from tool import readinfo # tool.py から readinfo 関数をインポート
# model.txt を読み込む
with open("../data/model.txt", "r") as model_file:
model_content = model_file.read()
# result.txt を読み込む
with open("data/result.txt", "r") as result_file:
result_content = result_file.read()
とりあえずこれで終わり。下には、自分が詰まった時のメモしとく。
もう少し複雑な場合用メモ:
tool.py の readinfo 関数で model.txt と result.txt を開き、何らかの操作を行うには、ファイルのパスを関数に渡すか、関数内でパスを正しく指定する必要があります。以下の手順で実装できます。
まず、tool.py 内の readinfo 関数を考えます。この関数は2つのファイルのパスを引数として受け取り、それぞれのファイルを開いて処理を行います。
tool.py:
def readinfo(model_path, result_path):
# model.txt を読み込む
with open(model_path, "r") as model_file:
model_content = model_file.read()
# ここで model_content に対する何らかの処理を行う
# result.txt を読み込む
with open(result_path, "r") as result_file:
result_content = result_file.read()
# ここで result_content に対する何らかの処理を行う
# 必要に応じて何らかのデータを返す
source.py:
import sys
import os
# tools モジュールをインポートするために必要なパスを追加
sys.path.append(os.path.join(os.path.dirname(__file__), 'tools'))
from tool import readinfo # tool.py から readinfo 関数をインポート
# 正しいファイルパスを指定
model_path = "../data/model.txt"
result_path = "data/result.txt"
# readinfo 関数を呼び出し
readinfo(model_path, result_path)
まとめとベストプラクティス
Pythonプロジェクトの管理においては、ファイル構造の明確化、適切なインポート方法の選択、カレントディレクトリの理解が重要。また、仮想環境の利用により、プロジェクトごとの依存関係を維持し、開発環境の整合性を保つことができる。
むやみにシステムパスを変更せず、するとしても小規模なコードや一時的なものにとどめておこう。
この記事が気に入ったらサポートをしてみませんか?