如何比较Python中的版本号?

我正在遍历一个包含卵的目录,以便将这些卵添加到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。是否有一种优雅的方法来比较版本字符串?

195820 次浏览

将版本字符串转换为元组并从那里开始有什么问题?对我来说已经够优雅了

>>> (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的解决方案是一个简单的例子,说明代码看起来有多好。

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


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

使用packaging.version.parse

>>> # pip install packaging
>>> 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没有文档,所以在这里是相关的文档字符串。

包装库包含用于使用版本和其他与打包相关的功能的实用程序。这实现了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

包装包可用,它将允许你根据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

基于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

你可以使用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

我正在寻找一个解决方案,不会增加任何新的依赖。检查以下(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的方法是使用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
< p >…回到简单的话题… 对于简单的脚本,您可以使用:

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

在代码的后面

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

类似于标准的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

假设你的语义版本是“干净”的,这里有一些东西会起作用。(例如x.x.x),你有一个需要排序的版本列表。

# 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']

要获得最新版本,您只需选择列表versions[-1]中的最后一个元素或使用sorted()reverse属性进行反向排序,将其设置为True,并获得[0]元素。

当然,您可以将所有这些打包到一个方便的函数中以供重用。

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"]))

使用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))

这是一个用于比较三个版本号的紧凑代码。注意,这里的字符串比较对所有对都失败了。

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)}")

这给了我们:

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)为我们提供了一个可迭代对象的所有长度为2的排列。例如,

list(permutations('ABC', 2))

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

如果你想在一个库版本上创建一个过滤器,你可以使用__version__属性(这里是jwt库的一个例子):

from packaging import version
import jwt


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