
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.metadata
import 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
のようにして探してみるとよいかと。 ↩