メモの日々


2024年09月12日(木) [長年日記]

[python] Pythonのモジュールとパッケージ

Pythonの「モジュール」と「パッケージ」という用語の使い分けがよくわからなくなる。

pipでインストールしているものはモジュールなのかパッケージなのか?importしているものはモジュールなのかパッケージなのか?

定義

Pythonの用語集には次のように書かれている。

(モジュール) Python コードの組織単位としてはたらくオブジェクトです。モジュールは任意の Python オブジェクトを含む名前空間を持ちます。モジュールは importing の処理によって Python に読み込まれます。

パッケージ を参照してください。

(パッケージ) サブモジュールや再帰的にサブパッケージを含むことの出来る module のことです。専門的には、パッケージは __path__ 属性を持つ Python オブジェクトです。

regular package と namespace package を参照してください。

パッケージの説明に「__path__ 属性を持つ Python オブジェクトです」とあるけど、英語版には「a package is a Python module with a __path__ attribute.」とある。つまり、

  • __path__ 属性を持つモジュールのことを特別にパッケージと呼ぶ

と考えればよさそう。では、__path__ 属性とは何か?

__path__ 属性

__path__ 属性については次に説明があった。

パッケージの __path__ 属性は、そのサブパッケージのインポート中に使われます。インポート機構の内部では、それは sys.path とほとんど同じように機能します。つまり、インポート中にモジュールを探す場所のリストを提供します。しかし、一般的に __path__ は sys.path よりも制約が強いです。

この説明からわかるのは、__path__ 属性はモジュールのインポート処理に使うためのものだということ。

__path__ 属性の有無を調べてみる。

% python
Python 3.12.6 (main, Sep 12 2024, 17:06:52) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os, json
>>> hasattr(os, "__path__")
False
>>> hasattr(json, "__path__")
True
>>>

osモジュールはパッケージではないが、jsonモジュールはパッケージであることがわかる。

配布パッケージとインポートパッケージ

上述したことから、importしているものはモジュールだということになるだろう。

一方、pipでインストールするものはパッケージと呼ばれている気がする。

どうやら、Pythonのパッケージという用語には二種類あると考えるべきなようで、Python Packaging User Guideの次のページで説明がなされていた。

これを読むと、先に調べたPythonのパッケージは詳しくは「インポートパッケージ」であり、pipでインストールするものは「配布パッケージ」であると考えるようだ。

パッケージという言葉はあいまいなので、使い方に注意が必要そう。次の言明は正しいと言えるだろうか。

「NumPyという配布パッケージをインストールすると、numpyというモジュールをインポートできるようになる」


2024年09月24日(火) [長年日記]

[dev] git submodule statusで表示される3番目の項目の内容

submoduleを使用しているGitリポジトリにおいて「git submodule」を実行すると次のように表示される。

 a2e59f0e7065404b44dfe92a28aca47ba1378dc4 submodule/pybind11 (v2.11.0-182-ga2e59f0e)

このサブモジュールはpybind11のタグv2.13.6を指すようにしているのだが、括弧内にはv2.11.0と表示されていてモヤモヤする。

この最後の括弧内に表示される内容が何なのかについて調べたのでメモ。手元のGitのバージョンは2.44.0。

括弧内に表示される内容

「git submodule」で何が表示されるのかについては、git submoduleのマニュアル

With no arguments, shows the status of existing submodules.

とある。「status」の説明は無いが、「git submodule status」を実行したときと同じ内容が出力されるという理解でいいのだと思う。

同マニュアルのstatusサブコマンドの説明には次のようにある。

Show the status of the submodules. This will print the SHA-1 of the currently checked out commit for each submodule, along with the submodule path and the output of git describe for the SHA-1.

出力の括弧内には「git describe」の出力結果が表示されるということのようだ。

実際、submodule/pybind11 にて「git describe」を実行すると「v2.11.0-182-ga2e59f0e」と表示された。

git describeの出力

git describeは何を表示しているのか。

