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


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 StrictVersion와 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

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.Version과 packaging.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.