Haruskah saya menerapkan __ne__ dalam istilah __eq__ dengan Python?

100

Saya memiliki kelas di mana saya ingin mengganti __eq__metode ini. Tampaknya masuk akal bahwa saya harus menimpa __ne__metode juga, tapi tidak masuk akal untuk menerapkan __ne__dalam hal __eq__seperti itu?

class A:

    def __init__(self, attr):
        self.attr = attr

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

Atau apakah ada sesuatu yang saya lewatkan dengan cara Python menggunakan metode ini yang membuat ini bukan ide yang bagus?

Falmarri
sumber

Jawaban:

57

Ya, tidak apa-apa. Faktanya, dokumentasi tersebut mendorong Anda untuk menentukan __ne__kapan Anda mendefinisikan __eq__:

Tidak ada hubungan tersirat di antara operator pembanding. Kebenaran x==ytidak berarti bahwa x!=y itu salah. Oleh karena itu, saat mendefinisikan __eq__(), seseorang juga harus mendefinisikan __ne__()sehingga operator akan berperilaku seperti yang diharapkan.

Dalam banyak kasus (seperti ini), itu akan sesederhana meniadakan hasil dari __eq__, tetapi tidak selalu.

Daniel DiPaolo
sumber
12
ini adalah jawaban yang benar (di sini, oleh @ aaron-hall). Dokumentasi yang dikutip tidak tidak mendorong Anda untuk menerapkan __ne__menggunakan __eq__, hanya bahwa Anda menerapkannya.
guyarad
2
@guyarad: Sebenarnya, jawaban Aaron masih sedikit salah karena tidak mendelegasikan dengan benar; alih-alih memperlakukan NotImplementedpengembalian dari satu sisi sebagai isyarat untuk didelegasikan ke __ne__sisi lain, not self == otheradalah (dengan asumsi operan __eq__tidak tahu bagaimana membandingkan operan lain) secara implisit mendelegasikan ke __eq__dari sisi lain, kemudian membalikkannya. Untuk tipe aneh, misalnya kolom SQLAlchemy ORM, ini menyebabkan masalah .
ShadowRanger
1
Kritik ShadowRanger hanya akan berlaku untuk kasus yang sangat patologis (IMHO) dan sepenuhnya dibahas dalam jawaban saya di bawah.
Aaron Hall
1
Dokumentasi yang lebih baru (setidaknya untuk 3.7, mungkin bahkan lebih awal) __ne__secara otomatis didelegasikan ke __eq__dan kutipan dalam jawaban ini tidak lagi ada di dokumen. Intinya adalah pythonic sempurna untuk hanya mengimplementasikan __eq__dan membiarkan __ne__mendelegasikan.
bluesummers
134

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==ytidak berarti bahwa x!=yitu 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 NotImplementeduntuk tipe yang dicentang, Python akan memeriksa other.__eq__(self) Dari dokumentasi dengan tepat :

The NotImplementedobjek

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 otheradalah subtipe, dan jika memiliki bahwa operator didefinisikan, menggunakan othermetode 's pertama (inverse untuk <, <=, >=dan >). Jika NotImplementeddikembalikan, 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.cdiobject_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 NotImplementeddi __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_pythonmelakukan logika dengan Python yang seharusnya ditangani pada level C.

Tanggapan untuk beberapa kritikus

Penjawab lain menulis:

Implementasi Aaron Hall not self == otherdari __ne__metode tidak benar karena tidak pernah dapat kembali NotImplemented( not NotImplementedyang 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 NotImplementedtidak membuatnya salah. Sebaliknya, kami menangani prioritas dengan NotImplementedmelalui check for equality with ==. Dengan asumsi ==diterapkan dengan benar, kita sudah selesai.

not self == otherdulunya 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 isdan 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 NotImplementeddi __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:

  • benar
  • sederhana
  • penampil

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.

Aaron Hall
sumber
3
Contoh yang bagus! Bagian yang mengejutkan adalah bahwa urutan operan sama sekali tidak penting , tidak seperti beberapa metode ajaib dengan pantulan "sisi kanan" mereka. Untuk mengulangi bagian yang saya lewatkan (dan yang menghabiskan banyak waktu): Metode perbandingan kaya subkelas dicoba terlebih dahulu, terlepas dari apakah kode tersebut memiliki superclass atau subclass di sebelah kiri operator. Inilah sebabnya mengapa Anda a1 != c2kembali False--- itu tidak berjalan a1.__ne__, tapi c2.__ne__, yang menegasikan mixin ini __eq__ metode. Sejak NotImplementeditu benar, not NotImplementedadalah False.
Kevin J. Chase
2
Pembaruan terbaru Anda berhasil menunjukkan keunggulan kinerja not (self == other), tetapi tidak ada yang membantahnya tidak cepat (yah, lebih cepat daripada opsi lain di Py2). Masalahnya adalah salah dalam beberapa kasus; Python sendiri dulu melakukannya not (self == other), tetapi berubah karena salah dengan adanya subkelas yang berubah-ubah . Jawaban tercepat sampai salah masih salah .
ShadowRanger
1
Contoh spesifiknya sebenarnya tidak penting. Masalahnya adalah, dalam implementasi Anda, perilaku __ne__delegasi Anda ke __eq__(dari kedua sisi jika perlu), tetapi tidak pernah jatuh kembali ke __ne__sisi lain bahkan ketika keduanya __eq__"menyerah". __ne__Delegasi yang benar menjadi miliknya sendiri __eq__ , tetapi jika itu kembali NotImplemented, itu kembali untuk pergi ke pihak lain __ne__, daripada membalikkan pihak lain __eq__(karena pihak lain mungkin tidak secara eksplisit memilih untuk mendelegasikan __eq__, dan Anda tidak seharusnya membuat keputusan itu untuk itu).
ShadowRanger
1
@AaronHall: Saat memeriksa ulang hari ini, saya rasa penerapan Anda tidak bermasalah untuk subkelas secara normal (akan sangat berbelit-belit untuk membuatnya rusak, dan subkelas, yang diasumsikan memiliki pengetahuan penuh tentang induknya, seharusnya dapat menghindarinya ). Tapi saya hanya memberikan contoh yang tidak berbelit-belit dalam jawaban saya. Kasus non-patologis adalah ORM SQLAlchemy, di mana tidak ada __eq__atau __ne__mengembalikan salah satu Trueatau False, melainkan objek proxy (yang kebetulan "benar"). Penerapan yang salah __ne__berarti urutan penting untuk perbandingan (Anda hanya mendapatkan proxy dalam satu urutan).
ShadowRanger
1
Untuk lebih jelasnya, dalam 99% (atau mungkin 99,999%) kasus, solusi Anda baik-baik saja, dan (jelas) lebih cepat. Tetapi karena Anda tidak memiliki kendali atas kasus-kasus di mana itu tidak baik, sebagai penulis perpustakaan yang kodenya dapat digunakan oleh orang lain (baca: apa pun kecuali skrip dan modul satu kali yang sederhana hanya untuk penggunaan pribadi), Anda harus gunakan implementasi yang benar untuk mematuhi kontrak umum untuk operator yang kelebihan beban dan bekerja dengan kode lain apa pun yang mungkin Anda temui. Untungnya, di Py3, semua ini tidak penting, karena Anda dapat menghilangkan __ne__sepenuhnya. Setahun dari sekarang, Py2 akan mati dan kita abaikan ini. :-)
ShadowRanger
10

Sekadar catatan, portabel Py2 / Py3 yang benar dan silang secara kanonik __ne__akan terlihat seperti:

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

Ini berfungsi dengan apa pun __eq__yang mungkin Anda tentukan:

  • Tidak seperti not (self == other), tidak mengganggu dalam beberapa kasus yang mengganggu / kompleks yang melibatkan perbandingan di mana salah satu kelas yang terlibat tidak berarti bahwa hasil __ne__adalah sama dengan hasil notpada __eq__(ORM misalnya SQLAlchemy, di mana baik __eq__dan __ne__kembali proksi benda-benda khusus, tidak Trueatau False, dan mencoba nothasil __eq__akan kembali False, bukan objek proxy yang benar).
  • Tidak seperti not self.__eq__(other), ini dengan benar mendelegasikan ke __ne__contoh lain ketika self.__eq__kembali NotImplemented( not self.__eq__(other)akan sangat salah, karena NotImplementedbenar, jadi ketika __eq__tidak tahu bagaimana melakukan perbandingan, __ne__akan kembali False, menyiratkan bahwa kedua objek itu sama padahal sebenarnya satu-satunya objek yang ditanyakan tidak tahu, yang berarti default tidak sama)

Jika Anda __eq__tidak menggunakan NotImplementedpengembalian, ini berfungsi (dengan overhead yang tidak berarti), jika NotImplementedkadang-kadang digunakan , ini menanganinya dengan benar. Dan pemeriksaan versi Python berarti bahwa jika kelasnya diberi- importdi Python 3, __ne__dibiarkan tidak terdefinisi, memungkinkan __ne__implementasi fallback asli dan efisien Python (versi C di atas) untuk mengambil alih.


Mengapa ini dibutuhkan

Aturan overloading Python

Penjelasan mengapa Anda melakukan ini dan bukan solusi lain agak misterius. Python memiliki beberapa aturan umum tentang operator overloading, dan operator perbandingan khususnya:

  1. (Berlaku untuk semua operator) Saat menjalankan LHS OP RHS, coba LHS.__op__(RHS), dan jika berhasil NotImplemented, coba RHS.__rop__(LHS). Pengecualian: Jika RHSmerupakan subclass dari LHSkelas, maka uji RHS.__rop__(LHS) terlebih dahulu . Dalam kasus operator pembanding, __eq__dan __ne__merupakan "rop" mereka sendiri (jadi urutan pengujiannya __ne__adalah LHS.__ne__(RHS), kemudian RHS.__ne__(LHS), dibalik jika RHSadalah subclass dari LHSkelasnya)
  2. Selain gagasan tentang operator "yang ditukar", tidak ada hubungan tersirat antara operator. Bahkan untuk instance dari kelas yang sama, LHS.__eq__(RHS)mengembalikan Truetidak berarti LHS.__ne__(RHS)mengembalikan False(pada kenyataannya, operator bahkan tidak diharuskan untuk mengembalikan nilai boolean; ORM seperti SQLAlchemy sengaja tidak melakukannya, memungkinkan sintaks kueri yang lebih ekspresif). Pada Python 3, __ne__implementasi default berperilaku seperti ini, tetapi tidak kontraktual; Anda dapat menimpa __ne__dengan cara yang tidak berlawanan __eq__.

Bagaimana ini berlaku untuk pembanding yang kelebihan beban

Jadi, saat Anda membebani operator, Anda memiliki dua pekerjaan:

  1. Jika Anda tahu cara mengimplementasikan operasi itu sendiri, lakukanlah, hanya dengan menggunakan pengetahuan Anda sendiri tentang cara melakukan perbandingan (jangan pernah mendelegasikan, secara implisit atau eksplisit, ke sisi lain operasi; melakukan hal itu berisiko tidak benar dan / atau rekursi tak terbatas, tergantung bagaimana Anda melakukannya)
  2. Jika Anda tidak tahu cara mengimplementasikan operasi sendiri, selalu kembali NotImplemented, sehingga Python dapat mendelegasikan ke implementasi operan lain

Masalah dengan not self.__eq__(other)

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

tidak pernah mendelegasikan ke pihak lain (dan tidak benar jika __eq__dikembalikan dengan benar NotImplemented). Ketika self.__eq__(other)kembali NotImplemented(yang "benar"), Anda diam-diam kembali False, jadi A() != something_A_knows_nothing_aboutmengembalikan False, ketika seharusnya diperiksa apakah something_A_knows_nothing_abouttahu bagaimana membandingkan dengan contoh A, dan jika tidak, itu seharusnya dikembalikan True(karena jika tidak ada pihak yang tahu bagaimana caranya dibandingkan dengan yang lain, mereka dianggap tidak setara satu sama lain). Jika A.__eq__diimplementasikan secara tidak benar (mengembalikan Falsebukannya NotImplementedketika tidak mengenali sisi lain), maka ini adalah "benar" dari Aperspektif, kembali True(karena Atidak menganggapnya sama, jadi tidak sama), tetapi mungkin saja salah darisomething_A_knows_nothing_aboutperspektif, karena tidak pernah ditanya something_A_knows_nothing_about; A() != something_A_knows_nothing_aboutberakhir True, tapi something_A_knows_nothing_about != A()bisa False, atau nilai kembali lainnya.

Masalah dengan not self == other

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

lebih halus. Ini akan benar untuk 99% kelas, termasuk semua kelas yang __ne__merupakan invers logis dari __eq__. Tapi not self == othermelanggar kedua aturan yang disebutkan di atas, yang berarti untuk kelas di mana __ne__ bukan invers logis __eq__, hasilnya sekali lagi non-simetris, karena salah satu operan tidak pernah ditanya apakah dapat diimplementasikan __ne__sama sekali, bahkan jika yang lain operan tidak bisa. Contoh paling sederhana adalah kelas aneh yang kembali Falseuntuk semua perbandingan, sehingga A() == Incomparable()dan A() != Incomparable()keduanya kembali False. Dengan implementasi yang benar A.__ne__(yang kembali NotImplementedketika tidak tahu bagaimana melakukan perbandingan), hubungannya simetris; A() != Incomparable()danIncomparable() != A()setuju pada hasil (karena dalam kasus pertama, A.__ne__kembali NotImplemented, kemudian Incomparable.__ne__kembali False, sedangkan dalam kasus terakhir, Incomparable.__ne__kembali Falsesecara langsung). Tapi ketika A.__ne__diimplementasikan sebagai return not self == other, A() != Incomparable()mengembalikan True(karena A.__eq__mengembalikan, bukan NotImplemented, lalu Incomparable.__eq__mengembalikan False, dan A.__ne__membalikkannya menjadi True), sementara Incomparable() != A()mengembalikanFalse.

Anda dapat melihat contohnya di sini .

Jelas, kelas yang selalu kembali Falseuntuk keduanya __eq__dan __ne__sedikit aneh. Tapi seperti yang disebutkan sebelumnya, __eq__dan __ne__bahkan tidak perlu mengembalikan True/ False; ORM SQLAlchemy memiliki kelas dengan pembanding yang mengembalikan objek proxy khusus untuk pembuatan kueri, bukan True/ Falsesama sekali (mereka "benar" jika dievaluasi dalam konteks boolean, tetapi mereka tidak pernah seharusnya dievaluasi dalam konteks seperti itu).

Dengan gagal membebani __ne__dengan benar, Anda akan merusak kelas semacam itu, sebagai kode:

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

akan berfungsi (dengan asumsi SQLAlchemy sama sekali tahu cara memasukkan MyClassWithBadNEke dalam string SQL; ini dapat dilakukan dengan adaptor tipe tanpa MyClassWithBadNEharus bekerja sama sama sekali), meneruskan objek proxy yang diharapkan ke filter, sementara:

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

akan berakhir filterdengan polos False, karena self == othermengembalikan objek proxy, dan not self == otherhanya mengubah objek proxy yang benar menjadi False. Mudah-mudahan, filtermelempar pengecualian saat menangani argumen yang tidak valid seperti False. Meskipun saya yakin banyak yang akan berpendapat bahwa MyTable.fieldname seharusnya secara konsisten berada di sisi kiri perbandingan, faktanya tetap bahwa tidak ada alasan terprogram untuk memberlakukan ini dalam kasus umum, dan generik yang benar __ne__akan berfungsi dengan cara apa pun, sementara return not self == otherhanya berfungsi dalam satu pengaturan.

