Python, haruskah saya menerapkan __ne__()
operator berdasarkan __eq__
?
Jawaban Singkat: Jangan terapkan, tetapi jika harus, gunakan ==
, jangan__eq__
Dalam Python 3, !=
negasi dari ==
secara default, jadi Anda bahkan tidak diharuskan untuk menulis __ne__
, dan dokumentasinya tidak lagi beropini untuk menulisnya.
Secara umum, untuk kode Python 3-saja, jangan menulisnya kecuali Anda perlu menaungi implementasi induk, misalnya untuk objek bawaan.
Artinya, ingatlah komentar Raymond Hettinger :
The __ne__
Metode berikut secara otomatis dari __eq__
hanya jika
__ne__
belum didefinisikan dalam superclass. Jadi, jika Anda mewarisi dari bawaan, yang terbaik adalah mengganti keduanya.
Jika Anda membutuhkan kode Anda untuk bekerja dengan Python 2, ikuti rekomendasi untuk Python 2 dan itu akan bekerja dengan baik di Python 3.
Di Python 2, Python sendiri tidak secara otomatis mengimplementasikan operasi apa pun dalam istilah lain - oleh karena itu, Anda harus mendefinisikan __ne__
in ==
daripada __eq__
. MISALNYA
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)`
Lihat buktinya
__ne__()
operator pelaksana berdasarkan __eq__
dan
- tidak menerapkan
__ne__
di Python 2 sama sekali
memberikan perilaku yang salah dalam demonstrasi di bawah ini.
Jawaban panjang
The dokumentasi untuk Python 2 mengatakan:
Tidak ada hubungan tersirat di antara operator pembanding. Kebenaran x==y
tidak berarti bahwa x!=y
itu salah. Oleh karena itu, saat mendefinisikan __eq__()
, seseorang juga harus mendefinisikan __ne__()
sehingga operator akan berperilaku seperti yang diharapkan.
Artinya, jika kita mendefinisikan __ne__
kebalikan dari __eq__
, kita bisa mendapatkan perilaku yang konsisten.
Bagian dokumentasi ini telah diperbarui untuk Python 3:
Secara default, __ne__()
delegasi ke __eq__()
dan membalikkan hasil kecuali jika memang demikian NotImplemented
.
dan di bagian "apa yang baru" , kami melihat perilaku ini telah berubah:
!=
sekarang mengembalikan kebalikan dari ==
, kecuali jika ==
kembali NotImplemented
.
Untuk mengimplementasikan __ne__
, kami lebih suka menggunakan ==
operator daripada menggunakan __eq__
metode secara langsung sehingga jika self.__eq__(other)
subclass mengembalikan NotImplemented
untuk tipe yang dicentang, Python akan memeriksa other.__eq__(self)
Dari dokumentasi dengan tepat :
The NotImplemented
objek
Jenis ini memiliki nilai tunggal. Ada satu objek dengan nilai ini. Objek ini diakses melalui nama bawaan
NotImplemented
. Metode numerik dan metode perbandingan kaya dapat mengembalikan nilai ini jika mereka tidak mengimplementasikan operasi untuk operan yang disediakan. (Penerjemah kemudian akan mencoba operasi yang direfleksikan, atau beberapa fallback lainnya, tergantung pada operatornya.) Nilai kebenarannya adalah benar.
Ketika diberi operator perbandingan yang kaya, jika mereka bukan tipe yang sama, cek Python jika other
adalah subtipe, dan jika memiliki bahwa operator didefinisikan, menggunakan other
metode 's pertama (inverse untuk <
, <=
, >=
dan >
). Jika NotImplemented
dikembalikan, maka itu menggunakan metode sebaliknya. (Ini tidak memeriksa metode yang sama dua kali.) Menggunakan ==
operator memungkinkan logika ini berlangsung.
Harapan
Secara semantik, Anda harus mengimplementasikan __ne__
dalam hal pemeriksaan kesetaraan karena pengguna kelas Anda akan mengharapkan fungsi berikut setara untuk semua instance 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
Artinya, kedua fungsi di atas harus selalu mengembalikan hasil yang sama. Tapi ini tergantung pada programmernya.
Demonstrasi perilaku tak terduga saat mendefinisikan __ne__
berdasarkan __eq__
:
Pertama penyiapan:
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."""
Instantiate instance yang tidak setara:
right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)
Perilaku yang Diharapkan:
(Catatan: meskipun pernyataan setiap detik dari masing-masing pernyataan di bawah ini setara dan oleh karena itu secara logis berlebihan dengan pernyataan sebelumnya, saya menyertakan pernyataan tersebut untuk menunjukkan bahwa urutan tidak menjadi masalah jika salah satu merupakan subkelas dari yang lain. )
Instance ini telah __ne__
diimplementasikan dengan ==
:
assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1
Instance ini, yang diuji dengan Python 3, juga bekerja dengan benar:
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
Dan ingat bahwa ini telah __ne__
diterapkan dengan __eq__
- meskipun ini adalah perilaku yang diharapkan, penerapannya salah:
assert not wrong1 == wrong2 # These are contradicted by the
assert not wrong2 == wrong1 # below unexpected behavior!
Perilaku Tak Terduga:
Perhatikan bahwa perbandingan ini bertentangan dengan perbandingan di atas ( not wrong1 == wrong2
).
>>> assert wrong1 != wrong2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
dan,
>>> assert wrong2 != wrong1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
Jangan lewati __ne__
dengan Python 2
Untuk bukti bahwa Anda tidak boleh melewatkan penerapan __ne__
di Python 2, lihat objek yang setara ini:
>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True
Hasil di atas seharusnya False
!
Sumber Python 3
Implementasi CPython default untuk __ne__
ada typeobject.c
diobject_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;
Tapi default __ne__
menggunakan __eq__
?
__ne__
Detail implementasi default Python 3 di level C digunakan __eq__
karena level yang lebih tinggi ==
( PyObject_RichCompare ) akan kurang efisien - dan oleh karena itu ia juga harus menangani NotImplemented
.
Jika __eq__
diterapkan dengan benar, maka negasi dari ==
juga benar - dan ini memungkinkan kita untuk menghindari detail implementasi tingkat rendah di __ne__
.
Menggunakan ==
memungkinkan kita untuk menjaga logika rendah tingkat kami di satu tempat, dan menghindari menangani NotImplemented
di __ne__
.
Orang mungkin salah berasumsi bahwa ==
mungkin kembali NotImplemented
.
Ini sebenarnya menggunakan logika yang sama dengan implementasi default __eq__
, yang memeriksa identitas (lihat do_richcompare dan bukti kami di bawah)
class Foo:
def __ne__(self, other):
return NotImplemented
__eq__ = __ne__
f = Foo()
f2 = Foo()
Dan perbandingannya:
>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True
Performa
Jangan percaya kata-kata saya untuk itu, mari kita lihat apa yang lebih berkinerja:
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
Saya pikir angka-angka kinerja ini berbicara sendiri:
>>> 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
Ini masuk akal ketika Anda mempertimbangkan bahwa low_level_python
melakukan logika dengan Python yang seharusnya ditangani pada level C.
Tanggapan untuk beberapa kritikus
Penjawab lain menulis:
Implementasi Aaron Hall not self == other
dari __ne__
metode tidak benar karena tidak pernah dapat kembali NotImplemented
( not NotImplemented
yang False
) dan oleh karena itu __ne__
metode yang memiliki prioritas tidak pernah bisa jatuh kembali pada __ne__
metode yang tidak memiliki prioritas.
Tidak __ne__
pernah kembali NotImplemented
tidak membuatnya salah. Sebaliknya, kami menangani prioritas dengan NotImplemented
melalui check for equality with ==
. Dengan asumsi ==
diterapkan dengan benar, kita sudah selesai.
not self == other
dulunya adalah implementasi Python 3 default untuk __ne__
metode ini, tetapi itu adalah bug dan diperbaiki dengan Python 3.4 pada Januari 2015, seperti yang diperhatikan ShadowRanger (lihat masalah # 21408).
Baiklah, mari kita jelaskan ini.
Seperti disebutkan sebelumnya, Python 3 secara default menangani __ne__
dengan terlebih dahulu memeriksa apakah self.__eq__(other)
return NotImplemented
(a singleton) - yang harus diperiksa dengan is
dan dikembalikan jika demikian, kalau tidak ia harus mengembalikan inversnya. Berikut adalah logika yang ditulis sebagai campuran kelas:
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
Ini diperlukan untuk kebenaran untuk API Python tingkat C, dan itu diperkenalkan di Python 3, membuat
mubazir. Semua __ne__
metode yang relevan telah dihapus, termasuk metode yang menerapkan pemeriksaan mereka sendiri serta metode yang didelegasikan __eq__
secara langsung atau melalui ==
- dan ==
merupakan cara paling umum untuk melakukannya.
Apakah Simetri Penting?
Kritikus gigih kami memberikan contoh patologis untuk membuat kasus untuk penanganan NotImplemented
di __ne__
, menilai simetri di atas segalanya. Mari kita menguatkan argumen dengan contoh yang jelas:
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)
Jadi, dengan logika ini, untuk menjaga simetri, kita perlu menulis rumit __ne__
, apa pun versi Pythonnya.
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)
Rupanya kita tidak perlu peduli bahwa contoh ini sama dan tidak sama.
Saya mengusulkan simetri yang kurang penting daripada praduga kode yang masuk akal dan mengikuti saran dari dokumentasi.
Namun, jika A memiliki implementasi yang masuk akal __eq__
, maka kita masih bisa mengikuti arahan saya di sini dan kita masih memiliki simetri:
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)
Kesimpulan
Untuk kode yang kompatibel dengan Python 2, gunakan ==
untuk mengimplementasikan __ne__
. Ini lebih:
Hanya dalam Python 3, gunakan negasi tingkat rendah pada tingkat C - ini bahkan lebih sederhana dan berkinerja (meskipun pemrogram bertanggung jawab untuk menentukan bahwa itu benar ).
Sekali lagi, lakukan tidak logika tingkat rendah menulis di tingkat tinggi Python.
__ne__
menggunakan__eq__
, hanya bahwa Anda menerapkannya.