簡體   English   中英

如何比較 Python 中的版本號?

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

我正在遍歷一個包含雞蛋的目錄,以將這些雞蛋添加到sys.path中。 如果目錄中有兩個版本的 same.egg,我只想添加最新的一個。

我有一個正則表達式r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$從文件名中提取名稱和版本。問題是比較版本號,它是一個像2.3.1這樣的字符串。

由於我比較的是字符串,2 排序高於 10,但這對於版本來說是不正確的。

>>> "2.3.1" > "10.1.1"
True

我可以做一些拆分、解析、轉換為 int 等,最終我會得到一個解決方法。 但這是 Python, 而不是 Java 有沒有一種優雅的方法來比較版本字符串?

使用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.LegacyVersion如果不兼容,則返回packaging.version.Version 后者將始終排在有效版本之前。

注意:包裝最近已被供應到 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 存在之前,引用的“原始算法”是在舊版本的文檔中定義的。

從語義StrictVersion ,該格式是 distutils 的StrictVersionLooseVersion類之間的粗略交叉; 如果您為其提供可與StrictVersion一起使用的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
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

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

將版本字符串轉換為元組並從那里開始有什么問題? 對我來說似乎足夠優雅

>>> (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 的解決方案是代碼外觀的快速示例。

包裝封裝,這將讓你的版本比較,按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

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

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

根據 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

我正在尋找一種不會添加任何新依賴項的解決方案。 查看以下(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()

編輯:添加了帶有元組比較的變體。 當然,元組比較的變體更好,但我正在尋找整數比較的變體

... 回到簡單 ... 對於簡單的腳本,您可以使用:

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

假設您的語義版本是“干凈的”(例如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']

要獲取最新版本,您只需選擇列表versions[-1]的最后一個元素,或使用sorted()reverse屬性進行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"]))

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

簡單的幾行:

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

我會更多地選擇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)

這就是我得到的:

3.4.5松散

(10,5,2)和與touples

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM