Làm cách nào để so sánh số phiên bản trong Python?


235

Tôi đang đi bộ một thư mục có chứa trứng để thêm những quả trứng vào sys.path . Nếu có hai phiên bản của cùng một .egg trong thư mục, tôi muốn chỉ thêm phiên bản mới nhất.

Tôi có một biểu thức chính quy r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$để trích xuất tên và phiên bản từ tên tệp. Vấn đề là so sánh số phiên bản, đó là một chuỗi như2.3.1 .

Vì tôi đang so sánh các chuỗi, 2 loại trên 10, nhưng điều đó không đúng với các phiên bản.

>>> "2.3.1" > "10.1.1"
True

Tôi có thể thực hiện một số phân tách, phân tích cú pháp, truyền tới int, v.v., và cuối cùng tôi sẽ có một cách giải quyết. Nhưng đây là Python, không phải Java . Có một cách thanh lịch để so sánh các chuỗi phiên bản?

Câu trả lời:


367

Sử dụng 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.parselà một tiện ích của bên thứ ba nhưng được sử dụng bởi setuptools (vì vậy bạn có thể đã cài đặt nó) và phù hợp với PEP 440 hiện tại ; nó sẽ trả về packaging.version.Versionnếu phiên bản tuân thủ và packaging.version.LegacyVersionnếu không. Cái sau sẽ luôn luôn sắp xếp trước các phiên bản hợp lệ.

Lưu ý : bao bì gần đây đã được đưa vào setuptools .


Một thay thế cổ xưa vẫn được sử dụng bởi rất nhiều phần mềm là distutils.version, được xây dựng nhưng không có giấy tờ và chỉ tuân thủ PEP 386 thay thế ;

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

Như bạn có thể thấy, nó thấy các phiên bản PEP 440 hợp lệ là không nghiêm ngặt và do đó không phù hợp với quan niệm của Python hiện đại về phiên bản hợp lệ là gì.

Như distutils.versionkhông có giấy tờ, đây là các tài liệu liên quan.


2
Có vẻ như NormalizedVersion sẽ không xuất hiện, vì nó đã được thay thế, và LooseVersion và StrictVersion do đó không còn bị phản đối nữa.
Taywee

12
Đó là một sự xấu hổ khóc distutils.versionlà không có giấy tờ.
John Y

tìm thấy nó bằng cách sử dụng công cụ tìm kiếm và tìm trực tiếp version.pymã nguồn. Rất độc đáo đặt!
Joël

@Taywee họ tốt hơn, vì họ không tuân thủ PEP 440.
cừu bay

2
imho packaging.version.parsekhông thể tin tưởng để so sánh các phiên bản. Hãy thử parse('1.0.1-beta.1') > parse('1.0.0')ví dụ.
Trondh

104

Các bao bì thư viện chứa các tiện ích cho làm việc với các phiên bản và chức năng đóng gói-liên quan khác. Điều này thực hiện PEP 0440 - Nhận dạng phiên bản và cũng có thể phân tích các phiên bản không tuân theo PEP. Nó được sử dụng bởi pip và các công cụ Python phổ biến khác để cung cấp phân tích và so sánh phiên bản.

$ pip install packaging
from packaging.version import parse as parse_version
version = parse_version('1.0.3.dev')

Điều này đã được tách ra khỏi mã ban đầu trong setuptools và pkg_resource để cung cấp gói nhẹ hơn và nhanh hơn.


Trước khi thư viện đóng gói tồn tại, chức năng này đã (và vẫn có thể) được tìm thấy trong pkg_resource, một gói được cung cấp bởi setuptools. Tuy nhiên, điều này không còn được ưa thích vì setuptools không còn được đảm bảo để cài đặt (các công cụ đóng gói khác tồn tại) và pkg_resours sử dụng khá nhiều tài nguyên khi được nhập. Tuy nhiên, tất cả các tài liệu và thảo luận vẫn có liên quan.

Từ các parse_version()tài liệu :

Phân tích chuỗi phiên bản của dự án như được định nghĩa bởi PEP 440. Giá trị được trả về sẽ là một đối tượng đại diện cho phiên bản. Các đối tượng này có thể được so sánh với nhau và được sắp xếp. Thuật toán sắp xếp được xác định bởi PEP 440 với việc thêm bất kỳ phiên bản nào không phải là phiên bản PEP 440 hợp lệ sẽ được coi là ít hơn bất kỳ phiên bản PEP 440 hợp lệ nào và các phiên bản không hợp lệ sẽ tiếp tục sắp xếp bằng thuật toán gốc.

"Thuật toán gốc" được tham chiếu được xác định trong các phiên bản cũ hơn của tài liệu, trước khi PEP 440 tồn tại.

Về mặt ngữ nghĩa, định dạng là một giao thoa thô giữa các lớp StrictVersionLooseVersioncác lớp; nếu bạn cung cấp cho nó các phiên bản sẽ hoạt động cùng StrictVersionthì chúng sẽ so sánh theo cùng một cách. Mặt khác, so sánh giống như một hình thức "thông minh hơn" LooseVersion. Có thể tạo các lược đồ mã hóa phiên bản bệnh lý sẽ đánh lừa trình phân tích cú pháp này, nhưng chúng rất hiếm trong thực tế.

Các tài liệu cung cấp một số ví dụ:

Nếu bạn muốn chắc chắn rằng sơ đồ đánh số đã chọn của bạn hoạt động theo cách bạn nghĩ, bạn có thể sử dụng pkg_resources.parse_version() chức năng để so sánh các số phiên bản khác nhau:

>>> 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
Các câu trả lời khác nằm trong thư viện tiêu chuẩn và tuân theo các tiêu chuẩn PEP.
Chris

