파이썬에서 버전 번호를 어떻게 비교합니까?


236

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

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

문자열을 비교하고 있기 때문에 2는 10보다 높지만 버전에는 맞지 않습니다.

>>> "2.3.1" > "10.1.1"
True

분할, 파싱, int로 캐스팅 등을 할 수 있었고 결국 해결 방법을 얻었습니다. 그러나 이것은 Java가 아닌 Python 입니다. 버전 문자열을 비교할 수있는 우아한 방법이 있습니까?

답변:


367

사용하십시오 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버전이 호환 되는 경우 a를 반환하고 그렇지 않은 경우 a를 반환합니다 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 버전은“엄격하지 않은”것으로 보이므로 현대 파이썬의 유효한 버전에 대한 개념과 일치하지 않습니다.

으로 distutils.version문서화되고, 여기에 '관련 문서화 문자열을이야.


2
대체 된 NormalizedVersion처럼 보이지 않으므로 LooseVersion 및 StrictVersion은 더 이상 사용되지 않습니다.
Taywee

12
우는 부끄러움 distutils.version이 문서화되어 있지 않습니다.
John Y

검색 엔진을 사용하여 version.py소스 코드를 직접 찾았습니다 . 아주 멋지게 넣어!
Joël

@Taywee는 PEP 440을 준수하지 않기 때문에 더 좋습니다.
날으는 양

2
임호 packaging.version.parse 는 버전을 비교할 수 없습니다. parse('1.0.1-beta.1') > parse('1.0.0')예를 들어 보십시오 .
Trondh

104

포장 라이브러리를위한 유틸리티가 포함되어 버전 작업 및 기타 포장 관련 기능을 제공합니다. 이것은 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이 존재하기 전에 이전 버전의 문서에서 정의되었습니다.

의미 적으로, 형식은 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

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

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

10
다른 답변은 표준 라이브러리에 있으며 PEP 표준을 따릅니다.
Chris

1
이 경우 당신은 제거 할 수 map()의 결과로, 완전히 기능 split()입니다 이미 문자열. 그러나 당신은 어쨌든 그렇게하고 싶지 않습니다. 왜냐하면 그것들을 변경 해야하는 이유 int는 숫자로 올바르게 비교하기 때문입니다. 그렇지 않으면 "10" < "2".
kindall

6
이것은 다음과 같은 이유로 실패합니다 versiontuple("1.0") > versiontuple("1"). 버전은 동일하지만 튜플이 생성(1,)!=(1,0)
임마

3
어떤 의미에서 버전 1과 버전 1.0은 동일합니까? 버전 번호는 부동이 아닙니다.
kindall

12
아니요, 이것은 정답 이 아닙니다 . 고맙게도 그렇지 않습니다. 버전 지정자의 안정적인 구문 분석은 일반적으로 사소하지 않습니다 (실제로는 불가능할 경우). 바퀴를 재발 명하지 말고 계속 깰 수 있습니다. 으로 ecatmur가 제안 , 단지 사용 distutils.version.LooseVersion. 그것이 거기에있는 것입니다.
Cecil Curry

12

버전 문자열을 튜플로 변환하고 거기에서 나가는 것은 무엇이 문제입니까? 나에게 충분히 우아해 보인다

>>> (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의 솔루션은 코드가 얼마나 잘 보이는지에 대한 간단한 예입니다.


1
PEP440 문자열을 튜플로 변환하는 코드를 제공 하여이 답변을 확장 할 수 있다고 생각합니다 . 나는 그것이 사소한 작업이 아니라는 것을 알게 될 것이라고 생각합니다. 에 대한 번역을 수행하는 패키지에 맡기는 setuptools것이 pkg_resources좋습니다.

@TylerGubala 이것은 버전이 항상 "단순"하다는 것을 알고있는 상황에서 훌륭한 해답입니다. pkg_resources는 큰 패키지이며 분산 실행 파일이 다소 부 풀릴 수 있습니다.
Erik Aronesty

@Erik Aronesty 분산 실행 파일 내부의 버전 제어는 질문의 범위를 다소 벗어난 것으로 생각하지만 적어도 일반적으로 동의합니다. 나는의 재사용성에 대해 말할 것이 있고 pkg_resources간단한 패키지 명명의 가정이 항상 이상적이지는 않을 것이라고 생각합니다.

그것은 sys.version_info > (3, 6)무엇이든 확인하는 데 효과적 입니다.
Gqqnbig

7

포장 이 따라 버전을 비교 할 수 있도록 가능한 패키지, 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

3
차이 궁금 분들 packaging.version.Versionpackaging.version.parse"[ version.parse] 버전 스트링을 취하고로 파싱 Version그렇지는로 파싱, 버전이 유효한 PEP (440) 버전 인 경우 LegacyVersion." (반면에 version.Version올릴 것이다 InvalidVersion; 소스 )
브라만 스나이더

5

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

3

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

2

그렇게하는 방식으로 함수를 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

0

새로운 종속성을 추가하지 않는 솔루션을 찾고있었습니다. 다음 (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에서 사용)가 필요하지 않습니까?
Josiah L.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.