Cara elegan untuk mendukung kesetaraan ("kesetaraan") di kelas Python

421

Saat menulis kelas khusus seringkali penting untuk memungkinkan kesetaraan melalui operator ==dan !=. Dalam Python, ini dimungkinkan dengan mengimplementasikan __eq__dan __ne__metode khusus, masing-masing. Cara termudah yang saya temukan untuk melakukan ini adalah metode berikut:

class Foo:
    def __init__(self, item):
        self.item = item

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

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

Apakah Anda tahu cara yang lebih elegan untuk melakukan ini? Apakah Anda mengetahui adanya kerugian tertentu dalam menggunakan metode perbandingan __dict__di atas?

Catatan : Sedikit klarifikasi - kapan __eq__dan __ne__tidak ditentukan, Anda akan menemukan perilaku ini:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

Artinya, a == bdievaluasi Falsekarena benar-benar berjalan a is b, tes identitas (yaitu, "Apakah aobjek yang sama b?").

Saat __eq__dan __ne__ditentukan, Anda akan menemukan perilaku ini (yang kami kejar):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
hasil
sumber
6
+1, karena saya tidak tahu bahwa dict menggunakan persamaan anggota untuk ==, saya berasumsi hanya menghitungnya sama dengan dict objek yang sama. Saya kira ini jelas karena Python memiliki isoperator untuk membedakan identitas objek dari perbandingan nilai.
SingleNegationElimination
5
Saya pikir jawaban yang diterima dikoreksi atau dipindahkan ke jawaban Algorias, sehingga pemeriksaan tipe ketat diterapkan.
maks
1
Pastikan juga hash diganti stackoverflow.com/questions/1608842/...
Alex Punnen

Jawaban:

328

Pertimbangkan masalah sederhana ini:

class Number:

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


n1 = Number(1)
n2 = Number(1)

n1 == n2 # False -- oops

Jadi, Python secara default menggunakan pengidentifikasi objek untuk operasi perbandingan:

id(n1) # 140400634555856
id(n2) # 140400634555920

Mengganti __eq__fungsi tampaknya dapat menyelesaikan masalah:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return False


n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3

Dalam Python 2 , selalu ingat untuk mengganti __ne__fungsi juga, seperti yang dinyatakan oleh dokumentasi :

Tidak ada hubungan yang tersirat di antara operator pembanding. Kebenaran x==ytidak menyiratkan bahwa x!=yitu salah. Dengan demikian, ketika mendefinisikan __eq__(), kita juga harus mendefinisikan __ne__()sehingga operator akan berperilaku seperti yang diharapkan.

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    return not self.__eq__(other)


n1 == n2 # True
n1 != n2 # False

Dalam Python 3 , ini tidak lagi diperlukan, karena dokumentasi menyatakan:

Secara default, __ne__()delegasikan ke __eq__()dan membalikkan hasilnya kecuali jika itu benar NotImplemented. Tidak ada hubungan tersirat lainnya di antara operator pembanding, misalnya, kebenaran (x<y or x==y)tidak menyiratkan x<=y.

Tapi itu tidak menyelesaikan semua masalah kita. Mari kita tambahkan subkelas:

class SubNumber(Number):
    pass


n3 = SubNumber(1)

n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False

Catatan: Python 2 memiliki dua jenis kelas:

  • kelas gaya klasik (atau gaya lama ), yang tidak mewarisi dariobjectdan yang dinyatakan sebagaiclass A:,class A():atau diclass A(B):manaBkelas gaya klasik;

  • kelas gaya baru , yang mewarisi dariobjectdan yang dinyatakan sebagaiclass A(object)atau diclass A(B):manaBkelas gaya baru. Python 3 hanya memiliki kelas gaya baru yang dinyatakan sebagaiclass A:,class A(object):atauclass A(B):.

Untuk kelas gaya klasik, operasi perbandingan selalu memanggil metode operan pertama, sedangkan untuk kelas gaya baru, selalu memanggil metode operan subkelas, terlepas dari urutan operan .

Jadi di sini, jika Numberkelas gaya klasik:

  • n1 == n3panggilan n1.__eq__;
  • n3 == n1panggilan n3.__eq__;
  • n1 != n3panggilan n1.__ne__;
  • n3 != n1panggilan n3.__ne__.

Dan jika Numberkelas gaya baru:

  • keduanya n1 == n3dan n3 == n1telepon n3.__eq__;
  • keduanya n1 != n3dan n3 != n1menelepon n3.__ne__.

Untuk memperbaiki masalah non-komutatif dari ==dan !=operator untuk kelas gaya klasik Python 2, __eq__dan __ne__metode harus mengembalikan NotImplementednilai ketika jenis operan tidak didukung. The dokumentasi mendefinisikan NotImplementednilai sebagai:

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 operator.) Nilai kebenarannya benar.

Dalam hal ini operator mendelegasikan operasi perbandingan dengan metode yang direfleksikan dari operan lain . The dokumentasi mendefinisikan tercermin metode sebagai:

Tidak ada versi argumen bertukar metode ini (untuk digunakan ketika argumen kiri tidak mendukung operasi tetapi argumen yang benar tidak); lebih, __lt__()dan __gt__()merupakan cerminan satu sama lain, __le__()dan __ge__()merupakan cerminan satu sama lain, dan __eq__()dan __ne__()merupakan cerminan mereka sendiri.

Hasilnya terlihat seperti ini:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return NotImplemented

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    x = self.__eq__(other)
    if x is NotImplemented:
        return NotImplemented
    return not x

Mengembalikan NotImplementednilai bukan Falsemerupakan hal yang benar untuk dilakukan bahkan untuk kelas gaya baru jika komutatif dari ==dan !=operator yang diinginkan ketika operan dari jenis yang tidak terkait (tidak ada warisan).

Apakah kita sudah sampai? Tidak terlalu. Berapa nomor unik yang kita miliki?

len(set([n1, n2, n3])) # 3 -- oops

Set menggunakan hash objek, dan secara default Python mengembalikan hash dari pengenal objek. Mari kita coba menimpanya:

def __hash__(self):
    """Overrides the default implementation"""
    return hash(tuple(sorted(self.__dict__.items())))

len(set([n1, n2, n3])) # 1

Hasil akhirnya terlihat seperti ini (saya menambahkan beberapa pernyataan di akhir untuk validasi):

class Number:

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

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Number):
            return self.number == other.number
        return NotImplemented

    def __ne__(self, other):
        """Overrides the default implementation (unnecessary in Python 3)"""
        x = self.__eq__(other)
        if x is not NotImplemented:
            return not x
        return NotImplemented

    def __hash__(self):
        """Overrides the default implementation"""
        return hash(tuple(sorted(self.__dict__.items())))


class SubNumber(Number):
    pass


n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)

assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1

assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1

assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1

assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2
Tal Weiss
sumber
3
hash(tuple(sorted(self.__dict__.items())))tidak akan berfungsi jika ada objek yang tidak hash di antara nilai-nilai dari self.__dict__(yaitu, jika salah satu atribut objek diatur ke, katakanlah, a list).
Maks.
3
Benar, tetapi kemudian jika Anda memiliki objek yang bisa berubah seperti itu di vars Anda () kedua objek tidak benar-benar sama ...
Tal Weiss
12
Ringkasan yang bagus, tetapi Anda harus menerapkan __ne__menggunakan ==alih-alih__eq__ .
Florian Brucker
1
Tiga komentar: 1. Dalam Python 3, tidak perlu menerapkan __ne__lagi: "Secara default, __ne__()delegasikan ke __eq__()dan membalikkan hasilnya kecuali jika itu NotImplemented". 2. Jika salah satu masih ingin menerapkan __ne__, implementasi yang lebih generik (yang digunakan oleh Python 3 saya pikir) adalah: x = self.__eq__(other); if x is NotImplemented: return x; else: return not x. 3. Yang diberikan __eq__dan __ne__implementasi bersifat suboptimal: if isinstance(other, type(self)):memberikan 22 __eq__dan 10 __ne__panggilan, sementara if isinstance(self, type(other)):akan memberikan 16 __eq__dan 6 __ne__panggilan.
Maggyero
4
Dia bertanya tentang keanggunan, tetapi dia menjadi kuat.
GregNash
201

Anda harus berhati-hati dengan warisan:

>>> class Foo:
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

>>> class Bar(Foo):pass

>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False

Periksa jenis lebih ketat, seperti ini:

def __eq__(self, other):
    if type(other) is type(self):
        return self.__dict__ == other.__dict__
    return False

Selain itu, pendekatan Anda akan bekerja dengan baik, itulah metode khusus yang ada.

