Python, tôi có nên triển khai toán tử __ne __ () dựa trên __eq__ không?


98

Tôi có một lớp mà tôi muốn ghi đè __eq__()toán tử. Có vẻ hợp lý rằng tôi cũng nên ghi đè __ne__()toán tử, nhưng liệu có hợp lý khi triển khai __ne__dựa trên __eq__như vậy không?

class A:
    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return not self.__eq__(other)

Hoặc có điều gì đó mà tôi thiếu với cách Python sử dụng các toán tử này khiến điều này không phải là một ý tưởng hay?

Câu trả lời:


57

Vâng, điều đó hoàn toàn tốt. Trên thực tế, tài liệu thúc giục bạn xác định __ne__khi bạn xác định __eq__:

Không có mối quan hệ ngụ ý nào giữa các toán tử so sánh. Sự thật của x==ykhông có nghĩa là điều đó x!=y là sai. Theo đó, khi định nghĩa __eq__(), người ta cũng nên xác định __ne__()sao cho các toán tử sẽ hoạt động như mong đợi.

Trong rất nhiều trường hợp (chẳng hạn như trường hợp này), nó sẽ đơn giản như phủ định kết quả của __eq__, nhưng không phải lúc nào cũng vậy.


12
đây là câu trả lời đúng (dưới đây, bởi @ aaron-hall). Tài liệu bạn đã trích dẫn không khuyến khích bạn triển khai __ne__bằng cách sử dụng __eq__, chỉ là bạn triển khai nó.
guyarad

2
@guyarad: Thực ra câu trả lời của Aaron vẫn hơi sai do chưa phân quyền hợp lý; thay vì coi một NotImplementedtrả về từ một phía như một dấu hiệu để ủy quyền cho __ne__phía bên kia, not self == otherlà (giả sử toán hạng __eq__không biết cách so sánh toán hạng khác) ủy quyền ngầm cho __eq__từ phía bên kia, sau đó đảo ngược nó. Đối với các kiểu lạ, ví dụ như các trường của SQLAlchemy ORM, điều này gây ra sự cố .
ShadowRanger

1
Lời chỉ trích của ShadowRanger sẽ chỉ áp dụng cho các trường hợp rất bệnh lý (IMHO) và được giải đáp đầy đủ trong câu trả lời của tôi dưới đây.
Aaron Hall

1
Các tài liệu mới hơn (ít nhất là 3.7, thậm chí có thể sớm hơn) __ne__tự động ủy quyền __eq__và trích dẫn trong câu trả lời này không còn tồn tại trong tài liệu. Tóm lại, việc chỉ thực hiện __eq__và để __ne__ủy quyền là điều hoàn toàn khó hiểu .
bluesummers

132

Python, tôi có nên triển __ne__()khai toán tử dựa trên __eq__không?

Câu trả lời ngắn gọn: Đừng triển khai nó, nhưng nếu bạn phải, hãy sử dụng ==, không__eq__

Trong Python 3, !=là phủ định của ==theo mặc định, vì vậy bạn thậm chí không bắt buộc phải viết một __ne__và tài liệu không còn quan tâm đến việc viết một.

Nói chung, đối với mã chỉ dành cho Python 3, không viết một mã trừ khi bạn cần làm lu mờ việc triển khai gốc, ví dụ như đối với một đối tượng nội trang.

Đó là, hãy ghi nhớ nhận xét của Raymond Hettinger :

Các __ne__phương pháp tự động sau từ __eq__chỉ nếu __ne__chưa được định nghĩa trong một lớp cha. Vì vậy, nếu bạn đang kế thừa từ một nội dung, tốt nhất là ghi đè cả hai.

Nếu bạn cần mã của mình để hoạt động trong Python 2, hãy làm theo khuyến nghị cho Python 2 và nó sẽ hoạt động tốt trong Python 3.

Trong Python 2, bản thân Python không tự động triển khai bất kỳ hoạt động nào theo nghĩa khác - do đó, bạn nên định nghĩa các __ne__điều khoản ==thay vì __eq__. VÍ DỤ

class A(object):
    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return not self == other # NOT `return not self.__eq__(other)`

Xem bằng chứng rằng

  • __ne__()nhà điều hành triển khai dựa trên __eq__
  • hoàn toàn không triển khai __ne__bằng Python 2

cung cấp hành vi không chính xác trong phần minh họa bên dưới.

Câu trả lời dài

Các tài liệu cho Python 2 nói:

Không có mối quan hệ ngụ ý nào giữa các toán tử so sánh. Sự thật của x==ykhông có nghĩa là điều đó x!=ylà sai. Theo đó, khi định nghĩa __eq__(), người ta cũng nên xác định __ne__()sao cho các toán tử sẽ hoạt động như mong đợi.

Vì vậy, điều đó có nghĩa là nếu chúng ta xác định __ne__theo nghịch đảo của __eq__, chúng ta có thể có được hành vi nhất quán.

Phần này của tài liệu đã được cập nhật cho Python 3:

Theo mặc định, __ne__()ủy quyền __eq__()và đảo ngược kết quả trừ khi nó được NotImplemented.

và trong phần "có gì mới" , chúng tôi thấy hành vi này đã thay đổi:

  • !=bây giờ trả về ngược lại ==, trừ khi ==trả về NotImplemented.

Đối với việc triển khai __ne__, chúng tôi thích sử dụng ==toán tử thay vì sử dụng __eq__phương thức trực tiếp để nếu self.__eq__(other)một lớp con trả về NotImplementedkiểu được kiểm tra, Python sẽ kiểm tra một cách thích hợp other.__eq__(self) Từ tài liệu :

