Pythonのパッケージを作る際、通常pyproject.tomlやsetup.pyにバージョンを記載しますが、 pythonのコード内でもそのバージョンを取得する方法があります。
ただ、読み込むためのライブラリがちょっと重いので 必要なときだけ読み込むような工夫も入れてみます。
よくあるバージョン情報記述
パッケージルートディレクトリの__init__.pyに
1 2 3 | |
のように書いておくとこれがmy_packageというパッケージだった場合、
1 2 3 | |
とか、my_package.__version__でバージョンを取得できます。
ただ、__init__.pyに直接書いてしまうと パッケージ内の他のモジュールから参照するのに問題が起こる時があります。
例えばmain.pyの中で__version__を参照したいが、
__init__.pyの方でmain.pyの中で定義されている関数などをimportしていると
循環参照になってしまい、エラーが発生します。
なのでversion情報を別に分けて
1
| |
1 2 3 4 | |
1 2 3 4 5 6 7 8 9 | |
みたいな感じにすれば外からは最初のと同様に、 パッケージ内からも参照できるようになります。
これでコードの中だけにおいてはバージョンを統一的に扱うことが出来ますが、
管理ツール側でversionを定義している場合、
そちらとの値を同期させる必要があり面倒です。
importlib.metadata.version
uvで管理しているプロジェクトでpyproject.tomlにproject.versionが定義してあるような場合、
そのパッケージ内で
1 2 3 | |
とすればproject.versionの値が取得できます。
基本的には他の管理ツールでも同様にversionの値が取得できます。
これをversion.pyの中に
1 2 3 | |
のように書いておけば、上の例のままmy_package.__version__でバージョンが取れますし、
管理ツール側でversionを更新すればそのまま反映されます。
通常はこれでも十分。
__getattr__を使った遅延読み込み
ただ、importlib.metadataをimportするのにちょっと時間がかかるので
1
必要なときだけ読み込むようにしたい場合があります。
とは言っても今の普通の環境で0.1秒かからない程度なので通常は無理にやる必要もないかもですが、
例えばコマンドラインツールを作っていて、my_package --helpのようにヘルプを表示したいだけの場合、
0.1秒でもちょっと気になることがあります。
そのような場合は、version.pyはそのままで、 __init__.pyの方で__getattr__を使ったlazy loadingな実装をしてみます。
1 2 3 4 5 6 7 8 9 10 11 12 | |
のように、__version__が参照されたときだけ
version.pyをimportするようにします。
ただ、このままだと、__init__.pyが呼ばれたとき、main.pyの方も呼ばれて 結果その中でversion.pyがimportされてしまいます。
なので、
1 2 3 4 5 6 7 8 | |
のような感じでこちらもimportを関数の中に隠す必要があります。
実際に
importlib.metadataのimportで気になるような場合は
他の外部ライブラリの読み込みもすべて必要なときだけにしたい場合が多いので、
その場合は時間がかかるものをすべて関数内など必要な部分に隠すことになるかと思います。
ただモジュール自体が呼ばれなければ良いので、 例えばnumpyとか大量に使うモジュールでトップに書いておきたい場合などは そのモジュールをimportする側で関数内に隠すなどの工夫をすることもありだと思います。
__getattr__を使う際の注意
__getattr__を使う際の注意として、
__getattr__での指定はファイルでのモジュール名の指定よりも優先度が低くなる点。
どういうことかというと、例えば、version.pyではなく、
__version__.pyというファイル名にした場合、
仮に、init.pyの中で__version__に関して何も指定していなければ、
my_package.__version__はversion.py**のファイル自体をモジュールとしたものになります。
中の__version__の値は
my_package.__version__.__version__で取得できます。
__init__.pyの中で、
1 2 3 | |
のように__version__.pyの中の__version__をimportして__all__に追加しておけば、
my_package.__version__は__version__.pyの中の__version__の値を参照することになります。
この場合、my_package.__version__.__version__でも同じ値が取得できます。
一方、上のように__getattr__を使っている場合、
my_package.__version__はまず__version__.pyを参照するので、
my_package.__version__は__version__.pyのモジュール自体を参照することになります。
この__getattr__に関してはちょっと不思議な挙動になります。
簡易的に直接バージョンを書いた__version__.pyを用意してimportされたときにprintするようにしてみます。
1 2 3 | |
__init__.pyの方は
1 2 3 4 5 6 7 8 9 10 11 12 | |
のようにprintを入れておきます。
直接my_package.__version__をimportした場合は
1 2 3 4 | |
のように__getattr__は呼ばれず、__version__.pyが直接読み込まれます。
from my_package import __version__のようにした場合、
1 2 3 4 5 | |
__getattr__は呼ばれているものの、__version__は__version__.pyのモジュール自体を参照しています。
最後に、my_packageだけimportしてその属性値として__version__を参照した場合、
1 2 3 4 5 6 7 8 | |
のように、最初に呼ばれたときは__version__.__version__の値が取得されますが、
その後は__version__.pyのモジュール自体を参照するようになります。
いずれにしろこういった感じで把握出来てない挙動を避けるため、
ファイル名は__getattr__で指定する名前とは異なる名前にしておくのが無難です。
-
1$ python -X importtime -c 'import importlib.metadata' 2> logとかするとこのライブラリの読み込みにかかった時間がわかります。
1 2 3 4 5 6 7 8
import time: self [us] | cumulative | imported package import time: 36 | 36 | _string import time: 36 | 36 | _abc ... import time: 486 | 9718 | email.utils import time: 670 | 11854 | email.message import time: 279 | 12418 | importlib.metadata._adapters import time: 1439 | 47991 | importlib.metadataimport my_packageのようにすればパッケージ全体の読み込み時間もわかります。時間がかかってるものを探すには
1$ python -X importtime -c 'import my_package' 2>&1 | sort -n - k5 > sorted.logのようにすれば
cumulativeの時間でソートされるので 時間のかかっているものがわかります。外部ライブラリで読み込みに時間がかかているものを探すなら
1$ python -X importtime -c 'import my_package' 2>&1 | grep -v my_package | sort -n - k5 > external.logのようにして探してみるとよいかと。 ↩