git describeのマニュアルには次のようにあった。

The command finds the most recent tag that is reachable from a commit. If the tag points to the commit, then only the tag is shown. Otherwise, it suffixes the tag name with the number of additional commits on top of the tagged object and the abbreviated object name of the most recent commit.

A-B-C という形式の出力で次のようになると考えてよさそう。

  • Aの部分は、カレントのコミットから過去に辿って見つかる最初のタグの名前
  • Bの部分は、Aのタグからカレントのコミットまでにあるコミットの数
  • Cの部分は、カレントのコミットの名前の先頭部分

でもおかしい。手元のサブモジュールはタグv2.13.6を指しているのにAの部分がv2.11.0になっている。これは何故か。

マニュアルの続きに次のようにあった。

By default (without --all or --tags) git describe only shows annotated tags.

Gitのタグにはlightweightとannotatedの2種類があり、オプションなしのgit describeがAの部分に出力するのはannotatedなタグだけのようである。

なので、pybind11のタグv2.13.6はlightweightタグであり、それより古い最新のannotatedタグがv2.11.0だからあのような出力になっているということになる。

タグの種類の確認方法

タグがlightweightなのかannotatedなのかを調べる明快な方法はわからなかったが、git cat-fileの -t オプションを使うのがいいように思う。

このコマンドは指定したオブジェクトのタイプを出力するが、lightweightタグのタイプはcommit、annotatedタグのタイプはtagになるのでこれで区別できる。

$ git cat-file -t v2.13.6
commit

$ git cat-file -t v2.11.0
tag

また、git for-each-refを使うと全てのタグについてタイプを確認できる。

$ git for-each-ref refs/tags
(省略)
8d8aecf4a5579c0e51d07fb93411aa120ae0360c tag    refs/tags/v2.11.0
0630807c3070287c716f6be3eacb00b8816b4215 tag    refs/tags/v2.11.1
95d943ae0ebdf609bbd650d119fda539509929b6 commit refs/tags/v2.11.2
3e9dfa2866941655c56877882565e7577de6fc7b commit refs/tags/v2.12.0
2e0815278cb899b20870a67ca8205996ef47e70f commit refs/tags/v2.12.1
0c69e1eb2177fa8f8580632c7b1f97fdb606ce8f commit refs/tags/v2.13.0
941f45bcb51457884fa1afd6e24a67377d70f75c commit refs/tags/v2.13.1
07f30430d4186c2712761f1ffaea50ede63f2b2b commit refs/tags/v2.13.2
bd67643652d3800837f1f41549a2a5adbaa3fafe commit refs/tags/v2.13.3
c6239a8a1b6871cc0fb5f7af885a02ffd1349f9d commit refs/tags/v2.13.4
7c33cdc2d39c7b99a122579f53bc94c8eb3332ff commit refs/tags/v2.13.5
a2e59f0e7065404b44dfe92a28aca47ba1378dc4 commit refs/tags/v2.13.6
(省略)

これを見ると、v2.11.1もannotatedタグなのでおかしいなと思ったが、どうやらタグv2.11.1はタグv2.13.6の先祖には位置していないようだった。どうしてそうなっているのかはわからないが。


2024年09月26日(木) [長年日記]

[python] Pythonにおけるパッケージング(2)

7年前にPython 2でパッケージ化する方法をメモしていた。少し前にPython 3でパッケージを作る方法を調べたので、忘れないようにメモしておく。

情報源が https://packaging.python.org/ であることは変わっていない。日本語版もある。

pyproject.tomlを用意

現代ではsetup.pyの代わりにpyproject.tomlを使う。ここに次のような内容を設定する。

[project]
name = "oreore"
version = "0.1.0"

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[build-system] に指定可能なツールが色々あって悩みどころだが、本記事ではSetuptoolsを使う。

Pythonのコードを用意

以前と同じ構成のコードを用意する。ただし、コードは「src/パッケージ名」というディレクトリの下に置くのが標準的なようなのでそのようにする(参考:src レイアウト対フラットレイアウト)。

