Pythonでバージョン番号を比較するにはどうすればよいですか?


236

これらの卵をに追加するために、卵を含むディレクトリを歩いていますsys.path。ディレクトリに同じ.eggの2つのバージョンがある場合、最新のバージョンのみを追加します。

r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$ファイル名から名前とバージョンを抽出する正規表現があります。問題は、のような文字列であるバージョン番号を比較することです2.3.1

文字列を比較しているので、2は10より上でソートされますが、バージョンでは正しくありません。

>>> "2.3.1" > "10.1.1"
True

分割、解析、intへのキャストなどを行うことができ、最終的には回避策が得られます。しかし、これはPythonではなく、Javaです。バージョン文字列を比較するエレガントな方法はありますか?

回答:


367

を使用しpackaging.version.parseます。

>>> from packaging import version
>>> version.parse("2.3.1") < version.parse("10.1.2")
True
>>> version.parse("1.3.a4") < version.parse("10.1.2")
True
>>> isinstance(version.parse("1.3.a4"), version.Version)
True
>>> isinstance(version.parse("1.3.xy123"), version.LegacyVersion)
True
>>> version.Version("1.3.xy123")
Traceback (most recent call last):
...
packaging.version.InvalidVersion: Invalid version: '1.3.xy123'

packaging.version.parseサードパーティのユーティリティですが、setuptoolsによって使用され(おそらくすでにインストールされています)、現在のPEP 440に準拠しています。packaging.version.Versionバージョンが準拠しているpackaging.version.LegacyVersion場合はを、準拠していない場合はを返します。後者は常に有効なバージョンの前にソートされます。

:パッケージは最近setuptoolsに組み込まれました


多くのソフトウェアでまだ使用されている古代の代替案はdistutils.version、組み込まれていますが、文書化されておらず、置き換えられたPEP 386にのみ準拠しています。

>>> from distutils.version import LooseVersion, StrictVersion
>>> LooseVersion("2.3.1") < LooseVersion("10.1.2")
True
>>> StrictVersion("2.3.1") < StrictVersion("10.1.2")
True
>>> StrictVersion("1.3.a4")
Traceback (most recent call last):
...
ValueError: invalid version number '1.3.a4'

ご覧のとおり、有効なPEP 440バージョンは「厳密ではない」と見なされているため、最新のPythonの有効なバージョンの概念と一致しません。

distutils.version文書化され、ここで「関連するドキュメンテーション文字列をね。


2
NormalizedVersionは置き換えられたように見えなくなり、LooseVersionとStrictVersionは廃止されなくなりました。
Taywee

12
それは泣いて恥だdistutils.version文書化されていません。
John Y

検索エンジンを使用してそれを見つけ、version.pyソースコードを直接見つけました。とてもきれいに入れます!
ジョエル・

@Tayweeは、PEP 440に準拠していないため、より適切です。
空飛ぶ羊

2
imho packaging.version.parseはバージョンの比較を信頼できません。parse('1.0.1-beta.1') > parse('1.0.0')たとえば試してみてください。
トロンド

104

パッケージングライブラリがためのユーティリティが含まれているバージョンでの作業やその他の包装関連機能。これはPEP 0440-バージョン識別を実装し、PEPに従わないバージョンを解析することもできます。バージョン解析と比較を提供するために、pipやその他の一般的なPythonツールで使用されます。

$ pip install packaging
from packaging.version import parse as parse_version
version = parse_version('1.0.3.dev')

これは、より軽量で高速なパッケージを提供するために、setuptoolsとpkg_resourcesの元のコードから分離されました。


パッケージライブラリが存在する前は、この機能はsetuptoolsが提供するパッケージであるpkg_resourcesにありました(現在も使用できます)。ただし、setuptoolsのインストールが保証されなくなったため(他のパッケージツールが存在するため)、pkg_resourcesは皮肉にもインポート時に大量のリソースを使用するため、これは推奨されなくなりました。ただし、すべてのドキュメントとディスカッションはまだ関連しています。

