简体   繁体   English

如何比较 Python 中的版本号?

[英]How do I compare version numbers in Python?

I am walking a directory that contains eggs to add those eggs to the sys.path .我正在遍历一个包含鸡蛋的目录,以将这些鸡蛋添加到sys.path中。 If there are two versions of the same.egg in the directory, I want to add only the latest one.如果目录中有两个版本的 same.egg,我只想添加最新的一个。

I have a regular expression r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$ to extract the name and version from the filename. The problem is comparing the version number, which is a string like 2.3.1 .我有一个正则表达式r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$从文件名中提取名称和版本。问题是比较版本号,它是一个像2.3.1这样的字符串。

Since I'm comparing strings, 2 sorts above 10, but that's not correct for versions.由于我比较的是字符串,2 排序高于 10,但这对于版本来说是不正确的。

>>> "2.3.1" > "10.1.1"
True

I could do some splitting, parsing, casting to int, etc., and I would eventually get a workaround.我可以做一些拆分、解析、转换为 int 等,最终我会得到一个解决方法。 But this is Python, not Java .但这是 Python, 而不是 Java Is there an elegant way to compare version strings?有没有一种优雅的方法来比较版本字符串?

Use packaging.version.parse .使用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 is a third-party utility but is used by setuptools (so you probably already have it installed) and is conformant to the current PEP 440 ; packaging.version.parse是第三方实用程序,但由setuptools 使用(因此您可能已经安装了它)并且符合当前的PEP 440 it will return a packaging.version.Version if the version is compliant and a packaging.version.LegacyVersion if not.如果版本兼容,它将返回packaging.version.LegacyVersion如果不兼容,则返回packaging.version.Version The latter will always sort before valid versions.后者将始终排在有效版本之前。

Note : packaging has recently been vendored into setuptools .注意:包装最近已被供应到 setuptools 中


An ancient alternative still used by a lot of software is distutils.version , built in but undocumented and conformant only to the superseded PEP 386 ;许多软件仍在使用的一个古老的替代品是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'

As you can see it sees valid PEP 440 versions as “not strict” and therefore doesn't match modern Python's notion of what a valid version is.如您所见,它将有效的 PEP 440 版本视为“不严格”,因此与现代 Python 的有效版本概念不符。

As distutils.version is undocumented, here 's the relevant docstrings.由于distutils.version没有记录, 这里是相关的文档字符串。

The packaging library contains utilities for working with versions and other packaging-related functionality.打包库包含用于处理版本和其他打包相关功能的实用程序。 This implements PEP 0440 -- Version Identification and is also able to parse versions that don't follow the PEP.这实现了PEP 0440 -- 版本标识,并且还能够解析不遵循 PEP 的版本。 It is used by pip, and other common Python tools to provide version parsing and comparison. pip 和其他常用 Python 工具使用它来提供版本解析和比较。

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

This was split off from the original code in setuptools and pkg_resources to provide a more lightweight and faster package.这是从 setuptools 和 pkg_resources 中的原始代码中分离出来的,以提供更轻量和更快的包。


Before the packaging library existed, this functionality was (and can still be) found in pkg_resources, a package provided by setuptools.在打包库存在之前,这个功能已经(并且仍然可以)在 pkg_resources 中找到,这是一个由 setuptools 提供的包。 However, this is no longer preferred as setuptools is no longer guaranteed to be installed (other packaging tools exist), and pkg_resources ironically uses quite a lot of resources when imported.然而,这不再是首选,因为不再保证安装 setuptools(存在其他打包工具),并且具有讽刺意味的是 pkg_resources 在导入时使用了相当多的资源。 However, all the docs and discussion are still relevant.但是,所有文档和讨论仍然相关。

From the parse_version() docs :parse_version()文档

Parsed a project's version string as defined by PEP 440. The returned value will be an object that represents the version.解析了 PEP 440 定义的项目版本字符串。返回的值将是表示版本的对象。 These objects may be compared to each other and sorted.这些对象可以相互比较和排序。 The sorting algorithm is as defined by PEP 440 with the addition that any version which is not a valid PEP 440 version will be considered less than any valid PEP 440 version and the invalid versions will continue sorting using the original algorithm.排序算法由 PEP 440 定义,此外,任何不是有效 PEP 440 版本的版本都将被视为低于任何有效的 PEP 440 版本,并且无效版本将继续使用原始算法进行排序。

The "original algorithm" referenced was defined in older versions of the docs, before PEP 440 existed.在 PEP 440 存在之前,引用的“原始算法”是在旧版本的文档中定义的。