1
Trong trường hợp đó, bạn có thể loại bỏ map()hoàn toàn hàm, vì kết quả split()đã có chuỗi. Nhưng dù sao bạn cũng không muốn làm điều đó, bởi vì toàn bộ lý do để thay đổi chúng intlà để chúng so sánh đúng như số. Nếu không "10" < "2".
loại

6
Điều này sẽ thất bại cho một cái gì đó như versiontuple("1.0") > versiontuple("1"). Các phiên bản giống nhau, nhưng các bộ dữ liệu được tạo(1,)!=(1,0)
dawg

3
Theo nghĩa nào thì phiên bản 1 và phiên bản 1.0 giống nhau? Số phiên bản không nổi.
kindall

12
Không, đây không phải là câu trả lời được chấp nhận. Rất may, nó không phải là. Phân tích cú pháp đáng tin cậy của các trình xác định phiên bản là không tầm thường (nếu không thực tế là không khả thi) trong trường hợp chung. Đừng phát minh lại bánh xe và sau đó tiến hành phá vỡ nó. Như ecatmur gợi ý ở trên , chỉ cần sử dụng distutils.version.LooseVersion. Đó là những gì nó ở đó cho.
Cecil Curry

12

Có gì sai khi chuyển chuỗi phiên bản thành một tuple và đi từ đó? Có vẻ đủ thanh lịch đối với tôi

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

Giải pháp của @ kindall là một ví dụ nhanh về việc mã sẽ trông tốt như thế nào.


1
Tôi nghĩ rằng câu trả lời này có thể được mở rộng bằng cách cung cấp mã thực hiện chuyển đổi chuỗi PEP440 thành một tuple. Tôi nghĩ bạn sẽ thấy nó không phải là một nhiệm vụ tầm thường. Tôi nghĩ rằng tốt hơn hết là để gói thực hiện bản dịch setuptoolsđó, đó là pkg_resources.

@TylerGubala đây là một câu trả lời tuyệt vời trong các tình huống mà bạn biết rằng phiên bản này luôn luôn sẽ "đơn giản". pkg_resource là một gói lớn và có thể khiến một tệp thực thi phân tán trở nên khá khó chịu.
Erik Aronesty

@Erik Aronesty Tôi nghĩ rằng kiểm soát phiên bản bên trong các tệp thực thi phân tán có phần nào đó thuộc phạm vi của câu hỏi, nhưng tôi đồng ý, nói chung là ít nhất. Tôi nghĩ mặc dù có điều gì đó để nói về khả năng sử dụng lại pkg_resources, và các giả định về đặt tên gói đơn giản có thể không phải lúc nào cũng lý tưởng.

Nó hoạt động tuyệt vời để đảm bảo sys.version_info > (3, 6)hoặc bất cứ điều gì.
Gqqnbig

7

gói đóng gói có sẵn, sẽ cho phép bạn so sánh các phiên bản theo PEP-440 , cũng như các phiên bản cũ.

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

Hỗ trợ phiên bản kế thừa:

>>> LegacyVersion('1.2.8.5-5-gdeadbeef')
<LegacyVersion('1.2.8.5-5-gdeadbeef')>

So sánh phiên bản cũ với phiên bản PEP-440.

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

3
Đối với những người thắc mắc về sự khác biệt giữa packaging.version.Versionpackaging.version.parse: "[ version.parse] lấy một chuỗi phiên bản và sẽ phân tích nó như là Versionnếu phiên bản là phiên bản PEP 440 hợp lệ, nếu không, nó sẽ phân tích thành một chuỗi LegacyVersion." (trong khi version.Versionsẽ tăng InvalidVersion; nguồn )
Braham Snyder

5

Bạn có thể sử dụng gói semver để xác định xem một phiên bản có thỏa mãn yêu cầu phiên bản ngữ nghĩa hay không . Điều này không giống như so sánh hai phiên bản thực tế, nhưng là một loại so sánh.

Ví dụ: phiên bản 3.6.0 + 1234 phải giống với 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

Đăng chức năng đầy đủ của tôi dựa trên giải pháp của Kindall. Tôi đã có thể hỗ trợ bất kỳ ký tự chữ và số nào được trộn lẫn với các số bằng cách đệm từng phần phiên bản với các số 0 đứng đầu.

Mặc dù chắc chắn không đẹp như chức năng một lớp lót của anh ta, nó dường như hoạt động tốt với các số phiên bản alpha-số. (Chỉ cần đảm bảo đặt zfill(#)giá trị phù hợp nếu bạn có chuỗi dài trong hệ thống phiên bản của mình.)

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

Cách mà setuptoolsnó làm, nó sử dụng pkg_resources.parse_versionchức năng. Nó phải là PEP440 tuân thủ .

Thí dụ:

#! /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_resourceslà một phần của setuptools, mà phụ thuộc vào packaging. Xem các câu trả lời khác thảo luận packaging.version.parse, trong đó có một triển khai giống hệt nhau pkg_resources.parse_version.
Jed

0

Tôi đang tìm kiếm một giải pháp sẽ không thêm bất kỳ phụ thuộc mới nào. Kiểm tra giải pháp (Python 3) sau:

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

EDIT: thêm biến thể với so sánh tuple. Tất nhiên biến thể với so sánh tuple là đẹp hơn, nhưng tôi đã tìm kiếm biến thể với so sánh số nguyên


Tôi tò mò trong tình huống nào điều này tránh thêm phụ thuộc? Bạn không cần thư viện đóng gói (được sử dụng bởi setuptools) để tạo gói python?
Josiah L.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.