개발/Python

[파이썬] 버전 번호를 비교하는 방법

MinorMan 2023. 1. 21. 22:40
반응형

<질문>

계란을 추가하기 위해 계란이 포함된 디렉토리를 걷고 있습니다.sys.path. 디렉토리에 동일한 .egg의 두 가지 버전이 있는 경우 최신 버전만 추가하고 싶습니다.

정규 표현식이 있습니다.r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$파일 이름에서 이름과 버전을 추출합니다. 문제는 다음과 같은 문자열인 버전 번호를 비교하는 것입니다.2.3.1.

문자열을 비교하고 있기 때문에 2는 10 이상으로 정렬되지만 버전에는 올바르지 않습니다.

>>> "2.3.1" > "10.1.1"
True

일부 분할, 구문 분석, int로 캐스팅 등을 수행할 수 있으며 결국 해결 방법을 얻게 됩니다. 그러나 이것은 파이썬입니다.not Java. 버전 문자열을 비교하는 우아한 방법이 있습니까?


<답변1>

사용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그렇지 않다면. 후자는 항상 유효한 버전보다 먼저 정렬됩니다.

메모: 최근에 포장이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관련 독스트링입니다.


<답변2>

그만큼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의 원래 코드에서 분리되었습니다.

패키징 라이브러리가 존재하기 전에 이 기능은 setuptools에서 제공하는 패키지인 pkg_resources에서 찾을 수 있었습니다. 그러나 이것은 더 이상 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') 참 >>> parse_version('2.1-rc2') < parse_version('2.1') 참 >>> parse_version('0.6a9dev-r41475') < parse_version(' 0.6a9') 참


<답변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>

있다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')

레거시 버전과 PEP-440 버전 비교.

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

<답변7>

Kindall의 솔루션을 기반으로 내 전체 기능을 게시합니다. 각 버전 섹션을 선행 0으로 채워서 숫자와 혼합된 영숫자 문자를 지원할 수 있었습니다.

확실히 그의 한 줄짜리 함수만큼 예쁘지는 않지만 영숫자 버전 번호와 잘 작동하는 것 같습니다. (반드시 설정하십시오.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버전이 다음을 충족하는지 확인하기 위한 패키지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

<답변9>

새로운 종속성을 추가하지 않는 솔루션을 찾고 있었습니다. 다음(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()

편집: 튜플 비교와 함께 변형을 추가했습니다. 물론 튜플 비교가 있는 변형이 더 좋지만 정수 비교가 있는 변형을 찾고 있었습니다.


<답변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>

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

<답변12>

세 가지 버전 번호를 비교하기 위한 간단한 코드입니다. 여기서 모든 쌍에 대해 문자열 비교가 실패합니다.

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 길이를 제공합니다permutations반복 가능합니다. 예를 들어

list(permutations('ABC', 2))

우리에게 주어지다 [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')].


<답변13>

표준과 유사strverscmp그리고 유사this solution by Mark Byers그러나 빈 케이스를 피하기 위해 split 대신 findall을 사용합니다.

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

<답변14>

다음은 시맨틱 버전이 "깨끗하다"고 가정하면 작동하는 것입니다(예: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"]))

<답변15>

라이브러리 버전에서 필터를 만들려면 다음을 사용할 수 있습니다.__version__속성(여기서는 jwt 라이브러리의 예):

from packaging import version
import jwt

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


<답변16>

간단한 몇 줄짜리:

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