ShadowRanger
sumber
1
Satu-satunya jawaban yang benar, lengkap dan jujur ​​(maaf @AaronHall). Ini harus menjadi jawaban yang diterima.
Maggyero
Anda mungkin tertarik dengan jawaban diperbarui saya yang menggunakan saya pikir argumen kuat dari Anda Incomparablekelas sejak kelas ini istirahat yang pelengkap hubungan antara !=dan ==operator dan karena itu mungkin dianggap tidak valid atau "patologis" Misalnya seperti @AaronHall meletakkannya. Dan saya akui bahwa @AaronHall ada benarnya ketika dia menunjukkan bahwa argumen SQLAlchemy Anda mungkin dianggap tidak relevan karena berada dalam konteks non-Boolean. (Argumen Anda masih sangat menarik dan dipikirkan dengan baik.)
Maggyero
4

__ne__Implementasi yang benar

Implementasi @ ShadowRanger untuk metode khusus __ne__adalah yang benar:

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

Ini juga merupakan implementasi default dari metode khusus __ne__ sejak Python 3.4 , seperti yang dinyatakan dalam dokumentasi Python :

Secara default, __ne__()delegasi ke __eq__()dan membalikkan hasil kecuali jika memang demikian NotImplemented.

Perhatikan juga bahwa mengembalikan nilai NotImplementeduntuk operand yang tidak didukung tidak spesifik untuk metode khusus __ne__. Faktanya, semua metode perbandingan khusus 1 dan metode numerik khusus 2 harus mengembalikan nilai NotImplementeduntuk operan yang tidak didukung , seperti yang ditentukan dalam dokumentasi Python :

NotImplemented

Jenis ini memiliki nilai tunggal. Ada satu objek dengan nilai ini. Objek ini diakses melalui nama bawaan NotImplemented. Metode numerik dan metode perbandingan kaya harus 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.

Contoh untuk metode numerik khusus diberikan dalam dokumentasi Python :

class MyIntegral(Integral):

    def __add__(self, other):
        if isinstance(other, MyIntegral):
            return do_my_adding_stuff(self, other)
        elif isinstance(other, OtherTypeIKnowAbout):
            return do_my_other_adding_stuff(self, other)
        else:
            return NotImplemented

    def __radd__(self, other):
        if isinstance(other, MyIntegral):
            return do_my_adding_stuff(other, self)
        elif isinstance(other, OtherTypeIKnowAbout):
            return do_my_other_adding_stuff(other, self)
        elif isinstance(other, Integral):
            return int(other) + int(self)
        elif isinstance(other, Real):
            return float(other) + float(self)
        elif isinstance(other, Complex):
            return complex(other) + complex(self)
        else:
            return NotImplemented

1 Metode perbandingan khusus: __lt__, __le__, __eq__, __ne__, __gt__dan __ge__.

2 Metode numerik khusus: __add__, __sub__, __mul__, __matmul__, __truediv__, __floordiv__, __mod__, __divmod__, __pow__, __lshift__, __rshift__, __and__, __xor__, __or__dan mereka __r*__tercermin dan __i*__di tempat rekan-rekan.

__ne__Penerapan yang salah # 1

Implementasi @ Falmarri atas metode khusus __ne__salah:

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

Masalah dengan implementasi ini adalah ia tidak kembali ke metode khusus __ne__dari operan lain karena tidak pernah mengembalikan nilai NotImplemented(ekspresi not self.__eq__(other)mengevaluasi ke nilai Trueatau False, termasuk ketika subekspresi self.__eq__(other)mengevaluasi ke nilai NotImplementedsejak bool(NotImplemented)itu True). Evaluasi nilai Boolean NotImplementedmemutuskan hubungan komplemen antara operator !=dan ==:

class Correct:

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


class Incorrect:

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


x, y = Correct(), Correct()
assert (x != y) is not (x == y)

x, y = Incorrect(), Incorrect()
assert (x != y) is not (x == y)  # AssertionError

__ne__Penerapan yang salah # 2

Implementasi @ AaronHall untuk metode khusus __ne__juga salah:

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

Masalah dengan implementasi ini adalah ia secara langsung kembali ke metode khusus __eq__dari operan lain, melewati metode khusus __ne__dari operan lain karena tidak pernah mengembalikan nilai NotImplemented(ekspresi not self == otherjatuh kembali ke metode khusus __eq__dari operan lain dan mengevaluasi ke nilai Trueatau False). Melewati metode tidak benar karena metode itu mungkin memiliki efek samping seperti memperbarui status objek:

class Correct:

    def __init__(self):
        self.counter = 0

    def __ne__(self, other):
        self.counter += 1
        result = self.__eq__(other)
        if result is not NotImplemented:
            return not result
        return NotImplemented


class Incorrect:

    def __init__(self):
        self.counter = 0

    def __ne__(self, other):
        self.counter += 1
        return not self == other


x, y = Correct(), Correct()
assert x != y
assert x.counter == y.counter

x, y = Incorrect(), Incorrect()
assert x != y
assert x.counter == y.counter  # AssertionError

Memahami operasi perbandingan

Dalam matematika, relasi biner R atas himpunan X adalah himpunan pasangan terurut ( xy ) dalam  X 2 . Pernyataan ( xy ) di  R berbunyi " x adalah R -terkait untuk y " dan dilambangkan dengan XRY .

Properti relasi biner R atas himpunan X :

  • R adalah refleksif saat untuk semua x di X , XRX .
  • R tidak refleksif (juga disebut ketat ) jika untuk semua x dalam X , bukan xRx .
  • R adalah simetris saat untuk semua x dan y di X , jika XRY kemudian yRx .
  • R adalah antisymmetric saat untuk semua x dan y di X , jika XRY dan yRx kemudian x  =  y .
  • R adalah transitif saat untuk semua x , y dan z di X , jika XRY dan yRz kemudian XRZ .
  • R adalah connex (juga disebut Total ) ketika untuk semua x dan y di X , XRY atau yRx .
  • R adalah relasi ekivalen jika R refleksif, simetris dan transitif.
    Misalnya, =. Namun ≠ hanya simetris.
  • R adalah hubungan keteraturan ketika R refleksif, antisimetris dan transitif.
    Misalnya, ≤ dan ≥.
  • R adalah relasi tatanan yang ketat jika R tidak refleksif, antisimetris, dan transitif.
    Misalnya, <dan>.

Operasi pada dua relasi biner R dan S pada himpunan X :

  • The Kebalikan dari R adalah relasi biner R T  = {( yx ) | XRY } lebih X .
  • The pelengkap dari R adalah relasi biner ¬ R  = {( xy ) | tidak XRY } lebih X .
  • The serikat dari R dan S adalah relasi biner R  ∪  S  = {( xy ) | XRY atau xSy } lebih X .

Hubungan antar hubungan perbandingan yang selalu valid:

  • 2 hubungan yang saling melengkapi: = dan ≠ adalah saling melengkapi;
  • 6 hubungan kebalikan: = adalah kebalikan dari dirinya sendiri, ≠ adalah kebalikan dari dirinya sendiri, <dan> adalah kebalikan satu sama lain, dan ≤ dan ≥ adalah kebalikan satu sama lain;
  • 2 hubungan serikat pekerja: ≤ adalah serikat <dan =, dan ≥ adalah gabungan> dan =.

Hubungan antar relasi perbandingan yang hanya valid untuk perintah connex :

  • 4 hubungan komplementer: <dan ≥ saling melengkapi, dan> dan ≤ saling melengkapi.

Jadi untuk benar menerapkan Python operator perbandingan ==, !=, <, >, <=, dan >=sesuai dengan hubungan perbandingan =, ≠, <,>, ≤, dan ≥, semua di atas sifat matematika dan hubungan harus terus.

Python memungkinkan pengguna menyesuaikan operasi perbandingan x operator ydengan membebani metode perbandingan khusus operan kirinyax.__operator__(y) :

class X:

    def __operator__(self, other):
        # user implementation

Implementasi default dari metode perbandingan khusus dijelaskan dalam dokumentasi Python :

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

Inilah yang disebut metode "perbandingan kaya". Korespondensi antara simbol operator dan nama metode adalah sebagai berikut: x<ypanggilan x.__lt__(y), x<=ypanggilan x.__le__(y), x==ypanggilan x.__eq__(y), x!=ypanggilan x.__ne__(y), x>ypanggilan x.__gt__(y), dan x>=y panggilan x.__ge__(y).

Metode perbandingan yang kaya dapat mengembalikan singleton NotImplementedjika tidak mengimplementasikan operasi untuk pasangan argumen tertentu.

[…]

Tidak ada versi argumen yang ditukar dari metode ini (untuk digunakan ketika argumen kiri tidak mendukung operasi tetapi argumen kanan mendukung); melainkan, __lt__()dan __gt__()merupakan cerminan satu sama lain, __le__()dan __ge__()merupakan cerminan satu sama lain, __eq__()dan __ne__()merupakan cerminan mereka sendiri. Jika operan memiliki tipe yang berbeda, dan tipe operan kanan adalah subkelas langsung atau tidak langsung dari tipe operan kiri, metode yang dipantulkan dari operan kanan memiliki prioritas, jika tidak, metode operan kiri memiliki prioritas. Subclass virtual tidak dianggap.

Karena R = ( R T ) T , perbandingan xRy setara dengan perbandingan sebaliknya yR T x (secara informal dinamai "tercermin" dalam dokumentasi Python). Jadi, ada dua cara untuk menghitung hasil operasi perbandingan x operator y: memanggil salah satu x.__operator__(y)atau y.__operatorT__(x). Python menggunakan strategi komputasi berikut:

  1. Ia memanggil x.__operator__(y)kecuali kelas operan kanan adalah turunan dari kelas operan kiri, dalam hal ini ia memanggil y.__operatorT__(x)( mengizinkan kelas untuk mengganti metode perbandingan kebalikan khusus leluhur mereka ).
  2. Jika operand xdan ytidak didukung (ditunjukkan oleh nilai kembalian NotImplemented), metode converse khusus dipanggil sebagai fallback pertama .
  3. Jika operan xdan ytidak didukung (ditunjukkan oleh nilai kembalian NotImplemented), itu memunculkan pengecualian TypeErrorkecuali untuk operator perbandingan ==dan !=yang menguji masing-masing identitas dan non-identitas operan xdan ysebagai fallback ke - 2 .
  4. Ini mengembalikan hasilnya.

Ini dapat diterjemahkan ke dalam kode Python (dengan nama equntuk ==, neuntuk !=, ltuntuk <, gtuntuk >, leuntuk , <=dan geuntuk >=):

def 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 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 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__}' "
            f"and '{type(right).__name__}'"
        )
    return result

def 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__}' "
            f"and '{type(right).__name__}'"
        )
    return result

def 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__}' "
            f"and '{type(right).__name__}'"
        )
    return result

def 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__}' "
            f"and '{type(right).__name__}'"
        )
    return result

Karena R = ¬ (¬ R ), perbandingan xRy setara dengan perbandingan komplemen ¬ ( x ¬ Ry ). ≠ adalah pelengkap dari =, jadi metode khusus __ne__diimplementasikan dalam istilah metode khusus __eq__untuk operan yang didukung secara default, sedangkan metode perbandingan khusus lainnya diimplementasikan secara independen secara default (fakta bahwa ≤ adalah gabungan dari <dan =, dan ≥ adalah gabungan> dan = secara mengejutkan tidak dipertimbangkan , yang berarti saat ini metode khusus __le__dan __ge__harus diimplementasikan oleh pengguna), seperti yang dijelaskan dalam dokumentasi Python :