Đối NotImplementedtượng

Loại này có một giá trị duy nhất. Có một đối tượng duy nhất có giá trị này. Đối tượng này được truy cập thông qua tên có sẵn NotImplemented. Phương pháp số và phương pháp so sánh phong phú có thể trả về giá trị này nếu chúng không triển khai hoạt động cho các toán hạng được cung cấp. (Sau đó, trình thông dịch sẽ thử hoạt động được phản ánh hoặc một số dự phòng khác, tùy thuộc vào toán tử.) Giá trị chân lý của nó là true.

Khi đưa một toán tử so sánh phong phú, nếu họ không cùng loại, Python kiểm tra nếu otherlà một subtype, và nếu nó có mà nhà điều hành được xác định, nó sử dụng otherphương pháp 's đầu tiên (nghịch đảo cho <, <=, >=>). Nếu NotImplementedđược trả về, thì nó sử dụng phương thức ngược lại. (Nó không kiểm tra cùng một phương pháp hai lần.) Sử dụng ==toán tử cho phép logic này diễn ra.


Kỳ vọng

Về mặt ngữ nghĩa, bạn nên triển khai __ne__về mặt kiểm tra tính bình đẳng vì người dùng trong lớp của bạn sẽ mong đợi các chức năng sau đây là tương đương cho tất cả các trường hợp của A.:

def negation_of_equals(inst1, inst2):
    """always should return same as not_equals(inst1, inst2)"""
    return not inst1 == inst2

def not_equals(inst1, inst2):
    """always should return same as negation_of_equals(inst1, inst2)"""
    return inst1 != inst2

Có nghĩa là, cả hai hàm trên phải luôn trả về cùng một kết quả. Nhưng điều này phụ thuộc vào lập trình viên.

Thể hiện hành vi không mong muốn khi xác định __ne__dựa trên __eq__:

Đầu tiên thiết lập:

class BaseEquatable(object):
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return isinstance(other, BaseEquatable) and self.x == other.x

class ComparableWrong(BaseEquatable):
    def __ne__(self, other):
        return not self.__eq__(other)

class ComparableRight(BaseEquatable):
    def __ne__(self, other):
        return not self == other

class EqMixin(object):
    def __eq__(self, other):
        """override Base __eq__ & bounce to other for __eq__, e.g. 
        if issubclass(type(self), type(other)): # True in this example
        """
        return NotImplemented

class ChildComparableWrong(EqMixin, ComparableWrong):
    """__ne__ the wrong way (__eq__ directly)"""

class ChildComparableRight(EqMixin, ComparableRight):
    """__ne__ the right way (uses ==)"""

class ChildComparablePy3(EqMixin, BaseEquatable):
    """No __ne__, only right in Python 3."""

Khởi tạo các phiên bản không tương đương:

right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)

Hành vi mong đợi:

(Lưu ý: mặc dù mọi khẳng định thứ hai của mỗi điều dưới đây là tương đương và do đó dư thừa về mặt logic đối với khẳng định trước nó, tôi đưa chúng vào để chứng minh rằng thứ tự không quan trọng khi một cái là lớp con của cái kia. )

Các trường hợp này đã được __ne__triển khai với ==:

assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1

Các trường hợp này, thử nghiệm trong Python 3, cũng hoạt động chính xác:

assert not right_py3_1 == right_py3_2
assert not right_py3_2 == right_py3_1
assert right_py3_1 != right_py3_2
assert right_py3_2 != right_py3_1

Và nhớ lại rằng những điều này đã được __ne__thực hiện với __eq__- trong khi đây là hành vi được mong đợi, việc triển khai không chính xác:

assert not wrong1 == wrong2         # These are contradicted by the
assert not wrong2 == wrong1         # below unexpected behavior!

Hành vi không mong muốn:

Lưu ý rằng so sánh này mâu thuẫn với so sánh ở trên ( not wrong1 == wrong2).

>>> assert wrong1 != wrong2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

và,

>>> assert wrong2 != wrong1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

Đừng bỏ qua __ne__trong Python 2

Để có bằng chứng cho thấy bạn không nên bỏ qua việc triển khai __ne__trong Python 2, hãy xem các đối tượng tương đương sau:

>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True

Kết quả trên nên được False!

Nguồn Python 3

Triển khai CPython mặc định cho __ne__typeobject.ctrongobject_richcompare :

case Py_NE:
    /* By default, __ne__() delegates to __eq__() and inverts the result,
       unless the latter returns NotImplemented. */
    if (Py_TYPE(self)->tp_richcompare == NULL) {
        res = Py_NotImplemented;
        Py_INCREF(res);
        break;
    }
    res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
    if (res != NULL && res != Py_NotImplemented) {
        int ok = PyObject_IsTrue(res);
        Py_DECREF(res);
        if (ok < 0)
            res = NULL;
        else {
            if (ok)
                res = Py_False;
            else
                res = Py_True;
            Py_INCREF(res);
        }
    }
    break;

Nhưng __ne__sử dụng mặc định __eq__?

__ne__Chi tiết triển khai mặc định của Python 3 ở cấp C sử dụng __eq__vì cấp cao hơn ==( PyObject_RichCompare ) sẽ kém hiệu quả hơn - và do đó nó cũng phải xử lý NotImplemented.

Nếu __eq__được triển khai chính xác, thì phủ định của ==cũng đúng - và nó cho phép chúng tôi tránh các chi tiết triển khai cấp thấp trong của chúng tôi __ne__.

