パッケージを作ってimportするまで(Python)
はじめに
こんにちは!
Pythonを書いていると、当たり前のように import を使いますよね。
でもふと立ち止まってみると、
パッケージって結局なんだろう
import するとき、Pythonは何を見ているんだろう
自分で作ったものも、同じように扱えるのかな
と、意外と曖昧なまま使っている部分が多い気がします。
この記事では、
がっつり自分で作ったパッケージを公開する手順は記述していません。
LICENCEやREADME周りやAPIに関することには触れていないので、
この記事を見れば世に公開することができる!とはいきません...
Pythonのパッケージについて 一緒に足元から整理しながら、
最終的に「自分で作ったパッケージを import できるところ」まで辿り着くことを目指します。
難しい前提知識は置かず、
「そういう仕組みだったのか」と腑に落ちることを大切に進めていきます。
概要
- パッケージとは何か
- Pythonにおける import の考え方
- パッケージの最小構成
- 自分でパッケージを作ってみる
- pipで扱えるようにする
1.パッケージとは
1.1 モジュールについて
Pythonには「モジュール」という機能があります。
モジュールはシンプルで、
1つの.pyファイル = 1つのモジュール です。
例えば、math.pyがあれば、
import math
と書いて、その中の関数や定義を使えます。
1.2 パッケージは何をまとめたものか
では「パッケージ」は何かというと、
複数のモジュールをまとめたもの です。
言い換えると、
- モジュールはファイル
- パッケージはディレクトリ
という関係ですね!
プログラムが大きくなると、
- ファイルが増えてきた
- 機能ごとに分けたくなった
- 名前の衝突を避けたい
と感じる場面が出てきます
そんなときに、
「このまとまりはここまで」と整理するために
パッケージという単位が使われます
2.Pythonにおけるimportの考え方
2.1 importは何をしているのか
importは何か特別な処理をしているように見えますが、
実はやっていることはとてもシンプルです
Pythonはimport文を見ると、
- その名前のファイルやディレクトリが
- 決められた場所のどこかに存在するか
を順番に探しています。
import sys
print(sys.path)
とすると、
- 標準ライブラリの場所
- site-packages
- 仮想環境のパス
などが並んでいるのがわかります
importできるかどうかは、最終的には sys.path 上に存在するかどうか ただそれだけです。
2.2 パッケージとして認識される条件
Pythonが「これはパッケージだな」と判断するための最低条件は、
- ディレクトリであること
- その中に**init.py**があること
(※ Python 3.3以降には例外もありますが、まずはこの理解で問題ありません)
です。
init.pyの中身は、空でも構いません。
存在していること自体が意味を持ちます。
3.パッケージの最小構成
最小のパッケージ構成は、
my_package/
└─ my_package/
├─ __init__.py
└─ sample.py
この状態で一つ上の階層からPythonを起動すると、importできます
4.パッケージを自分で作ってみる
ここからは、簡単な例で実際に作ってみます。
リストの中に、ある値がいくつ含まれているかを数える
そんな小さな関数をパッケージにしてみます。
4.1 構成
mypackage/
├─ mypackage/
│ ├─ __init__.py
│ └─ count.py
4.2 中身を書く
count.py
def count_in_list(lst, value):
count = 0
for elem in lst:
if elem == value:
count += 1
return count
4.3 init.pyの役割
init.pyに1文加えます、
from .count import count_in_list
init.pyにこの文を付け足すことで、
from mypackage import count_in_list
という書き方ができるようになります
この1文がなければPythonが知っているのは、
- mypackageというパッケージがある
- その中に何があるかは、まだ教えられていない
という状態です。
結果としてinit.pyに何も書かない場合、
from mypackage.count import count_in_list
と書く必要があります。
この場合、パッケージ側のcount.pyの場所を
utils/count.py
に移動した際、count.pyをimportしているすべてのファイルのimport文を書き直す必要があります。
非効率ですね!
4.4番外編
書いていて思ったのですが、
mypackage/ <-ここ
├─ mypackage/ <-ここ
│ ├─ __init__.py
│ └─ count.py
<-ここ同じ名前ですよね
なぜなのだろう?と思いませんか?
思わなければそういうものなのだ〜と読み飛ばしてもらって構いません
結論
1つ目の名前は、
pip install <mypackage>
2つ目の名前は、
import <mypackage>
を表しています。
統一するために同一の名前にしているのですね!
5. pipで扱えるようにする
ここからが本題ですね。
ここまでは下準備でした。
5.1 全体像を掴む
pip からインストール可能な状態にするというのは要するに、
Pythonに対して
このフォルダは配布可能なパッケージです
という情報をきちんと渡すことです。
そのために必要なことが3つあります。
- importできるパッケージ本体がある
- パッケージ情報をまとめた設定ファイルがある
- pipがそれをビルドできる形になっている
それぞれ解説していきます。
5.2 importできるパッケージ本体を用意する
これは今まで話してきた部分ですね。
大事なのは、
- ディレクトリであること
- init.pyが存在すること
です。
5.3 pipに教えるための設定ファイルを用意する
次に必要なのが、
このプロジェクトがどういうパッケージなのか
をpipに伝えるためのファイルです。
今の主流は pyproject.toml です。
プロジェクトルートに、こういうファイルを置きます。
ft_package/
├─ ft_package/
│ ├─ __init__.py
│ └─ count.py
├─ pyproject.toml
pyproject.tomlには、
- パッケージ名
- バージョン
- 誰が作ったか
- どうやってビルドするか
などを書きます。
最低限の例は以下の感じです。
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
version = "0.0.1"
これでpipは、
プロジェクトはmypackageという名前なんだな
と理解できるようになります。
5.4 pip installが何をしているか
ここで、pip installについて触れておきたいと思います。
pip installは、
- このプロジェクトをビルドする
- パッケージをsite-packagesにコピーする
という作業をしています。
pip install .
このコマンドは、
今いるディレクトリをパッケージとしてインストールする
という意味になります。
5.5 ローカルで pip install ができるようにする
準備が整ったら、いよいよ実際に pip install をしてみます。
プロジェクトのルートディレクトリで、次のコマンドを実行します。
pip install .
. は「今いるディレクトリ」を指しています。
このコマンドによって pip は、
- このディレクトリを pyproject.toml を元にビルドし
- 生成されたパッケージを site-packages に配置します
結果として、
- pip list に mypackage が表示される
- どのディレクトリからでも import mypackage ができる
状態になります。
この時点で、mypackage は
ローカル環境における正式な Python パッケージ
になっています。
6. dist にできるファイルの正体
次に、次のコマンドを実行してみます。
python -m build
すると、プロジェクト内に dist というディレクトリが作られます。
中には、次のようなファイルが生成されます。
dist/
├─ mypackage-0.0.1.tar.gz
└─ mypackage-0.0.1-py3-none-any.whl
これらは 配布用パッケージ と呼ばれるものです。
実は、
pip install pandas
pip install fastapi
を実行したとき、pip が裏でダウンロードしているのも
この tar.gz や whl と同じ形式のファイルです。
つまり dist に生成されたファイルは、
pip が本来インストールしたい
完成形のパッケージ
だと言えます。
7. pip install ファイル名 ができる意味
dist に配布物ができたら、次のような書き方もできます。
pip install dist/mypackage-0.0.1.whl
これは、
pip install pandas
と 仕組みとしてはまったく同じ です。
違うのは、
- pandas はリモートから取得している
- mypackage は手元にある
という点だけです。
この時点で、
- pip install .
- pip install dist/xxx.whl
の両方ができるということは、
このパッケージは
pip が期待する形式を満たしている
という確認にもなっています。
8. PyPI はこの流れのどこにあるのか
ここで、PyPI の話につながります。
pip install mypackage と書いたとき、
pip はまず PyPI という公開サーバを見に行きます。
そこに、
- mypackage という名前で
- 配布可能なファイル(tar.gz や whl)が
置かれていれば、pip はそれをダウンロードして
インストールします。
逆に言えば、
どれだけ正しくパッケージを作っても
リモートに置かれていなければ
pip install mypackage は成立しません。
9. PyPI に置くまでの流れ
ここまでの流れを一度まとめます。
パッケージを書く
↓
pip install . ができる
↓
python -m build
↓
dist に配布物ができる
↓
PyPI にアップロードする
↓
pip install mypackage が成立する
この中で、
PyPI にアップロードする役割を担当するのが twine です。
10. twine を使って PyPI にアップロードする
まず PyPI にアカウントを作成し、
API トークンを発行します。
次に、twine をインストールします。
pip install twine
そして、次のコマンドを実行します。
twine upload dist/*
これによって、
- dist 内の tar.gz / whl が
- PyPI に登録され
- mypackage という名前で公開されます
11. pip install mypackage が成立する瞬間
ここまで来て、ようやく
pip install mypackage
が意味を持つようになります。
pip はこのコマンドを見ると、
- PyPI に mypackage があるか探し
- 対応するバージョンを選び
- ダウンロードして
- site-packages に配置します
これは、
pip install pandas
pip install fastapi
と まったく同じ仕組み です。
おわりに
ここまで見てきたように、
- import はファイルを探す仕組み
- pip は配置する仕組み
- dist は完成形
- PyPI は置き場所
という関係で成り立っています。
pip install mypackage は、
これらがすべて正しくつながった結果として
初めて成立します。
一度この流れを理解してしまえば、
- import エラー
- pip の挙動
- パッケージ構成
に対して、落ち着いて向き合えるようになります。
一緒にここまで辿ってきた整理が、
その助けになれば嬉しいです。
Discussion