Semantically, the format is a rough cross between distutils' StrictVersion and LooseVersion classes;从语义StrictVersion ,该格式是 distutils 的StrictVersionLooseVersion类之间的粗略交叉; if you give it versions that would work with StrictVersion , then they will compare the same way.如果您为其提供可与StrictVersion一起使用的StrictVersion ,那么它们将以相同的方式进行比较。 Otherwise, comparisons are more like a "smarter" form of LooseVersion .否则,比较更像是LooseVersion的“更智能”形式。 It is possible to create pathological version coding schemes that will fool this parser, but they should be very rare in practice.有可能创建会愚弄这个解析器的病态版本编码方案,但它们在实践中应该很少见。

The documentation provides some examples:文档提供了一些示例:

If you want to be certain that your chosen numbering scheme works the way you think it will, you can use the pkg_resources.parse_version() function to compare different version numbers:如果您想确定您选择的编号方案按您认为的方式工作,您可以使用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
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

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

What's wrong with transforming the version string into a tuple and going from there?将版本字符串转换为元组并从那里开始有什么问题? Seems elegant enough for me对我来说似乎足够优雅

>>> (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's solution is a quick example of how good the code would look. @kindall 的解决方案是代码外观的快速示例。

There is packaging package available, which will allow you to compare versions as per PEP-440 , as well as legacy versions.包装封装,这将让你的版本比较,按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

Legacy version support:旧版支持:

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

Comparing legacy version with PEP-440 version.将旧版本与 PEP-440 版本进行比较。

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

The way that setuptools does it, it uses the pkg_resources.parse_version function. setuptools的做法是使用pkg_resources.parse_version函数。 It should be PEP440 compliant.它应该符合PEP440

Example:例子:

#! /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

You can use the semver package to determine if a version satisfies a semantic version requirement.您可以使用semver包来确定版本是否满足语义版本要求。 This is not the same as comparing two actual versions, but is a type of comparison.这与比较两个实际版本不同,而是一种比较。

For example, version 3.6.0+1234 should be the same as 3.6.0.例如,版本 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

Posting my full function based on Kindall's solution.根据 Kindall 的解决方案发布我的完整功能。 I was able to support any alphanumeric characters mixed in with the numbers by padding each version section with leading zeros.通过用前导零填充每个版本部分,我能够支持与数字混合的任何字母数字字符。

While certainly not as pretty as his one-liner function, it seems to work well with alpha-numeric version numbers.虽然肯定不如他的单行函数那么漂亮,但它似乎适用于字母数字版本号。 (Just be sure to set the zfill(#) value appropriately if you have long strings in your versioning system.) (如果您的版本控制系统中有长字符串,请务必正确设置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

I was looking for a solution which wouldn't add any new dependencies.我正在寻找一种不会添加任何新依赖项的解决方案。 Check out the following (Python 3) solution:查看以下(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()

EDIT: added variant with tuple comparison.编辑:添加了带有元组比较的变体。 Of course the variant with tuple comparison is nicer, but I was looking for the variant with integer comparison当然,元组比较的变体更好,但我正在寻找整数比较的变体

... and getting back to easy ... for simple scripts you can use: ... 回到简单 ... 对于简单的脚本,您可以使用:

import sys
needs = (3, 9) # or whatever
pvi = sys.version_info.major, sys.version_info.minor    

later in your code稍后在您的代码中

try:
    assert pvi >= needs
except:
    print("will fail!")
    # etc.

similar to standard strverscmp and similar to this solution by Mark Byers but using findall instead of split to avoid empty case.类似于标准strverscmp并类似于Mark Byers 的此解决方案,但使用 findall 而不是 split 以避免出现空情况。

import re
num_split_re = re.compile(r'([0-9]+|[^0-9]+)')

def try_int(i, fallback=None):
    try:
        return int(i)
    except ValueError:
        pass
    except TypeError:
        pass
    return fallback

def ver_as_list(a):
    return [try_int(i, i) for i in num_split_re.findall(a)]

def strverscmp_lt(a, b):
    a_ls = ver_as_list(a)
    b_ls = ver_as_list(b)
    return a_ls < b_ls

Here is something that will work assuming your semantic versions are "clean" (eg xxx ) and you have a list of versions you need to sort.假设您的语义版本是“干净的”(例如xxx )并且您有一个需要排序的版本列表,那么这将起作用。

# Here are some versions
versions = ["1.0.0", "1.10.0", "1.9.0"]

# This does not work
versions.sort() # Result: ['1.0.0', '1.10.0', '1.9.0']

# So make a list of tuple versions
tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]

# And sort the string list based on the tuple list
versions = [x for _, x in sorted(zip(tuple_versions, versions))] # Result: ['1.0.0', '1.9.0', '1.10.0']

To get the latest version you could just select the last element in the list versions[-1] or reverse sort by using the reverse attribute of sorted() , setting it to True , and getting the [0] element.要获取最新版本,您只需选择列表versions[-1]的最后一个元素,或使用sorted()reverse属性进行reverse sorted() ,将其设置为True并获取[0]元素。

You could of course then wrap all this up in a convenient function for reuse.您当然可以将所有这些包装在一个方便的函数中以供重用。

def get_latest_version(versions):
    """
    Get the latest version from a list of versions.
    """
    try:
        tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]
        versions = [x for _, x in sorted(zip(tuple_versions, versions), reverse=True)]
        latest_version = versions[0]
    except Exception as e:
        print(e)
        latest_version = None

    return latest_version

print(get_latest_version(["1.0.0", "1.10.0", "1.9.0"]))

To increment version using python使用 python 增加版本

def increment_version(version):
    version = version.split('.')
    if int(version[len(version) - 1]) >= 99:
        version[len(version) - 1] = '0'
        version[len(version) - 2] = str(int(version[len(version) - 2]) + 1)
    else:
        version[len(version) - 1] = str(int(version[len(version) - 1]) + 1)
    return '.'.join(version)

version = "1.0.0"
version_type_2 = "1.0"
print("old version",version ,"new version",increment_version(version))
print("old version",version_type_2 ,"new version",increment_version(version_type_2))

This is a compact code for comparing three version numbers.这是用于比较三个版本号的紧凑代码。 Note that the string comparison fails for all pairs here.请注意,此处所有对的字符串比较都失败。

from itertools import permutations

for v1, v2 in permutations(["3.10.21", "3.10.3", "3.9.9"], 2):
    print(f"\nv1 = {v1}, v2 = {v2}")
    print(f"v1 < v2      version.parse(v1) < version.parse(v2)")
    print(f"{v1 < v2}         {version.parse(v1) < version.parse(v2)}")

That gives us:这给了我们:

v1='3.10.21', v2='3.10.3'
v1 < v2      version.parse(v1) < version.parse(v2)
True         False

v1='3.10.21', v2='3.9.9'
v1 < v2      version.parse(v1) < version.parse(v2)
True         False

v1='3.10.3', v2='3.10.21'
v1 < v2      version.parse(v1) < version.parse(v2)
False         True

v1='3.10.3', v2='3.9.9'
v1 < v2      version.parse(v1) < version.parse(v2)
True         False

v1='3.9.9', v2='3.10.21'
v1 < v2      version.parse(v1) < version.parse(v2)
False         True

v1='3.9.9', v2='3.10.3'
v1 < v2      version.parse(v1) < version.parse(v2)
False         True

permutations(iterable, 2) gives us all the 2-length permutations of an iterable. permutations(iterable, 2)为我们提供了一个可迭代对象的所有 2 长度排列 So for example所以例如

list(permutations('ABC', 2))

gives us [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')] .给我们[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

If you want to create a filter on a library version, you may use the __version__ attribute (here an example with the jwt library):如果要在库版本上创建过滤器,可以使用__version__属性(这里是 jwt 库的示例):

from packaging import version
import jwt

if version.parse(jwt.__version__) < version.parse('2.0.0'):
    # TODO: your code

simple few-liner:简单的几行:

import sys
if (sys.version_info.major, sys.version_info.minor) >= (3, 9):
    ...
else:
    ...

i will go more for the touple option, doing a test, using LooseVersion, i do get in my test the second biggest one, (might be doing something wront since is my first time using that library) 我会更多地选择touple选项,做一个测试,使用LooseVersion,我确实在我的测试中获得了第二大选项,(可能是因为我第一次使用该库而做了一些事情)

import itertools
from distutils.version import LooseVersion, StrictVersion

lista_de_frameworks = ["1.1.1", "1.2.5", "10.5.2", "3.4.5"]

for a, b in itertools.combinations(lista_de_frameworks, 2):
    if LooseVersion(a) < LooseVersion(b):
        big = b
print big

list_test = []
for a in lista_de_frameworks:
    list_test.append( tuple(map(int, (a.split(".")))))

print max(list_test)

and this is what i got: 这就是我得到的:

3.4.5 with Loose 3.4.5松散

(10, 5, 2) and with the touples (10,5,2)和与touples

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM