ฉันจะเปรียบเทียบหมายเลขรุ่นใน Python ได้อย่างไร


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 มีวิธีที่สวยงามเพื่อเปรียบเทียบสตริงรุ่น?

คำตอบ:


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ถ้ารุ่นที่เป็นไปตามและpackaging.version.LegacyVersionถ้าไม่ได้ หลังจะเรียงลำดับก่อนรุ่นที่ถูกต้องเสมอ

หมายเหตุ : บรรจุภัณฑ์ที่เพิ่งได้รับการvendored เข้า 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ไม่มีเอกสารนี่คือเอกสารที่เกี่ยวข้อง


2
ดูเหมือนว่า NormalizedVersion จะไม่มาเหมือนเดิมแทนที่และ LooseVersion และ StrictVersion จะไม่ถูกเลิกใช้อีกต่อไป
Taywee

12
มันเป็นความอัปยศร้องไห้distutils.versionไม่มีเอกสาร
John Y

พบโดยใช้เครื่องมือค้นหาและค้นหาversion.pyรหัสต้นฉบับโดยตรง ใส่อย่างมาก!
Joël

@aywee พวกเขาดีกว่าเพราะพวกเขาไม่ได้มาตรฐาน PEP 440
แกะบินได้

2
imho 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 เพื่อให้แพ็คเกจที่มีน้ำหนักเบาและรวดเร็วยิ่งขึ้น


ก่อนที่จะมีไลบรารีบรรจุภัณฑ์ฟังก์ชันการทำงานนี้ (และยังสามารถเป็นได้) พบได้ใน pkg_resources ซึ่งเป็นแพ็คเกจที่จัดทำโดย setuptools อย่างไรก็ตามสิ่งนี้ไม่เป็นที่ต้องการอีกต่อไปเนื่องจาก setuptools ไม่รับประกันว่าจะติดตั้งอีกต่อไป (มีเครื่องมือบรรจุภัณฑ์อื่น ๆ ) และ pkg_resources นั้นใช้ทรัพยากรค่อนข้างมากเมื่อนำเข้า อย่างไรก็ตามเอกสารและการสนทนาทั้งหมดยังคงเกี่ยวข้อง

จากparse_version()เอกสาร :

แยกสตริงเวอร์ชันของโปรเจ็กต์ตามที่กำหนดโดย PEP 440 ค่าที่ส่งคืนจะเป็นอ็อบเจ็กต์ที่แทนเวอร์ชัน วัตถุเหล่านี้อาจนำมาเปรียบเทียบกันและเรียงลำดับ อัลกอริทึมการเรียงลำดับตามที่กำหนดโดย PEP 440 พร้อมกับเพิ่มเติมว่ารุ่นใดที่ไม่ใช่รุ่น PEP ที่ถูกต้องจะถูกพิจารณาน้อยกว่ารุ่น PEP 440 ที่ถูกต้องและรุ่นที่ไม่ถูกต้องจะเรียงลำดับต่อไปโดยใช้อัลกอริทึมดั้งเดิม

การอ้างอิง "อัลกอริทึมดั้งเดิม" ถูกกำหนดในเอกสารเวอร์ชันเก่ากว่าก่อนที่จะมี PEP 440

Semantically รูปแบบคือการข้ามคร่าว ๆ ระหว่าง distutils ' StrictVersionและLooseVersionคลาส หากคุณให้รุ่นที่จะใช้งานได้StrictVersionพวกเขาจะเปรียบเทียบแบบเดียวกัน มิฉะนั้นการเปรียบเทียบมีมากขึ้นเช่นรูปแบบ "อย่างชาญฉลาด" LooseVersionของ เป็นไปได้ที่จะสร้างรูปแบบการเข้ารหัสเวอร์ชันทางพยาธิวิทยาที่จะหลอก parser นี้ แต่พวกเขาควรจะหายากมากในทางปฏิบัติ

เอกสารให้ตัวอย่างบางส่วน:

หากคุณต้องการให้แน่ใจว่ารูปแบบการกำหนดหมายเลขที่คุณเลือกใช้งานได้ตามที่คุณคิดคุณจะสามารถใช้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
คริส

