rcmdnk's blog

20250809_pythonversion_200_200

Pythonのパッケージを作る際、通常pyproject.tomlやsetup.pyにバージョンを記載しますが、 pythonのコード内でもそのバージョンを取得する方法があります。

ただ、読み込むためのライブラリがちょっと重いので 必要なときだけ読み込むような工夫も入れてみます。

よくあるバージョン情報記述

パッケージルートディレクトリの__init__.py

__init__.py
1
2
3
__version__ = "0.1.0"

__all__ = ["__version__"]

のように書いておくとこれがmy_packageというパッケージだった場合、

1
2
3
from my_package import __version__

print(__version__)

とか、my_package.__version__でバージョンを取得できます。

ただ、__init__.pyに直接書いてしまうと パッケージ内の他のモジュールから参照するのに問題が起こる時があります。

例えばmain.pyの中で__version__を参照したいが、 __init__.pyの方でmain.pyの中で定義されている関数などをimportしていると 循環参照になってしまい、エラーが発生します。

なのでversion情報を別に分けて

version.py
1
__version__ = "0.1.0"
__init__.py
1
2
3
4
from .version import __version__
from .main import main_function

__all__ = ["__version__", "main_function"]
main.py
1
2
3
4
5
6
7
8
9
from .version import __version__


def version() -> str:
    return __version__


def main_function() -> None:
    ....

みたいな感じにすれば外からは最初のと同様に、 パッケージ内からも参照できるようになります。

これでコードの中だけにおいてはバージョンを統一的に扱うことが出来ますが、 管理ツール側でversionを定義している場合、 そちらとの値を同期させる必要があり面倒です。

importlib.metadata.version

uvで管理しているプロジェクトでpyproject.tomlproject.versionが定義してあるような場合、 そのパッケージ内で

1
2
3
import importlib.metadata

print(importlib.metadata.version(__package__))

とすればproject.versionの値が取得できます。

基本的には他の管理ツールでも同様にversionの値が取得できます。

これをversion.pyの中に

version.py
1
2
3
import importlib.metadata

__version__ = importlib.metadata.version(__package__)

のように書いておけば、上の例のままmy_package.__version__でバージョンが取れますし、 管理ツール側でversionを更新すればそのまま反映されます。

通常はこれでも十分。

__getattr__を使った遅延読み込み

ただ、importlib.metadataをimportするのにちょっと時間がかかるので 1 必要なときだけ読み込むようにしたい場合があります。

とは言っても今の普通の環境で0.1秒かからない程度なので通常は無理にやる必要もないかもですが、 例えばコマンドラインツールを作っていて、my_package --helpのようにヘルプを表示したいだけの場合、 0.1秒でもちょっと気になることがあります。

そのような場合は、version.pyはそのままで、 __init__.pyの方で__getattr__を使ったlazy loadingな実装をしてみます。

__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
from .main import main_function

__all__ = ["__version__", "main_function"]


def __getattr__(name: str) -> str:
    if name == '__version__':
        from .version import __version__

        return __version__
    msg = f'module {__name__} has no attribute {name}'
    raise AttributeError(msg)

のように、__version__が参照されたときだけ version.pyをimportするようにします。

ただ、このままだと、__init__.pyが呼ばれたとき、main.pyの方も呼ばれて 結果その中でversion.pyがimportされてしまいます。

なので、

main.py
1
2
3
4
5
6
7
8
def version() -> str:
    from .version import __version__

    return __version__


def main_function() -> None:
    ....

のような感じでこちらも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の中で、

__init__.py
1
2
3
from .__version__ import __version__

__all__ = ["__version__"]

のように__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するようにしてみます。

__version__.py
1
2
3
print("Loading __version__.py")

__version__ = "0.1.0"

__init__.pyの方は

__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
__all__ = ["__version__"]


def __getattr__(name: str) -> str:
    if name == '__version__':
        print('__getattr__ is called for __version__')

        from .version import __version__

        return __version__
    msg = f'module {__name__} has no attribute {name}'
    raise AttributeError(msg)

のようにprintを入れておきます。

直接my_package.__version__をimportした場合は

1
2
3
4
>>> import my_package.__version__
Loading __version__.py
>>> print(my_package.__version__)
<module 'my_package.__version__' from '.../my_package/src/my_package/xyz/__version__.py'>

のように__getattr__は呼ばれず、__version__.pyが直接読み込まれます。

from my_package import __version__のようにした場合、

1
2
3
4
5
>>> from my_package import __version__
__getattr__ is called for __version__
Loading __version__.py
>>> print(__version__)
<module 'my_package.__version__' from '.../my_package/src/my_package/__version__.py'>

__getattr__は呼ばれているものの、__version____version__.pyのモジュール自体を参照しています。

最後に、my_packageだけimportしてその属性値として__version__を参照した場合、

1
2
3
4
5
6
7
8
>>> import my_package
>>> __version__ = my_package.__version__
__getattr__ is called for __version__
Loading __version__.py
>>> print(__version__)
0.1.0
>>> __version__ = my_package.__version__
<module 'my_package.__version__' from '/.../my_package/src/my_package/__version__.py'>

のように、最初に呼ばれたときは__version__.__version__の値が取得されますが、 その後は__version__.pyのモジュール自体を参照するようになります。

いずれにしろこういった感じで把握出来てない挙動を避けるため、 ファイル名は__getattr__で指定する名前とは異なる名前にしておくのが無難です。

Sponsored Links
  1. 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
    

    のようにして探してみるとよいかと。

Sponsored Links

« git worktreeの管理 Oura Ring 4購入 »

}