如何比较Python中的版本号?


236

我正在走一个包含鸡蛋的目录,将这些鸡蛋添加到sys.path。如果目录中有相同.egg的两个版本,我只想添加最新的一个。

我有一个正则表达式r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$可以从文件名中提取名称和版本。问题是比较版本号,它是一个类似的字符串2.3.1

由于我正在比较字符串,所以2在10之上排序,但这对于版本来说是不正确的。

>>> "2.3.1" > "10.1.1"
True

我可以进行一些拆分,解析,转换为int等操作,最终得到解决方法。但这是Python,而不是Java。有没有比较版本字符串的优雅方法?

Answers:


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 440packaging.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没有记录,这是一种哭泣的耻辱。
约翰Y

使用搜索引擎找到了它,并直接找到了version.py源代码。非常好放!
乔尔

@Taywee更好,因为它们不符合PEP 440。
飞羊

2
packaging.version.parse无法信任imho 来比较版本。parse('1.0.1-beta.1') > parse('1.0.0')例如尝试。
Trondh

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中的原始代码分开,以提供更轻便,更快速的程序包。


在打包库存在之前,可以(并且仍然可以)在pkg_resources(setuptools提供的软件包)中找到此功能。但是,由于不再保证已安装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标准。
克里斯(Chris)

1
在这种情况下,你可以删除map()全部功能,结果split()已经串。但是您还是不想这样做,因为将它们更改为的全部原因int是为了将它们正确地比较为数字。否则"10" < "2"
kindall

6
这样会失败versiontuple("1.0") > versiontuple("1")。版本是相同的,但是创建了元组(1,)!=(1,0)
dawg

3
从什么意义上说版本1和版本1.0相同?版本号不是浮点数。
kindall

12
不,这应该不会是公认的答案。幸运的是,事实并非如此。在一般情况下,可靠地解析版本说明符是不容易的(如果实际上不可行)。不要重新发明轮子,然后继续进行断裂。正如ecatmur 在上面建议的那样,只需使用即可distutils.version.LooseVersion。那就是它的目的。
Cecil Curry'3

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字符串转换为元组的代码来扩展此答案。我认为您会发现这不是一件容易的事。我认为最好是执行翻译的软件包setuptoolspkg_resources

@TylerGubala在您知道该版本始终是“简单”的情况下,这是一个很好的答案。pkg_resources是一个很大的程序包,它可能导致分布式可执行文件变得肿。
Erik Aronesty

@Erik Aronesty我认为分布式可执行文件内部的版本控制在一定程度上避免了这个问题,但是至少我同意。我认为尽管关于的可​​重用性尚有话要说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.Versionpackaging.version.parse:“[ version.parse]需要一个版本字符串,并将其解析为Version如果版本是一个有效的PEP 440版本,否则将解析它的LegacyVersion。” (鉴于version.Version会提出InvalidVersion来源
Braham Snyder

5

您可以使用semver包来确定版本是否满足语义版本要求。这与比较两个实际版本不同,只是一种比较。

例如,版本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,取决于packaging。请参阅其他讨论的答案packaging.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()

编辑:添加了元组比较的变体。当然,具有元组比较的变体更好,但是我一直在寻找具有整数比较的变体


我很好奇在什么情况下可以避免添加依赖项?您是否不需要打包库(由setuptools使用)来创建python包?
Josiah L.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.