1
ในกรณีนั้นคุณสามารถลบmap()ฟังก์ชั่นทั้งหมดได้เนื่องจากผลลัพธ์ของสตริงsplit()นั้นมีอยู่แล้ว แต่คุณไม่ต้องการทำเช่นนั้นเพราะเหตุผลทั้งหมดในการเปลี่ยนintเป็นเพื่อให้พวกเขาเปรียบเทียบอย่างถูกต้องเป็นตัวเลข "10" < "2"มิฉะนั้น
ใจดี

6
versiontuple("1.0") > versiontuple("1")นี้จะล้มเหลวบางอย่างเช่น รุ่นเหมือนกัน แต่สิ่งอันดับที่สร้างขึ้น(1,)!=(1,0)
dawg

3
รุ่น 1 และรุ่น 1.0 มีความหมายเหมือนกันหรือไม่ หมายเลขเวอร์ชันไม่ลอย
kindall

12
ไม่มีนี้ควรจะไม่ได้เป็นคำตอบที่ได้รับการยอมรับ โชคดีที่มันไม่ใช่ การแยกวิเคราะห์ที่เชื่อถือได้ของตัวระบุรุ่นไม่ใช่เรื่องเล็กน้อย (หากไม่สามารถทำได้จริง) ในกรณีทั่วไป อย่าบูรณาการล้อแล้วดำเนินการต่อเพื่อทำลายมัน ในฐานะที่เป็นecatmurแสดงให้เห็นข้างต้นdistutils.version.LooseVersionให้ใช้เพียง นั่นคือสิ่งที่มันมีไว้เพื่อ
เซซิลแกง

12

มีอะไรผิดปกติกับการเปลี่ยนสายรุ่นเป็น tuple และไปจากที่นั่น? ดูเหมือนสง่างามพอสำหรับฉัน

>>> (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เป็น tuple ฉันคิดว่าคุณจะพบว่ามันไม่ใช่งานที่สำคัญ ฉันคิดว่ามันเหลือดีกว่าที่จะแพคเกจที่มีประสิทธิภาพที่แปลซึ่งเป็นsetuptools pkg_resources

@TylerGubala นี่เป็นคำตอบที่ยอดเยี่ยมในสถานการณ์ที่คุณรู้ว่าเวอร์ชั่นนั้นเป็นและ "เรียบง่าย" เสมอ pkg_resources เป็นแพ็กเกจขนาดใหญ่และสามารถทำให้ไฟล์ที่รันได้แบบกระจายค่อนข้างพองตัว
Erik Aronesty

@Erik Aronesty ฉันคิดว่าการควบคุมเวอร์ชันภายในของ executables แบบกระจายนั้นค่อนข้างจะขอบขอบเขตของคำถาม แต่โดยทั่วไปฉันก็เห็นด้วยอย่างน้อย ฉันคิดว่ามีบางสิ่งที่จะพูดเกี่ยวกับความสามารถในการนำกลับมาใช้ใหม่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; แหล่งที่มา )
Braham Snyder

5

คุณสามารถใช้แพ็คเกจsemverเพื่อตรวจสอบว่ารุ่นตรงกับรุ่น semantic หรือไม่ตามข้อกำหนดของหรือไม่ สิ่งนี้ไม่เหมือนกับการเปรียบเทียบสองเวอร์ชันจริง แต่เป็นการเปรียบเทียบประเภทหนึ่ง

ตัวอย่างเช่นรุ่น 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 ฉันสามารถรองรับตัวอักษรและตัวเลขใด ๆ ที่ผสมกับตัวเลขด้วยการเติมแต่ละส่วนของเวอร์ชันด้วยศูนย์นำหน้า

แม้ว่าจะไม่ได้สวยเท่าฟังก์ชั่นซับใน แต่ดูเหมือนว่าจะทำงานได้ดีกับหมายเลขรุ่นที่เป็นตัวอักษรและตัวเลข (ตรวจสอบให้แน่ใจว่าได้ตั้ง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
เจด

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

แก้ไข: เพิ่มตัวแปรด้วยการเปรียบเทียบทูเปิล แน่นอนว่าตัวแปรที่มีการเปรียบเทียบทูเปิลคือ nicer แต่ฉันกำลังมองหาตัวแปรที่มีการเปรียบเทียบจำนวนเต็ม


ฉันสงสัยในสิ่งที่สถานการณ์นี้หลีกเลี่ยงการเพิ่มการพึ่งพา? คุณไม่ต้องการไลบรารีบรรจุภัณฑ์ (ใช้โดย setuptools) เพื่อสร้างแพ็กเกจหลาม?
Josiah L.
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.