Algorias
sumber
Ini poin yang bagus. Saya kira perlu dicatat bahwa sub-classing built-in tipe masih memungkinkan untuk kesetaraan baik arah, dan memeriksa bahwa itu adalah tipe yang sama bahkan mungkin tidak diinginkan.
Gotgenes
12
Saya sarankan untuk mengembalikan NotImplemented jika jenisnya berbeda, mendelegasikan perbandingan ke rhs.
maks
4
@max perbandingan tidak harus dilakukan sisi kiri (LHS) ke sisi kanan (RHS), kemudian RHS ke LHS; lihat stackoverflow.com/a/12984987/38140 . Namun, kembali NotImplementedseperti yang Anda sarankan akan selalu menyebabkan superclass.__eq__(subclass), yang merupakan perilaku yang diinginkan.
dapatkan
4
Jika Anda memiliki banyak anggota, dan tidak banyak salinan objek yang ada, maka sebaiknya tambahkan tes identitas awal if other is self. Ini menghindari perbandingan kamus yang lebih panjang, dan bisa menjadi penghematan besar ketika objek digunakan sebagai kunci kamus.
Dane White
2
Dan jangan lupa untuk mengimplementasikan__hash__()
Dane White
161

Cara Anda menggambarkan adalah cara saya selalu melakukannya. Karena ini benar-benar generik, Anda selalu dapat memecah fungsi itu menjadi kelas mixin dan mewarisinya di kelas di mana Anda menginginkan fungsi itu.

class CommonEqualityMixin(object):

    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)

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

class Foo(CommonEqualityMixin):

    def __init__(self, item):
        self.item = item
cdleary
sumber
6
+1: Pola strategi untuk memungkinkan penggantian mudah di subkelas.
S.Lott
3
isinstance menyebalkan. Mengapa memeriksanya? Mengapa tidak mandiri saja .__ dict__ == lainnya .__ dict__?
nosklo
3
@nosklo: Saya tidak mengerti .. bagaimana jika dua objek dari kelas yang benar-benar tidak berhubungan memiliki atribut yang sama?
maks
1
Saya pikir nokslo menyarankan untuk melewatkan isinstance. Dalam hal ini Anda tidak lagi tahu apakah otherdari subkelas self.__class__.
maks
10
Masalah lain dengan __dict__perbandingan adalah bagaimana jika Anda memiliki atribut yang tidak ingin Anda pertimbangkan dalam definisi kesetaraan Anda (misalnya, misalnya id objek unik, atau metadata seperti cap waktu yang dibuat).
Adam Parkin
14

Bukan jawaban langsung tetapi tampaknya cukup relevan untuk ditempelkan karena menghemat sedikit kebosanan verbose pada kesempatan. Dipotong langsung dari dokumen ...


functools.total_ordering (cls)

Mengingat kelas mendefinisikan satu atau lebih metode pemesanan perbandingan kaya, dekorator kelas ini memasok sisanya. Ini menyederhanakan upaya yang terlibat dalam menentukan semua operasi perbandingan kaya yang mungkin:

Kelas harus menentukan salah satu dari __lt__(), __le__(), __gt__(), atau __ge__(). Selain itu, kelas harus menyediakan __eq__()metode.

Baru di versi 2.7

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))
John Mee
sumber
1
Namun total_ordering memiliki jebakan halus: regebro.wordpress.com/2010/12/13/… . Waspadalah !
Mr_and_Mrs_D
8

Anda tidak harus menimpa keduanya __eq__dan __ne__Anda dapat menimpa hanya __cmp__tetapi ini akan membuat implikasi pada hasil ==,! ==, <,> dan seterusnya.

istes untuk identitas objek. Ini berarti a isakan berada Truedalam kasus ketika a dan b keduanya memegang referensi ke objek yang sama. Dalam python Anda selalu memegang referensi ke objek dalam variabel bukan objek yang sebenarnya, jadi pada dasarnya untuk a adalah benar, objek di dalamnya harus berada di lokasi memori yang sama. Bagaimana dan yang paling penting mengapa Anda harus mengabaikan perilaku ini?

Sunting: Saya tidak tahu __cmp__telah dihapus dari python 3 jadi hindarilah.

Vasil
sumber
Karena terkadang Anda memiliki definisi kesetaraan yang berbeda untuk objek Anda.
Ed S.
operator is memberi Anda jawaban juru bahasa untuk identitas objek, tetapi Anda masih bebas untuk mengekspresikan pandangan Anda tentang kesetaraan dengan mengesampingkan cmp
Vasil
7
Dalam Python 3, "Fungsi cmp () hilang, dan metode khusus __cmp __ () tidak lagi didukung." is.gd/aeGv
gotgenes
4

Dari jawaban ini: https://stackoverflow.com/a/30676267/541136 Saya telah menunjukkan itu, sementara itu benar untuk mendefinisikan __ne__dalam hal __eq__- bukan

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

kamu harus menggunakan:

def __ne__(self, other):
    return not self == other
Aaron Hall
sumber
2

Saya pikir dua istilah yang Anda cari adalah persamaan (==) dan identitas . Sebagai contoh:

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True       <-- a and b have values which are equal
>>> a is b
False      <-- a and b are not the same list object
terlalu banyak php
sumber
1
Mungkin, kecuali bahwa seseorang dapat membuat kelas yang hanya membandingkan dua item pertama dalam dua daftar, dan jika item-item itu sama, ia mengevaluasi ke True. Ini adalah kesetaraan, saya pikir, bukan kesetaraan. Sangat valid dalam persamaan , masih.
Gotgenes
Saya setuju, bagaimanapun, bahwa "adalah" adalah ujian identitas.
Gotgenes
1

Tes 'is' akan menguji identitas menggunakan fungsi builtin 'id ()' yang pada dasarnya mengembalikan alamat memori objek dan oleh karena itu tidak kelebihan beban.

Namun dalam hal menguji persamaan kelas Anda mungkin ingin sedikit lebih ketat tentang tes Anda dan hanya membandingkan atribut data di kelas Anda:

import types

class ComparesNicely(object):

    def __eq__(self, other):
        for key, value in self.__dict__.iteritems():
            if (isinstance(value, types.FunctionType) or 
                    key.startswith("__")):
                continue

            if key not in other.__dict__:
                return False

            if other.__dict__[key] != value:
                return False

         return True

Kode ini hanya akan membandingkan data anggota yang tidak berfungsi di kelas Anda serta melewatkan apa pun yang bersifat pribadi yang umumnya Anda inginkan. Dalam kasus Plain Old Python Objects saya memiliki kelas dasar yang mengimplementasikan __init__, __str__, __repr__ dan __eq__ sehingga objek POPO saya tidak membawa beban dari semua logika ekstra (dan dalam banyak kasus identik).

mcrute
sumber
Agak nitpicky, tetapi 'is' menguji menggunakan id () hanya jika Anda belum mendefinisikan fungsi anggota is_ () Anda (2.3+). [ docs.python.org/library/operator.html]
dihabiskan
Saya berasumsi dengan "menimpa" Anda sebenarnya maksud monyet-patching modul operator. Dalam hal ini pernyataan Anda tidak sepenuhnya akurat. Modul operator disediakan untuk kenyamanan dan mengganti metode-metode tersebut tidak mempengaruhi perilaku operator "is". Perbandingan menggunakan "is" selalu menggunakan id () dari suatu objek untuk perbandingan, perilaku ini tidak dapat ditimpa. Juga fungsi anggota is_ tidak berpengaruh pada perbandingan.
mcrute
mcrute - Saya berbicara terlalu cepat (dan salah), Anda benar sekali.
dihabiskan
Ini adalah solusi yang sangat bagus, terutama ketika __eq__akan dinyatakan dalam CommonEqualityMixin(lihat jawaban lainnya). Saya menemukan ini sangat berguna ketika membandingkan contoh kelas yang berasal dari Base di SQLAlchemy. Untuk tidak membandingkan _sa_instance_statesaya berubah key.startswith("__")):menjadi key.startswith("_")):. Saya juga memiliki beberapa referensi di dalamnya dan jawaban dari Algorias menghasilkan rekursi yang tak ada habisnya. Jadi saya menamai semua referensi kembali dimulai dengan '_'sehingga mereka juga dilewati selama perbandingan. CATATAN: dalam Python 3.x ubah iteritems()menjadi items().
Wookie88
@mcrute Biasanya, __dict__instance tidak memiliki apa pun yang dimulai __kecuali ditentukan oleh pengguna. Hal-hal seperti __class__,, __init__dll. Tidak ada dalam instance __dict__, melainkan dalam kelasnya ' __dict__. OTOH, atribut pribadi dapat dengan mudah dimulai __dan mungkin harus digunakan __eq__. Bisakah Anda mengklarifikasi apa yang sebenarnya Anda coba hindari ketika melewatkan __atribut -prefixed?
Maks.
1

Alih-alih menggunakan subclassing / mixin, saya lebih suka menggunakan dekorator kelas generik

def comparable(cls):
    """ Class decorator providing generic comparison functionality """

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__

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

    cls.__eq__ = __eq__
    cls.__ne__ = __ne__
    return cls

Pemakaian:

@comparable
class Number(object):
    def __init__(self, x):
        self.x = x

a = Number(1)
b = Number(1)
assert a == b
bluenote10
sumber
0

Ini menggabungkan komentar pada jawaban Algorias, dan membandingkan objek dengan satu atribut karena saya tidak peduli dengan keseluruhan dikt. hasattr(other, "id")pasti benar, tetapi saya tahu itu karena saya meletakkannya di konstruktor.

def __eq__(self, other):
    if other is self:
        return True

    if type(other) is not type(self):
        # delegate to superclass
        return NotImplemented

    return other.id == self.id
Noumenon
sumber