Sử dụng ==cho phép chúng ta giữ logic cấp thấp trong một nơi, và tránh việc giải quyết NotImplementedtrong __ne__.

Người ta có thể giả định không chính xác rằng nó ==có thể trở lại NotImplemented.

Nó thực sự sử dụng logic tương tự như việc triển khai mặc định __eq__, kiểm tra danh tính (xem do_richcompare và bằng chứng của chúng tôi bên dưới)

class Foo:
    def __ne__(self, other):
        return NotImplemented
    __eq__ = __ne__

f = Foo()
f2 = Foo()

Và so sánh:

>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True

Hiệu suất

Đừng nghe lời tôi, hãy xem những gì hiệu quả hơn:

class CLevel:
    "Use default logic programmed in C"

class HighLevelPython:
    def __ne__(self, other):
        return not self == other

class LowLevelPython:
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

def c_level():
    cl = CLevel()
    return lambda: cl != cl

def high_level_python():
    hlp = HighLevelPython()
    return lambda: hlp != hlp

def low_level_python():
    llp = LowLevelPython()
    return lambda: llp != llp

Tôi nghĩ rằng những con số hiệu suất này tự nói lên:

>>> import timeit
>>> min(timeit.repeat(c_level()))
0.09377292497083545
>>> min(timeit.repeat(high_level_python()))
0.2654011140111834
>>> min(timeit.repeat(low_level_python()))
0.3378178110579029

Điều này có ý nghĩa khi bạn xem xét điều đó low_level_pythonđang thực hiện logic trong Python mà nếu không sẽ được xử lý ở cấp C.

Trả lời một số nhà phê bình

Một người trả lời khác viết:

Việc thực hiện phương thức not self == othercủa Aaron Hall __ne__là không chính xác vì nó không bao giờ có thể trả về NotImplemented( not NotImplementedFalse) và do đó __ne__phương thức có ưu tiên không bao giờ có thể trở lại __ne__phương thức không có ưu tiên.

Không __ne__bao giờ trở lại NotImplementedkhông làm cho nó không chính xác. Thay vào đó, chúng tôi xử lý mức độ ưu tiên với NotImplementedthông qua kiểm tra sự bình đẳng với ==. Giả sử ==được triển khai chính xác, chúng tôi đã hoàn tất.

not self == othertừng là __ne__phương pháp triển khai Python 3 mặc định nhưng đó là một lỗi và nó đã được sửa chữa trong Python 3.4 vào tháng 1 năm 2015, như ShadowRanger nhận thấy (xem sự cố # 21408).

Vâng, hãy giải thích điều này.

Như đã lưu ý trước đó, Python 3 theo mặc định xử lý __ne__bằng cách kiểm tra đầu tiên nếu self.__eq__(other)trả về NotImplemented(một singleton) - sẽ được kiểm tra với isvà trả về nếu có, nếu không, nó sẽ trả về nghịch đảo. Đây là logic được viết dưới dạng hỗn hợp lớp:

class CStyle__ne__:
    """Mixin that provides __ne__ functionality equivalent to 
    the builtin functionality
    """
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

Điều này là cần thiết để đảm bảo tính đúng đắn cho API Python cấp C và nó đã được giới thiệu trong Python 3, khiến

dư thừa. Tất cả các __ne__phương pháp có liên quan đã bị loại bỏ, bao gồm cả những phương pháp thực hiện kiểm tra của riêng chúng cũng như những phương pháp ủy quyền __eq__trực tiếp hoặc thông qua ==- và ==là cách phổ biến nhất để làm như vậy.

Đối xứng có quan trọng không?

Phê bình dai dẳng của chúng tôi cung cấp một ví dụ bệnh lý để làm cho trường hợp để xử lý NotImplementedtrong __ne__, định giá đối xứng trên hết. Hãy thử lập luận bằng một ví dụ rõ ràng:

class B:
    """
    this class has no __eq__ implementation, but asserts 
    any instance is not equal to any other object
    """
    def __ne__(self, other):
        return True

class A:
    "This class asserts instances are equivalent to all other objects"
    def __eq__(self, other):
        return True

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, False, True)

Vì vậy, theo logic này, để duy trì tính đối xứng, chúng ta cần viết __ne__phiên bản Python phức tạp , bất kể là phiên bản nào.

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return True
    def __ne__(self, other):
        result = other.__eq__(self)
        if result is NotImplemented:
            return NotImplemented
        return not result

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, True, True)

Rõ ràng chúng ta không nên để ý rằng những trường hợp này vừa bằng nhau vừa không bằng nhau.

Tôi đề xuất rằng tính đối xứng ít quan trọng hơn giả định về mã hợp lý và làm theo lời khuyên của tài liệu.

Tuy nhiên, nếu A có một cách triển khai hợp lý __eq__, thì chúng ta vẫn có thể làm theo hướng của tôi ở đây và chúng ta sẽ vẫn có sự đối xứng:

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return False         # <- this boolean changed... 

>>> A() == B(), B() == A(), A() != B(), B() != A()
(False, False, True, True)

Phần kết luận

Đối với mã tương thích Python 2, hãy sử dụng ==để triển khai __ne__. Nó là nhiều hơn:

  • chính xác
  • đơn giản
  • người biểu diễn

Trong Python 3 chỉ, sử dụng phủ định ở mức độ thấp về mức độ C - nó thậm chí còn nhiều hơn đơn giản và performant (mặc dù các lập trình viên có trách nhiệm xác định rằng nó là đúng ).

