如何比较 Python 中的版本号?
- 2024-12-27 08:47:00
- admin 原创
- 115
问题描述:
我正在遍历包含鸡蛋的目录,以将这些鸡蛋添加到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。有没有一种优雅的方法来比较版本字符串?
解决方案 1:
使用packaging.version.Version
支持PEP 440样式排序的版本字符串。
>>> # pip install packaging
>>> from packaging.version import Version
>>> Version("2.3.1") < Version("10.1.2")
True
>>> Version("1.3.a4") < Version("10.1.2")
True
您可能会遇到一种古老且现已弃用的方法是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:
打包库包含用于处理版本和其他打包相关功能的实用程序。它实现了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 存在之前的旧版本文档中定义的。
StrictVersion
从语义上讲,该格式是 distutils和类的粗略混合LooseVersion
;如果您为其提供可与 配合使用的版本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
解决方案 3:
def versiontuple(v):
return tuple(map(int, (v.split("."))))
>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
解决方案 4:
将版本字符串转换为元组并从那里开始有什么问题?对我来说似乎足够优雅
>>> (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 的解决方案是一个简单示例,展示了代码看起来有多好。
解决方案 5:
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
解决方案 6:
有可用的包装包,可让您根据PEP-440比较版本。
>>> from packaging.version import Version
>>> 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
packaging
不支持旧版本字符串。如果您需要支持非有效 PEP 440 版本的字符串,请考虑使用looseversion 。
解决方案 7:
根据 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
解决方案 8:
您可以使用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
解决方案 9:
如果您想在库版本上创建过滤器,您可以使用该__version__
属性(此处是 jwt 库的示例):
from packaging import version
import jwt
if version.parse(jwt.__version__) < version.parse('2.0.0'):
# TODO: your code
解决方案 10:
使用 sys.version_info 元组:
assert sys.version_info >= (3, 9)
解决方案 11:
我正在寻找一种不会添加任何新依赖项的解决方案。请查看以下 (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()
编辑:添加了带有元组比较的变体。当然,带有元组比较的变体更好,但我正在寻找带有整数比较的变体
解决方案 12:
...回到简单...对于简单的脚本,您可以使用:
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.
解决方案 13:
使用 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))
解决方案 14:
这是用于比较三个版本号的简洁代码。请注意,此处所有对的字符串比较均失败。
from itertools import permutations
for v1, v2 in permutations(["3.10.21", "3.10.3", "3.9.9"], 2):
print(f"
v1 = {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')]
。
解决方案 15:
与标准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
解决方案 16:
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]
,或使用reverse
的属性进行反向排序sorted()
,将其设置为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"]))
解决方案 17:
简单的几行:
import sys
if (sys.version_info.major, sys.version_info.minor) >= (3, 9):
...
else:
...