Secara default, __ne__()delegasi ke __eq__()dan membalikkan hasil kecuali jika memang demikian NotImplemented. Tidak ada hubungan tersirat lain di antara operator perbandingan, misalnya, kebenaran (x<y or x==y)tidak berarti x<=y.

Ini dapat diterjemahkan ke dalam kode Python:

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

Jadi secara default, operasi perbandingan x operator ymemunculkan pengecualian TypeErrorkecuali untuk operator perbandingan ==dan !=yang mengembalikan masing-masing identitas dan non-identitas operan xdan y.

Maggyero
sumber
Untuk contoh terakhir Anda: "Karena implementasi ini gagal untuk mereplikasi perilaku implementasi default __ne__metode ketika __eq__metode mengembalikan NotImplemented, itu salah." - Amendefinisikan kesetaraan tanpa syarat. Jadi A() == B(),. Jadi A() != B() seharusnya Salah , dan memang benar . Contoh yang diberikan bersifat patologis (yaitu __ne__tidak mengembalikan string, dan __eq__tidak bergantung pada __ne__- melainkan __ne__harus bergantung pada __eq__, yang merupakan ekspektasi default dalam Python 3). Saya masih -1 pada jawaban ini sampai Anda bisa berubah pikiran.
Aaron Hall
@AaronHall Dari referensi bahasa Python : "Metode perbandingan yang kaya dapat mengembalikan singleton NotImplementedjika tidak mengimplementasikan operasi untuk pasangan argumen tertentu. Secara konvensi, Falsedan Truedikembalikan untuk perbandingan yang berhasil. Namun, metode ini dapat mengembalikan nilai apa pun , jadi jika operator perbandingan digunakan dalam konteks Boolean (misalnya, dalam kondisi pernyataan if), Python akan memanggil bool()nilai untuk menentukan apakah hasilnya benar atau salah. "
Maggyero
Contoh terakhir memiliki dua kelas,, Byang mengembalikan string yang benar pada semua pemeriksaan __ne__, dan Ayang mengembalikan Truesemua pemeriksaan untuk __eq__. Ini adalah kontradiksi patologis. Di bawah kontradiksi seperti itu, yang terbaik adalah mengajukan pengecualian. Tanpa pengetahuan B, Atidak ada kewajiban untuk menghormati Bimplementasi __ne__untuk tujuan simetri. Pada saat itu dalam contoh, bagaimana Amengimplementasikan __ne__tidak relevan dengan saya. Temukan kasus praktis dan non-patologis untuk menjelaskan maksud Anda. Saya telah memperbarui jawaban saya untuk menjawab Anda.
Aaron Hall
Kasus penggunaan SQLAlchemy adalah untuk Bahasa Khusus Domain. Jika seseorang mendesain DSL seperti itu, orang dapat membuang semua saran di sini ke luar jendela. Untuk terus menyiksa analogi yang buruk ini, contoh Anda mengharapkan pesawat terbang mundur separuh waktu, dan contoh saya hanya mengharapkan mereka terbang ke depan, dan saya pikir itu adalah keputusan desain yang masuk akal. Saya pikir kekhawatiran yang Anda ajukan tidak beralasan dan mundur.
Aaron Hall
-1

Jika semua __eq__, __ne__, __lt__, __ge__, __le__, dan __gt__masuk akal untuk kelas, kemudian hanya menerapkan __cmp__gantinya. Jika tidak, lakukan seperti yang Anda lakukan, karena sedikit yang dikatakan Daniel DiPaolo (saat saya mengujinya alih-alih mencarinya;))

Karl Knechtel
sumber
12
The __cmp__()metode khusus tidak lagi didukung dengan Python 3.x sehingga Anda harus terbiasa menggunakan operator perbandingan yang kaya.
Don O'Donnell
8
Atau sebagai alternatif jika Anda menggunakan Python 2.7 atau 3.x, dekorator functools.total_ordering juga cukup berguna.
Adam Parkin
Terimakasih atas peringatannya. Saya telah menyadari banyak hal seperti itu dalam satu setengah tahun terakhir. ;)
Karl Knechtel