parse_version()ドキュメントから:

PEP 440で定義されているプロジェクトのバージョン文字列を解析しました。戻り値は、バージョンを表すオブジェクトになります。これらのオブジェクトは互いに比較され、ソートされます。並べ替えアルゴリズムはPEP 440で定義されていますが、有効なPEP 440バージョンではないバージョンは有効なPEP 440バージョンよりも少ないと見なされ、無効なバージョンは元のアルゴリズムを使用して並べ替えを続行します。

参照されている「元のアルゴリズム」は、PEP 440が存在する前の古いバージョンのドキュメントで定義されていました。

意味的には、フォーマットはdistutils StrictVersionLooseVersionクラスの大まかなクロスです。で動作するバージョンStrictVersionを指定すると、同じように比較されます。それ以外の場合、比較はの「よりスマートな」形式に似ていLooseVersionます。このパーサーをだます病理学的バージョンのコード体系を作成することは可能ですが、実際には非常にまれであるべきです。

ドキュメントには、いくつかの例を提供します。

選択した番号付けスキームが思ったpkg_resources.parse_version() とおりに機能することを確認したい場合は、関数を使用してさまざまなバージョン番号を比較できます。

>>> from pkg_resources import parse_version
>>> parse_version('1.9.a.dev') == parse_version('1.9a0dev')
True
>>> parse_version('2.1-rc2') < parse_version('2.1')
True
>>> parse_version('0.6a9dev-r41475') < parse_version('0.6a9')
True

57
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False

10
他の答えは標準ライブラリにあり、PEP標準に従います。
クリス

1
その場合map()、結果split()すでに文字列であるため、関数を完全に削除できます。しかし、とにかくそれをしたくないのです。なぜなら、それらをに変更する理由はすべてint、数値として適切に比較できるようにするためです。それ以外の場合"10" < "2"
キンダル

6
これはのような場合に失敗しますversiontuple("1.0") > versiontuple("1")。バージョンは同じですが、タプルが作成されました(1,)!=(1,0)
dawg

3
バージョン1とバージョン1.0はどのような意味で同じですか?バージョン番号は浮動小数点数ではありません。
キンダル2015

12
いいえ、これはすべきではない受け入れられた答えです。ありがたいことに、そうではありません。バージョン指定子の信頼性の高い解析は、一般的なケースでは(実際には実行不可能でない場合)簡単ではありません。ホイールを再発明してから、それを壊そうとしないでください。以下のようecatmurが示唆以上、ちょうど使用distutils.version.LooseVersion。それが目的です。
セシルカレー

12

バージョン文字列をタプルに変換してそこから移動することの何が問題になっていますか?私には十分エレガントなようです

>>> (2,3,1) < (10,1,1)
True
>>> (2,3,1) < (10,1,1,1)
True
>>> (2,3,1,10) < (10,1,1,1)
True
>>> (10,3,1,10) < (10,1,1,1)
False
>>> (10,3,1,10) < (10,4,1,1)
True

@kindallのソリューションは、コードの見栄えの良い例です。


1
この答えは、PEP440文字列をタプルに変換するコードを提供することでさらに拡張できると思います。それは簡単な仕事ではないでしょう。私はよりよいのために実行することを翻訳することをパッケージに残っていると思うsetuptools、ですpkg_resources

@TylerGubalaこれは、バージョンが「シンプル」であり、常に「シンプル」であることがわかっている場合の優れた回答です。pkg_resourcesは大きなパッケージであり、分散実行可能ファイルをかなり肥大化させる可能性があります。
Erik Aronesty

@Erik誤解分散実行可能ファイル内のバージョン管理は、質問の範囲からある程度外れていると思いますが、少なくとも一般的には同意します。の再利用性については注意すべきことがあると思いますがpkg_resources、単純なパッケージの命名の仮定は必ずしも理想的ではないかもしれません。

それは確認sys.version_info > (3, 6)などに最適です。
Gqqnbig

7

あるパッケージあなたがごとにバージョンを比較することができます利用可能なパッケージ、PEP-440と同様に、レガシーバージョンを。

>>> from packaging.version import Version, LegacyVersion
>>> Version('1.1') < Version('1.2')
True
>>> Version('1.2.dev4+deadbeef') < Version('1.2')
True
>>> Version('1.2.8.5') <= Version('1.2')
False
>>> Version('1.2.8.5') <= Version('1.2.8.6')
True

レガシーバージョンのサポート:

>>> LegacyVersion('1.2.8.5-5-gdeadbeef')
<LegacyVersion('1.2.8.5-5-gdeadbeef')>

レガシーバージョンとPEP-440バージョンの比較。

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True

3
packaging.version.Versionとの違いについて不思議に思う人のためにpackaging.version.parse:「[ version.parse]はバージョン文字列を取りVersion、バージョンが有効なPEP 440バージョンであるかどうかを解析し、それ以外の場合はとして解析しLegacyVersionます。」(version.VersionレイズInvalidVersion; ソース
Braham Snyder

5

semverパッケージを使用して、バージョンがセマンティックバージョンを満たすかどうかを判断できます要件をます。これは、2つの実際のバージョンを比較することとは異なりますが、一種の比較です。

たとえば、バージョン3.6.0 + 1234は3.6.0と同じである必要があります。

import semver
semver.match('3.6.0+1234', '==3.6.0')
# True

from packaging import version
version.parse('3.6.0+1234') == version.parse('3.6.0')
# False

from distutils.version import LooseVersion
LooseVersion('3.6.0+1234') == LooseVersion('3.6.0')
# False

3

Kindallのソリューションに基づいて私の全機能を投稿します。各バージョンのセクションに先行ゼロを埋め込むことにより、数字と混在する任意の英数字をサポートすることができました。

確かに彼のワンライナー関数ほどきれいではありませんが、英数字のバージョン番号でうまく機能するようです。(zfill(#)バージョン管理システムに長い文字列がある場合は、値を適切に設定してください。)

def versiontuple(v):
   filled = []
   for point in v.split("."):
      filled.append(point.zfill(8))
   return tuple(filled)

>>> versiontuple("10a.4.5.23-alpha") > versiontuple("2a.4.5.23-alpha")
True


>>> "10a.4.5.23-alpha" > "2a.4.5.23-alpha"
False

2

それを行う方法setuptoolsは、pkg_resources.parse_version関数を使用します。PEP440である必要があります準拠します。

例:

#! /usr/bin/python
# -*- coding: utf-8 -*-
"""Example comparing two PEP440 formatted versions
"""
import pkg_resources

VERSION_A = pkg_resources.parse_version("1.0.1-beta.1")
VERSION_B = pkg_resources.parse_version("v2.67-rc")
VERSION_C = pkg_resources.parse_version("2.67rc")
VERSION_D = pkg_resources.parse_version("2.67rc1")
VERSION_E = pkg_resources.parse_version("1.0.0")

print(VERSION_A)
print(VERSION_B)
print(VERSION_C)
print(VERSION_D)

print(VERSION_A==VERSION_B) #FALSE
print(VERSION_B==VERSION_C) #TRUE
print(VERSION_C==VERSION_D) #FALSE
print(VERSION_A==VERSION_E) #FALSE

pkg_resources一部であるsetuptoolsに依存し、packagingpackaging.version.parseと同じ実装を持つについて説明している他の回答を参照してくださいpkg_resources.parse_version
Jed

0

新しい依存関係を追加しないソリューションを探していました。次の(Python 3)ソリューションを確認してください。

class VersionManager:

    @staticmethod
    def compare_version_tuples(
            major_a, minor_a, bugfix_a,
            major_b, minor_b, bugfix_b,
    ):

        """
        Compare two versions a and b, each consisting of 3 integers
        (compare these as tuples)

        version_a: major_a, minor_a, bugfix_a
        version_b: major_b, minor_b, bugfix_b

        :param major_a: first part of a
        :param minor_a: second part of a
        :param bugfix_a: third part of a

        :param major_b: first part of b
        :param minor_b: second part of b
        :param bugfix_b: third part of b

        :return:    1 if a  > b
                    0 if a == b
                   -1 if a  < b
        """
        tuple_a = major_a, minor_a, bugfix_a
        tuple_b = major_b, minor_b, bugfix_b
        if tuple_a > tuple_b:
            return 1
        if tuple_b > tuple_a:
            return -1
        return 0

    @staticmethod
    def compare_version_integers(
            major_a, minor_a, bugfix_a,
            major_b, minor_b, bugfix_b,
    ):
        """
        Compare two versions a and b, each consisting of 3 integers
        (compare these as integers)

        version_a: major_a, minor_a, bugfix_a
        version_b: major_b, minor_b, bugfix_b

        :param major_a: first part of a
        :param minor_a: second part of a
        :param bugfix_a: third part of a

        :param major_b: first part of b
        :param minor_b: second part of b
        :param bugfix_b: third part of b

        :return:    1 if a  > b
                    0 if a == b
                   -1 if a  < b
        """
        # --
        if major_a > major_b:
            return 1
        if major_b > major_a:
            return -1
        # --
        if minor_a > minor_b:
            return 1
        if minor_b > minor_a:
            return -1
        # --
        if bugfix_a > bugfix_b:
            return 1
        if bugfix_b > bugfix_a:
            return -1
        # --
        return 0

    @staticmethod
    def test_compare_versions():
        functions = [
            (VersionManager.compare_version_tuples, "VersionManager.compare_version_tuples"),
            (VersionManager.compare_version_integers, "VersionManager.compare_version_integers"),
        ]
        data = [
            # expected result, version a, version b
            (1, 1, 0, 0, 0, 0, 1),
            (1, 1, 5, 5, 0, 5, 5),
            (1, 1, 0, 5, 0, 0, 5),
            (1, 0, 2, 0, 0, 1, 1),
            (1, 2, 0, 0, 1, 1, 0),
            (0, 0, 0, 0, 0, 0, 0),
            (0, -1, -1, -1, -1, -1, -1),  # works even with negative version numbers :)
            (0, 2, 2, 2, 2, 2, 2),
            (-1, 5, 5, 0, 6, 5, 0),
            (-1, 5, 5, 0, 5, 9, 0),
            (-1, 5, 5, 5, 5, 5, 6),
            (-1, 2, 5, 7, 2, 5, 8),
        ]
        count = len(data)
        index = 1
        for expected_result, major_a, minor_a, bugfix_a, major_b, minor_b, bugfix_b in data:
            for function_callback, function_name in functions:
                actual_result = function_callback(
                    major_a=major_a, minor_a=minor_a, bugfix_a=bugfix_a,
                    major_b=major_b, minor_b=minor_b, bugfix_b=bugfix_b,
                )
                outcome = expected_result == actual_result
                message = "{}/{}: {}: {}: a={}.{}.{} b={}.{}.{} expected={} actual={}".format(
                    index, count,
                    "ok" if outcome is True else "fail",
                    function_name,
                    major_a, minor_a, bugfix_a,
                    major_b, minor_b, bugfix_b,
                    expected_result, actual_result
                )
                print(message)
                assert outcome is True
                index += 1
        # test passed!


if __name__ == '__main__':
    VersionManager.test_compare_versions()

編集:タプル比較付きのバリアントを追加しました。もちろんタプル比較のバリアントの方がいいですが、整数比較のバリアントを探していました


これが依存関係の追加を回避する状況を知りたいのですが。Pythonパッケージを作成するために、(setuptoolsによって使用される)パッケージライブラリが必要ではありませんか?
Josiah L.
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.