$ tree . --noreport
.
├── pyproject.toml
└── src
    └── oreore
        ├── hello
        │   ├── __init__.py
        │   └── hello.py
        └── world
            ├── __init__.py
            └── world.py

前回同様hello.pyを実行可能スクリプトと認識させたかったが、pyproject.tomlでそのような指定をする方法は無さそうだった。代わりに、hello.pyの関数を呼び出す実行可能スクリプトを自動生成させることはできて、そのためにはpyproject.tomlに次のような設定を追加すればよい(参考:Entry Points)。

[project.scripts]
hellohello = "oreore.hello.hello:hello"

これにより、oreore/hello/hello.py の関数hello()を呼び出すスクリプト hellohello が、oreoreパッケージのインストール時に作られるようになる。

Wheelファイルを作る

以上の準備により、Wheelファイルを作成できるようになる。

Wheelファイルを作成するにはbuildを使うことが標準的なようだ。ただし、別途インストールが必要。pipにもWheelの作成機能があるので、ここではbuildではなくpipを使う。

$ python -m pip wheel --no-deps .

これにより、カレントディレクトリに oreore-0.1.0-py3-none-any.whl が作られる。

pip wheelの注意点として、buildというディレクトリが作られその中にビルド時のソースコードが残るという点がある。これが次のpip wheel実行時にも参照されるので、例えばhello.pyをhello2.pyに改名してpip wheelを実行し直すとWheelファイル内に(本来は含まれるべきでない)hello.pyが含まれてしまう(pipのバージョン24.2で確認)。

なので、pip wheelの実行前にはbuildディレクトリは削除した方がよさそう。buildにはこの問題はなさそうだったので、pip wheelではなく素直にbuildを使う方がいいのかもしれない(この段落はディレクトリ名のbuildとツール名のbuildが混在していて分かりにくい…)。


2024年09月27日(金) [長年日記]

[python] 作成するWheelファイルのファイル名

昨日の例で作られるWheelファイルのファイル名は

  • oreore-0.1.0-py3-none-any.whl

になる。これの「py3-none-any」の部分はPlatform compatibility tagsと呼ばれ、

  • python tag
  • abi tag
  • platform tag

を並べたものになっている。

Wheelが特定のPythonバージョンや特定のプラットフォームを要求する場合、これらのタグを適切に指定する必要があるが、作成するファイル名を制御するにはどうすればいいのか?

方法1: buildの--config--settingオプションを使用する

昨日の例ではWheelの作成にpipを使用したが、pipでファイル名を制御する方法はわからなかった。buildを使えばある程度制御できて、昨日の環境で次のコマンド

$ python -m build . --wheel \
  --config-setting="--build-option=--python-tag hello --plat-name world"

を実行すると

  • oreore-0.1.0-hello-none-world.whl

というファイル名のWheelが作られる。

abi tagを指定する方法は分からなかった。また、確認したビルドバックエンドはSetuptoolsだけで、別のバックエンドを使う場合は別の指定方法になると思われる。

方法2: wheel tagsコマンドでファイル名を変更する

wheelというパッケージがあり、これをインストールするとwheel tagsというコマンドを使えるようになって、これでWheelのファイル名の変更が簡単にできる。

$ python -m wheel tags oreore-0.1.0-py3-none-any.whl \
  --python-tag=hello --platform-tag=world --abi-tag=abc

上のコマンドを実行すると次のファイル名のWheelが作られる。abi tagも指定できる。

  • oreore-0.1.0-hello-abc-world.whl

適切なタグ名を取得する

各タグの名前を取得するのには、packagingというパッケージが使える。

例えば、動作しているPython環境に一番適合したタグ名は packaging.tags.sys_tags() により取得できる。

>>> import packaging.tags
>>> print(next(packaging.tags.sys_tags()))
cp312-cp312-manylinux_2_35_x86_64
>>>