Một lần nữa, không viết logic cấp thấp bằng Python cấp cao.


3
Những ví dụ xuất sắc! Một phần của sự ngạc nhiên là thứ tự của các toán hạng hoàn toàn không quan trọng , không giống như một số phương pháp ma thuật với phản xạ "bên phải" của chúng. Để lặp lại phần mà tôi đã bỏ lỡ (và điều này khiến tôi tốn rất nhiều thời gian): Phương pháp so sánh phong phú của lớp con được thử trước, bất kể mã có lớp cha hay lớp con ở bên trái của toán tử. Đây là lý do tại sao bạn a1 != c2trả về False--- nó không chạy a1.__ne__, nhưng c2.__ne__, điều này đã phủ nhận phương thức của mixin __eq__ . Vì NotImplementedlà sự thật, not NotImplementedFalse.
Kevin J. Chase

2
Các bản cập nhật gần đây của bạn đã thể hiện thành công lợi thế về hiệu suất not (self == other), nhưng không ai tranh cãi rằng nó không nhanh (dù sao thì, nhanh hơn bất kỳ tùy chọn nào khác trên Py2). Vấn đề là nó sai trong một số trường hợp; Bản thân Python đã từng làm not (self == other), nhưng đã thay đổi vì nó không chính xác khi có các lớp con tùy ý . Nhanh nhất đến câu trả lời sai vẫn sai .
ShadowRanger

1
Ví dụ cụ thể là loại thực sự không quan trọng. Vấn đề là, trong việc thực hiện của bạn, hành vi của bạn __ne__ủy quyền cho __eq__(của cả hai bên nếu cần thiết), nhưng nó không bao giờ lùi về __ne__phía bên kia ngay cả khi cả hai đều __eq__"bỏ cuộc". Đúng __ne__ủy quyền cho chính__eq__, nhưng nếu điều đó quay trở lại NotImplemented, nó sẽ quay trở lại để chuyển sang phía bên kia __ne__, thay vì đảo ngược bên kia __eq__(vì bên kia có thể không chọn tham gia ủy quyền một cách rõ ràng __eq__và bạn không nên đang đưa ra quyết định cho nó).
ShadowRanger

1
@AaronHall: Khi xem xét lại điều này hôm nay, tôi không nghĩ rằng việc triển khai của bạn có vấn đề đối với các lớp con thông thường (sẽ rất phức tạp nếu làm cho nó bị hỏng và lớp con, được cho là có đầy đủ kiến ​​thức về cha mẹ, nên có thể tránh được nó ). Nhưng tôi chỉ đưa ra một ví dụ không phức tạp trong câu trả lời của mình. Các trường hợp không bệnh lý là ORM SQLAlchemy, nơi không phải __eq__và cũng không __ne__trở lại một trong hai Truehoặc False, mà đúng hơn là một đối tượng proxy (điều đó xảy ra là "truthy"). Việc triển khai không chính xác __ne__có nghĩa là thứ tự quan trọng đối với việc so sánh (bạn chỉ nhận được một proxy trong một lần đặt hàng).
ShadowRanger

1
Để rõ ràng, trong 99% (hoặc có thể là 99,999%) trường hợp, giải pháp của bạn ổn và (rõ ràng là) nhanh hơn. Nhưng vì bạn không có quyền kiểm soát các trường hợp không ổn, với tư cách là người viết thư viện có mã có thể được người khác sử dụng (đọc: bất kỳ thứ gì ngoại trừ các tập lệnh và mô-đun đơn giản chỉ dành cho mục đích cá nhân), bạn phải sử dụng cách triển khai đúng để tuân thủ hợp đồng chung về quá tải nhà điều hành và làm việc với bất kỳ mã nào khác mà bạn có thể gặp phải. May mắn thay, trên Py3, không có vấn đề nào trong số này, vì bạn có thể bỏ qua __ne__hoàn toàn. Một năm nữa, Py2 sẽ chết và chúng tôi bỏ qua điều này. :-)
ShadowRanger

10

Chỉ đối với bản ghi, một thiết bị di động Py2 / Py3 chính xác và chéo __ne__sẽ trông giống như sau:

import sys

class ...:
    ...
    def __eq__(self, other):
        ...

    if sys.version_info[0] == 2:
        def __ne__(self, other):
            equal = self.__eq__(other)
            return equal if equal is NotImplemented else not equal

