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__
và
- 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==y
khô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.
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ề NotImplemented
kiể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 NotImplemented
tượ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 other
là một subtype, và nếu nó có mà nhà điều hành được xác định, nó sử dụng other
phương pháp 's đầu tiên (nghịch đảo cho <
, <=
, >=
và >
). 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__
là typeobject.c
trongobject_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 NotImplemented
trong __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 == other
của Aaron Hall __ne__
là không chính xác vì nó không bao giờ có thể trả về NotImplemented
( not NotImplemented
là False
) 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 NotImplemented
khô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 NotImplemented
thô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 == other
từ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 is
và 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ý NotImplemented
trong __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.
__ne__
bằng cách sử dụng__eq__
, chỉ là bạn triển khai nó.