📝

パッケージを作ってimportするまで(Python)

に公開

はじめに

こんにちは!
Pythonを書いていると、当たり前のように import を使いますよね。
でもふと立ち止まってみると、

パッケージって結局なんだろう

import するとき、Pythonは何を見ているんだろう

自分で作ったものも、同じように扱えるのかな

と、意外と曖昧なまま使っている部分が多い気がします。
この記事では、
がっつり自分で作ったパッケージを公開する手順は記述していません。
LICENCEやREADME周りやAPIに関することには触れていないので、
この記事を見れば世に公開することができる!とはいきません...

Pythonのパッケージについて 一緒に足元から整理しながら、
最終的に「自分で作ったパッケージを import できるところ」まで辿り着くことを目指します。

難しい前提知識は置かず、
「そういう仕組みだったのか」と腑に落ちることを大切に進めていきます。

概要

  1. パッケージとは何か
  2. Pythonにおける import の考え方
  3. パッケージの最小構成
  4. 自分でパッケージを作ってみる
  5. 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つあります。

  1. importできるパッケージ本体がある
  2. パッケージ情報をまとめた設定ファイルがある
  3. 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は、

  1. このプロジェクトをビルドする
  2. パッケージを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