Điều này hoạt động với bất kỳ thứ gì __eq__bạn có thể xác định:

  • Không giống như not (self == other), không can thiệp vào một số trường hợp khó chịu / phức tạp liên quan đến so sánh trong đó một trong các lớp có liên quan không ngụ ý rằng kết quả của __ne__là giống với kết quả của noton __eq__(ví dụ: SQLAlchemy's ORM, trong đó cả hai __eq____ne__trả về các đối tượng proxy đặc biệt, không Truehoặc Falsevà cố gắng notkết quả của __eq__sẽ trả về False, thay vì đối tượng proxy chính xác).
  • Không giống như not self.__eq__(other), điều này ủy quyền chính xác cho đối tượng __ne__của trường hợp khác khi self.__eq__trả về NotImplemented( not self.__eq__(other)sẽ rất sai, vì NotImplementednó là sự thật, vì vậy khi __eq__không biết cách thực hiện so sánh, __ne__sẽ trả về False, ngụ ý rằng hai đối tượng bằng nhau trong khi thực tế là duy nhất đối tượng được hỏi không có ý tưởng, điều này có nghĩa là mặc định không bằng)

Nếu bạn __eq__không sử dụng NotImplementedtrả về, điều này hoạt động (với chi phí vô nghĩa), nếu NotImplementedđôi khi sử dụng , điều này sẽ xử lý đúng cách. Và việc kiểm tra phiên bản Python có nghĩa là nếu lớp được import-ed trong Python 3, __ne__không được xác định, cho phép __ne__triển khai dự phòng hiệu quả, nguyên bản của Python (phiên bản C của phần trên) tiếp quản.


Tại sao điều này là cần thiết

Quy tắc nạp chồng trong Python

Giải thích tại sao bạn làm điều này thay vì các giải pháp khác là hơi phức tạp. Python có một vài quy tắc chung về nạp chồng toán tử và toán tử so sánh nói riêng:

  1. (Áp dụng cho tất cả các toán tử) Khi chạy LHS OP RHS, hãy thử LHS.__op__(RHS)và nếu điều đó trả về NotImplemented, hãy thử RHS.__rop__(LHS). Ngoại lệ: Nếu RHSlà một lớp con của lớp của LHS, thì hãy kiểm tra RHS.__rop__(LHS) trước . Trong trường hợp các toán tử so sánh, __eq____ne__là "rop" của riêng chúng (vì vậy, thứ tự kiểm tra cho __ne__LHS.__ne__(RHS), sau đó RHS.__ne__(LHS), đảo ngược nếu RHSlà một lớp con của lớp của LHS')
  2. Ngoài ý tưởng về toán tử "hoán đổi", không có mối quan hệ ngụ ý nào giữa các toán tử. Ngay cả đối với cùng một lớp, việc LHS.__eq__(RHS)trả về Truekhông ngụ ý LHS.__ne__(RHS)trả về False(trên thực tế, các toán tử thậm chí không bắt buộc phải trả về giá trị boolean; ORM như SQLAlchemy cố tình không trả về, cho phép cú pháp truy vấn rõ ràng hơn). Kể từ Python 3, việc __ne__triển khai mặc định hoạt động theo cách này, nhưng nó không phải là hợp đồng; bạn có thể ghi đè __ne__theo những cách không đối lập nghiêm ngặt __eq__.

Điều này áp dụng như thế nào đối với bộ so sánh quá tải

Vì vậy, khi bạn quá tải một toán tử, bạn có hai công việc:

  1. Nếu bạn biết cách tự triển khai hoạt động, hãy làm như vậy, chỉ sử dụng kiến thức của riêng bạn về cách thực hiện phép so sánh (không bao giờ ủy quyền, ngầm hoặc rõ ràng, cho phía bên kia của hoạt động; làm như vậy có nguy cơ không chính xác và / hoặc đệ quy vô hạn, tùy thuộc vào cách bạn làm điều đó)
  2. Nếu bạn không biết cách tự thực hiện thao tác, hãy luôn trả về NotImplemented, để Python có thể ủy quyền cho việc triển khai toán hạng khác

Vấn đề với not self.__eq__(other)

def __ne__(self, other):
    return not self.__eq__(other)

không bao giờ ủy quyền cho phía bên kia (và không chính xác nếu __eq__trả về đúng cách NotImplemented). Khi nàoself.__eq__(other) trả về NotImplemented(là "sự thật"), bạn sẽ âm thầm trả về False, vì vậy, A() != something_A_knows_nothing_abouttrả về False, khi nó đáng lẽ phải kiểm tra xem có something_A_knows_nothing_aboutbiết cách so sánh với các trường hợp của hay không A, và nếu không, nó nên trả về True(vì nếu không bên nào biết cách so sánh với cái khác, chúng được coi là không bằng nhau). Nếu A.__eq__được triển khai không chính xác (trả về Falsethay vì NotImplementedkhi nó không nhận ra phía bên kia), thì điều này là "đúng" Atheo quan điểm của nó, trả về True(vì Akhông nghĩ rằng nó bằng nhau, vì vậy nó không bằng), nhưng nó có thể sai từsomething_A_knows_nothing_aboutquan điểm của, vì nó thậm chí chưa bao giờ hỏi something_A_knows_nothing_about; A() != something_A_knows_nothing_aboutkết thúc True, nhưng something_A_knows_nothing_about != A()có thể False, hoặc bất kỳ giá trị trả lại nào khác.

Vấn đề với not self == other

def __ne__(self, other):
    return not self == other

là tinh tế hơn. Nó sẽ đúng cho 99% các lớp, bao gồm tất cả các lớp __ne__là nghịch đảo logic của __eq__. Nhưng not self == othervi phạm cả hai quy tắc được đề cập ở trên, có nghĩa là đối với các lớp __ne__ không phải là nghịch đảo lôgic của __eq__, kết quả một lần nữa không đối xứng, bởi vì một trong các toán hạng không bao giờ được hỏi liệu nó có thể thực hiện đồng ý về kết quả hay không (bởi vì trong trường hợp trước trả về , sau đó trả về , trong khi trường hợp sau trả về trực tiếp). Nhưng khi được triển khai dưới dạng , trả về (bởi vì__ne__ , ngay cả khi toán hạng không thể. Ví dụ đơn giản nhất là một lớp bất thường trả về Falsecho tất cả các so sánh, vì vậy A() == Incomparable()A() != Incomparable()cả hai đều trả về False. Với việc triển khai đúng A.__ne__(một trong đó trả về NotImplementedkhi nó không biết cách so sánh), mối quan hệ là đối xứng; A() != Incomparable()Incomparable() != A()A.__ne__NotImplementedIncomparable.__ne__FalseIncomparable.__ne__FalseA.__ne__return not self == otherA() != Incomparable()TrueA.__eq__ trả về, không phải NotImplemented, sau đó Incomparable.__eq__trả về FalseA.__ne__đảo ngược điều đó thành True), trong khi Incomparable() != A()trả vềFalse.

Bạn có thể xem một ví dụ về điều này đang hoạt động tại đây .

Rõ ràng, một lớp học luôn quay trở lại Falsecho cả hai __eq____ne__hơi kỳ lạ. Nhưng như đã đề cập trước đây, __eq____ne__thậm chí không cần quay lại True/ False; SQLAlchemy ORM có các lớp với các bộ so sánh trả về một đối tượng proxy đặc biệt để xây dựng truy vấn, không phải True/ hoàn Falsetoàn (chúng là "thật" nếu được đánh giá trong ngữ cảnh boolean, nhưng chúng không bao giờ được đánh giá trong ngữ cảnh như vậy).

Bằng cách không tải __ne__đúng cách, bạn sẽ phá vỡ các lớp thuộc loại đó, như mã:

 results = session.query(MyTable).filter(MyTable.fieldname != MyClassWithBadNE())

sẽ hoạt động (giả sử SQLAlchemy biết cách chèn MyClassWithBadNEvào một chuỗi SQL; điều này có thể được thực hiện với bộ điều hợp kiểu mà không cần MyClassWithBadNEphải hợp tác), chuyển đối tượng proxy mong đợi đến filter, trong khi:

 results = session.query(MyTable).filter(MyClassWithBadNE() != MyTable.fieldname)

sẽ kết thúc việc chuyển filtermột cách đơn giản False, bởi vì self == othertrả về một đối tượng proxy và not self == otherchỉ chuyển đổi đối tượng proxy trung thực thành False. Hy vọng rằng, filterném một ngoại lệ về việc xử lý các đối số không hợp lệ như False. Trong khi tôi chắc rằng nhiều người sẽ tranh luận rằngMyTable.fieldname nên nhất quán ở bên trái của so sánh, thực tế vẫn là không có lý do lập trình nào để thực thi điều này trong trường hợp chung và một chung đúng __ne__sẽ hoạt động theo cả hai cách, trong khi return not self == otherchỉ hoạt động trong một sự sắp xếp.


1
Câu trả lời duy nhất đúng, đầy đủ và trung thực (xin lỗi @AaronHall). Đây phải là câu trả lời được chấp nhận.
Maggyero

4

Câu trả lời ngắn gọn: có (nhưng hãy đọc tài liệu để làm đúng)

Việc triển khai __ne__phương thức của ShadowRanger là phương pháp chính xác (và nó là cách triển khai mặc định của __ne__phương thức kể từ Python 3.4):

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

Tại sao? Bởi vì nó giữ một thuộc tính toán học quan trọng, tính đối xứng của !=toán tử. Toán tử này là nhị phân nên kết quả của nó phải phụ thuộc vào kiểu động của cả hai toán hạng, không chỉ một. Điều này được thực hiện thông qua công văn kép cho các ngôn ngữ lập trình cho phép nhiều công văn (chẳng hạn như Julia ). Trong Python chỉ cho phép gửi đơn, gửi kép được mô phỏng cho các phương thức sốphương thức so sánh đa dạng thức bằng cách trả về giá trịNotImplemented trong các phương thức triển khai không hỗ trợ kiểu của toán hạng khác; trình thông dịch sau đó sẽ thử phương thức được phản ánh của toán hạng khác.

Việc triển khai phương pháp not self == othercủa Aaron Hall __ne__là không chính xác vì nó loại bỏ tính đối xứng của !=toán tử. Thật vậy, nó không bao giờ có thể trả về NotImplemented( not NotImplementedFalse) và do đó __ne__phương thức có mức độ ưu tiên cao hơn không bao giờ có thể trở lại __ne__phương thức có mức độ ưu tiên thấp hơn. not self == othertừng là __ne__phương thức triển khai Python 3 mặc định nhưng đó là một lỗi đã được sửa trong Python 3.4 vào tháng 1 năm 2015, như ShadowRanger đã nhận thấy (xem sự cố # 21408 ).

Thực hiện các toán tử so sánh

Các Python Language Reference cho Python 3 tiểu bang trong nó chương mô hình dữ liệu III :

object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)

Đây là những phương pháp được gọi là "so sánh phong phú". Sự tương ứng giữa các ký hiệu nhà điều hành và tên phương thức như sau: x<ycuộc gọi x.__lt__(y), x<=ycuộc gọi x.__le__(y), x==ycuộc gọi x.__eq__(y), x!=ycuộc gọi x.__ne__(y), x>ycuộc gọi x.__gt__(y)x>=y cuộc gọi x.__ge__(y).

Phương thức so sánh chi tiết có thể trả về singleton NotImplementednếu nó không triển khai hoạt động cho một cặp đối số nhất định.

Không có phiên bản đối số được hoán đổi của các phương thức này (sẽ được sử dụng khi đối số bên trái không hỗ trợ thao tác nhưng đối số bên phải thì có); đúng hơn, __lt__()__gt__()là sự phản chiếu của nhau, __le__()__ge__()là sự phản ánh của nhau, __eq__()__ne__()là sự phản ánh của chính họ. Nếu các toán hạng thuộc các kiểu khác nhau và kiểu của toán hạng phải là một lớp con trực tiếp hoặc gián tiếp của kiểu toán hạng bên trái, thì phương thức phản ánh của toán hạng bên phải được ưu tiên, nếu không thì phương thức của toán hạng bên trái được ưu tiên. Phân lớp ảo không được xem xét.

Dịch mã này sang mã Python mang lại (sử dụng operator_eq cho ==, operator_necho !=, operator_ltcho <, operator_gtcho >, operator_lecho <=operator_gecho >=):

def operator_eq(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__eq__(left)

        if result is NotImplemented:
            result = left.__eq__(right)
    else:
        result = left.__eq__(right)

        if result is NotImplemented:
            result = right.__eq__(left)

    if result is NotImplemented:
        result = left is right

    return result


def operator_ne(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ne__(left)

        if result is NotImplemented:
            result = left.__ne__(right)
    else:
        result = left.__ne__(right)

        if result is NotImplemented:
            result = right.__ne__(left)

    if result is NotImplemented:
        result = left is not right

    return result


def operator_lt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__gt__(left)

        if result is NotImplemented:
            result = left.__lt__(right)
    else:
        result = left.__lt__(right)

        if result is NotImplemented:
            result = right.__gt__(left)

    if result is NotImplemented:
        raise TypeError(f"'<' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_gt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__lt__(left)

        if result is NotImplemented:
            result = left.__gt__(right)
    else:
        result = left.__gt__(right)

        if result is NotImplemented:
            result = right.__lt__(left)

    if result is NotImplemented:
        raise TypeError(f"'>' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_le(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ge__(left)

        if result is NotImplemented:
            result = left.__le__(right)
    else:
        result = left.__le__(right)

        if result is NotImplemented:
            result = right.__ge__(left)

    if result is NotImplemented:
        raise TypeError(f"'<=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_ge(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__le__(left)

        if result is NotImplemented:
            result = left.__ge__(right)
    else:
        result = left.__ge__(right)

        if result is NotImplemented:
            result = right.__le__(left)

    if result is NotImplemented:
        raise TypeError(f"'>=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result

Triển khai mặc định của các phương pháp so sánh

Tài liệu cho biết thêm:

Theo mặc định, __ne__()ủy quyền __eq__()và đảo ngược kết quả trừ khi nó được NotImplemented. Không có mối quan hệ ngụ ý nào khác giữa các toán tử so sánh, ví dụ, sự thật của (x<y or x==y)không ngụ ý x<=y.

Việc thực hiện mặc định của phương pháp so sánh ( __eq__, __ne__, __lt__, __gt__, __le____ge__) có thể như vậy được cho bởi:

def __eq__(self, other):
    return NotImplemented

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

def __lt__(self, other):
    return NotImplemented

def __gt__(self, other):
    return NotImplemented

def __le__(self, other):
    return NotImplemented

def __ge__(self, other):
    return NotImplemented

Vì vậy, đây là cách triển khai chính xác của __ne__ phương pháp. Và nó luôn không trả lại nghịch đảo của __eq__phương pháp bởi vì khi __eq__trở về phương pháp NotImplemented, nghịch đảo của nó not NotImplementedFalse(như bool(NotImplemented)True) thay vì mong muốn NotImplemented.

Triển khai không chính xác __ne__

Như Aaron Hall đã trình bày ở trên, not self.__eq__(other) không phải là cách triển khai mặc định của __ne__phương pháp. Nhưng cũng không not self == other. Điều sau được chứng minh bên dưới bằng cách so sánh hành vi của triển khai mặc định với hành vi của not self == othertriển khai trong hai trường hợp:

  • các __eq__trở về phương pháp NotImplemented;
  • các __eq__phương thức trả về một giá trị khác nhau từ NotImplemented.

Triển khai mặc định

Hãy xem điều gì sẽ xảy ra khi A.__ne__phương thức sử dụng triển khai mặc định vàA.__eq__ phương thức trả về NotImplemented:

class A:
    pass


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) == "B.__ne__"
  1. !=cuộc gọi A.__ne__.
  2. A.__ne__cuộc gọi A.__eq__.
  3. A.__eq__ trả lại NotImplemented .
  4. != cuộc gọi B.__ne__.
  5. B.__ne__ trả lại "B.__ne__" .

Điều này cho thấy rằng khi A.__eq__phương thức trả về NotImplemented,A.__ne__ phương thức đó sẽ trở lại B.__ne__phương thức.

Bây giờ chúng ta hãy xem điều gì sẽ xảy ra khi A.__ne__phương thức sử dụng triển khai mặc định và A.__eq__phương thức trả về một giá trị khác với NotImplemented:

class A:

    def __eq__(self, other):
        return True


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. != cuộc gọi A.__ne__.
  2. A.__ne__ cuộc gọi A.__eq__.
  3. A.__eq__ trả lại True .
  4. !=trả lại not True, đó làFalse .

Điều này cho thấy rằng trong trường hợp này, A.__ne__phương thức trả về nghịch đảo củaA.__eq__ phương thức. Do đó, __ne__phương thức hoạt động giống như được quảng cáo trong tài liệu.

Ghi đè việc triển khai mặc định của A.__ne__ phương pháp với triển khai đúng được đưa ra ở trên sẽ mang lại kết quả tương tự.

not self == other thực hiện

Hãy xem điều gì sẽ xảy ra khi ghi đè cài đặt mặc định của A.__ne__phương thức với việc not self == othertriển khai và A.__eq__phương thức trả về NotImplemented:

class A:

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is True
  1. !=cuộc gọi A.__ne__.
  2. A.__ne__cuộc gọi ==.
  3. ==cuộc gọi A.__eq__.
  4. A.__eq__lợi nhuậnNotImplemented .
  5. ==cuộc gọi B.__eq__.
  6. B.__eq__lợi nhuậnNotImplemented .
  7. ==lợi nhuận A() is B(), đó làFalse .
  8. A.__ne__lợi nhuận not False, đó làTrue .

Việc triển khai mặc định của __ne__phương thức được trả về "B.__ne__", không phải True.

Bây giờ chúng ta hãy xem điều gì sẽ xảy ra khi ghi đè cài đặt mặc định của A.__ne__phương thức với việc not self == othertriển khai và A.__eq__phương thức trả về một giá trị khác với NotImplemented:

class A:

    def __eq__(self, other):
        return True

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. !=cuộc gọi A.__ne__.
  2. A.__ne__cuộc gọi ==.
  3. ==cuộc gọi A.__eq__.
  4. A.__eq__lợi nhuậnTrue .
  5. A.__ne__lợi nhuận not True, đó làFalse .

Việc triển khai mặc định của __ne__phương thức cũng được trả vềFalse trong trường hợp này.

Vì quá trình triển khai này không thể tái tạo hành vi của việc triển khai mặc định của __ne__phương thức khi __eq__phương thức trả về NotImplemented, nên nó không chính xác.


Đối với ví dụ cuối cùng của bạn: "Vì triển khai này không thể tái tạo hành vi của triển khai mặc định của __ne__phương thức khi __eq__phương thức trả về NotImplemented, nên nó không chính xác." - Axác định bình đẳng vô điều kiện. Vì vậy A() == B(),. Vì vậy A() != B() nên False , và nó . Các ví dụ được đưa ra là bệnh lý (nghĩa là __ne__không nên trả về một chuỗi và __eq__không nên phụ thuộc vào __ne__- đúng hơn __ne__nên phụ thuộc vào __eq__, đó là kỳ vọng mặc định trong Python 3). Tôi vẫn -1 cho câu trả lời này cho đến khi bạn có thể thay đổi quyết định của tôi.
Aaron Hall

@AaronHall Từ tham chiếu ngôn ngữ Python : "Một phương thức so sánh phong phú có thể trả về singleton NotImplementednếu nó không triển khai hoạt động cho một cặp đối số nhất định. Theo quy ước, FalseTrueđược trả về để so sánh thành công. Tuy nhiên, các phương thức này có thể trả về bất kỳ giá trị nào , vì vậy nếu toán tử so sánh được sử dụng trong ngữ cảnh Boolean (ví dụ: trong điều kiện của câu lệnh if), Python sẽ gọi bool()giá trị để xác định xem kết quả là đúng hay sai. "
Maggyero

@AaronHall Việc triển khai của bạn __ne__giết chết một thuộc tính toán học quan trọng, tính đối xứng của !=toán tử. Toán tử này là nhị phân nên kết quả của nó phải phụ thuộc vào kiểu động của cả hai toán hạng, không chỉ một. Điều này được thực hiện chính xác trong các ngôn ngữ lập trình thông qua công văn kép cho ngôn ngữ cho phép nhiều công văn . Trong Python chỉ cho phép gửi một lần, điều phối kép được mô phỏng bằng cách trả về NotImplementedgiá trị.
Maggyero

Ví dụ cuối cùng có hai lớp, Btrả về một chuỗi trung thực cho tất cả các lần kiểm tra __ne__Atrả về Truecho tất cả các lần kiểm tra __eq__. Đây là một mâu thuẫn bệnh lý. Dưới sự mâu thuẫn như vậy, tốt nhất là nên nêu ra một ngoại lệ. Không có kiến ​​thức về B, Akhông có nghĩa vụ phải tôn trọng Bviệc thực hiện __ne__cho các mục đích đối xứng. Tại thời điểm đó trong ví dụ, cách Atriển khai __ne__không liên quan đến tôi. Hãy tìm một trường hợp thực tế, không bệnh lý để đưa ra quan điểm của mình. Tôi đã cập nhật câu trả lời của tôi để giải quyết cho bạn.
Aaron Hall

@AaronHall Để có một ví dụ thực tế hơn, hãy xem ví dụ SQLAlchemy do @ShadowRanger đưa ra. Cũng lưu ý rằng thực tế là việc bạn triển khai các __ne__tác phẩm trong các trường hợp sử dụng điển hình không làm cho nó đúng. Máy bay Boeing 737 MAX đã bay 500.000 chuyến trước khi gặp nạn…
Maggyero

-1

Nếu tất cả __eq__, __ne__, __lt__, __ge__, __le__, và __gt__có ý nghĩa cho lớp, sau đó chỉ cần thực hiện __cmp__để thay thế. Nếu không, hãy làm như bạn đang làm, vì một chút Daniel DiPaolo đã nói (trong khi tôi đang thử nghiệm nó thay vì tra cứu nó;))


12
Các __cmp__()phương pháp đặc biệt không còn được hỗ trợ bằng Python 3.x, do đó bạn phải làm quen với cách sử dụng các toán tử so sánh phong phú.
Don O'Donnell

8
Hoặc nếu bạn đang sử dụng Python 2.7 hoặc 3.x, trình trang trí functools.total_ordering cũng khá tiện dụng.
Adam Parkin

Cảm ơn cho những người đứng đầu lên. Tuy nhiên, tôi đã nhận ra nhiều điều dọc theo những dòng đó trong năm rưỡi qua. ;)
Karl Knechtel
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.