如何比较 Python 中的版本号?
Posted
技术标签:
【中文标题】如何比较 Python 中的版本号?【英文标题】:How do I compare version numbers in Python? 【发布时间】:2021-10-23 14:57:57 【问题描述】:我正在浏览一个包含鸡蛋的目录,以将这些鸡蛋添加到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,not Java。有没有比较优雅的方法来比较版本字符串?
【问题讨论】:
【参考方案1】:将版本字符串转换为元组并从那里开始有什么问题?对我来说似乎足够优雅
>>> (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 的解决方案是代码外观的一个简单示例。
【讨论】:
我认为可以通过提供将 PEP440 字符串转换为元组的代码来扩展此答案。我想你会发现这不是一项微不足道的任务。我认为最好留给为setuptools
执行翻译的包,即pkg_resources
。
@TylerGubala 在您知道版本是并且将永远是“简单”的情况下,这是一个很好的答案。 pkg_resources 是一个大包,可能导致分布式可执行文件相当臃肿。
@Erik Aronesty 我认为分布式可执行文件内部的版本控制有点超出了问题的范围,但我同意,至少一般来说。我认为尽管pkg_resources
的可重用性有一些话要说,而且简单包命名的假设可能并不总是理想的。
它非常适合确保sys.version_info > (3, 6)
或其他任何东西。【参考方案2】:
def versiontuple(v):
return tuple(map(int, (v.split("."))))
>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
【讨论】:
其他答案在标准库中,遵循 PEP 标准。 在这种情况下,您可以完全删除map()
函数,因为split()
的结果是已经 字符串。但是无论如何您都不想这样做,因为将它们更改为int
的全部原因是它们可以作为数字进行正确比较。否则"10" < "2"
.
这对于versiontuple("1.0") > versiontuple("1")
之类的东西会失败。版本相同,但创建的元组(1,)!=(1,0)
版本 1 和版本 1.0 在什么意义上是相同的?版本号不是浮点数。
不,这应该不是公认的答案。谢天谢地,它不是。在一般情况下,版本说明符的可靠解析是不平凡的(如果不是实际上不可行的话)。不要重新发明***,然后继续打破它。正如ecatmur 建议的above,只需使用distutils.version.LooseVersion
。这就是它的用途。【参考方案3】:
使用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
。后者总是在有效版本之前排序。
注意:最近打包的是vendored into setuptools。
您可能遇到的一个古老的now deprecated 方法是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
没有记录,here 是相关的文档字符串。
【讨论】:
看起来 NormalizedVersion 不会出现,因为它已被取代,因此 LooseVersion 和 StrictVersion 不再被弃用。 这是一个哭泣的耻辱distutils.version
是无证的。
通过搜索引擎找到的,直接找到version.py
源代码。非常好!
恕我直言 packaging.version.parse
不能信任比较版本。例如,试试parse('1.0.1-beta.1') > parse('1.0.0')
。
在 Python 3.6+ 中:from pkg_resources import packaging
然后packaging.version.parse("0.1.1rc1") < packaging.version.parse("0.1.1rc2")
【参考方案4】:
packaging 库包含用于working with versions 和其他与打包相关的功能的实用程序。这实现了PEP 0440 -- Version Identification,并且还能够解析不遵循 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 中找到,pkg_resources 是 setuptools 提供的一个包。但是,这不再是首选,因为不再保证安装 setuptools(存在其他打包工具),而且 pkg_resources 在导入时使用了相当多的资源。但是,所有文档和讨论仍然相关。
来自parse_version()
docs:
按照 PEP 440 的定义解析项目的版本字符串。返回值将是表示版本的对象。这些对象可以相互比较和分类。排序算法由 PEP 440 定义,此外,任何不是有效 PEP 440 版本的版本都将被视为低于任何有效 PEP 440 版本,无效版本将继续使用原始算法进行排序。
引用的“原始算法”是在 PEP 440 存在之前的旧版本文档中定义的。
从语义上讲,格式是 distutils 的
StrictVersion
和LooseVersion
类之间的粗略交叉;如果您给它提供与StrictVersion
一起使用的版本,那么它们将以相同的方式进行比较。否则,比较更像是LooseVersion
的“更智能”形式。可以创建会欺骗此解析器的病态版本编码方案,但在实践中应该很少见。
documentation 提供了一些示例:
如果您想确定您选择的编号方案是否有效 你认为它会的方式,你可以使用
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
【讨论】:
【参考方案5】:有可用的packaging 软件包,可让您根据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
【讨论】:
对于那些想知道packaging.version.Version
和packaging.version.parse
之间区别的人:“[version.parse
] 接受版本字符串,如果版本是有效的 PEP 440,则将其解析为Version
版本,否则它将解析为LegacyVersion
。” (而version.Version
会提高InvalidVersion
;source)
NB:LooseVersion
在 3.10 中产生弃用警告:DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 6s
【参考方案6】:
根据 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
【讨论】:
【参考方案7】:您可以使用semver 包来确定版本是否满足semantic version 要求。这与比较两个实际版本不同,而是一种比较。
例如版本 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
【讨论】:
【参考方案8】:我正在寻找一种不会添加任何新依赖项的解决方案。查看以下 (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 包吗?【参考方案9】: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 我不认为setuptools
依赖于packaging
。我可以导入setuptools
和pkg_resources
,但import packaging
会引发ImportError。
这是唯一适用于 16.04.6 LTS、python3.8 的解决方案【参考方案10】:
... 回到轻松... 对于您可以使用的简单脚本:
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.
【讨论】:
【参考方案11】:类似于标准的strverscmp 和this solution by 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
【讨论】:
以上是关于如何比较 Python 中的版本号?的主要内容,如果未能解决你的问题,请参考以下文章