Bandingkan instance objek untuk kesetaraan dengan atributnya

244

Saya memiliki kelas MyClass, yang berisi dua variabel anggota foodan bar:

class MyClass:
    def __init__(self, foo, bar):
        self.foo = foo
        self.bar = bar

Saya memiliki dua contoh kelas ini, yang masing-masing memiliki nilai identik untuk foodan bar:

x = MyClass('foo', 'bar')
y = MyClass('foo', 'bar')

Namun, ketika saya membandingkannya untuk persamaan, Python mengembalikan False:

>>> x == y
False

Bagaimana saya bisa membuat python menganggap kedua objek ini sama?

Željko Živković
sumber

Jawaban:

355

Anda harus menerapkan metode ini __eq__:

class MyClass:
    def __init__(self, foo, bar):
        self.foo = foo
        self.bar = bar

    def __eq__(self, other): 
        if not isinstance(other, MyClass):
            # don't attempt to compare against unrelated types
            return NotImplemented

        return self.foo == other.foo and self.bar == other.bar

Sekarang ini menghasilkan:

>>> x == y
True

Perhatikan bahwa implementasi __eq__akan secara otomatis membuat instance dari kelas Anda tidak dapat diakses, yang berarti mereka tidak dapat disimpan dalam set dan dikte. Jika Anda tidak memodelkan tipe yang tidak dapat diubah (yaitu jika atribut foodan bardapat mengubah nilai dalam masa hidup objek Anda), maka disarankan untuk membiarkan saja instance Anda sebagai yang tidak dapat pecah.

Jika Anda memodelkan tipe yang tidak dapat diubah, Anda juga harus mengimplementasikan kait model data __hash__:

class MyClass:
    ...

    def __hash__(self):
        # necessary for instances to behave sanely in dicts and sets.
        return hash((self.foo, self.bar))

Solusi umum, seperti ide untuk melakukan perulangan __dict__dan membandingkan nilai, tidak disarankan - solusi ini tidak pernah benar-benar umum karena __dict__mungkin memiliki jenis yang tidak dapat dibandingkan atau tidak dapat dihancurkan yang terkandung di dalamnya.

NB: menyadari bahwa sebelum Python 3, Anda mungkin perlu menggunakan __cmp__bukan __eq__. Pengguna Python 2 mungkin juga ingin menerapkan __ne__, karena perilaku default yang masuk akal untuk ketidaksetaraan (yaitu membalikkan hasil kesetaraan) tidak akan secara otomatis dibuat di Python 2.

e-satis
sumber
2
Saya ingin tahu tentang penggunaan return NotImplemented(bukannya meningkatkan NotImplementedError). Topik itu dibahas di sini: stackoverflow.com/questions/878943/…
init_js
48

Anda menimpa operator perbandingan kaya di objek Anda.

class MyClass:
 def __lt__(self, other):
      # return comparison
 def __le__(self, other):
      # return comparison
 def __eq__(self, other):
      # return comparison
 def __ne__(self, other):
      # return comparison
 def __gt__(self, other):
      # return comparison
 def __ge__(self, other):
      # return comparison

Seperti ini:

    def __eq__(self, other):
        return self._id == other._id
Christopher
sumber
3
Perhatikan bahwa dalam Python 2,5 dan seterusnya, kelas harus menentukan __eq__(), tetapi hanya satu dari __lt__(), __le__(), __gt__(), atau __ge__()dibutuhkan selain itu. Dari situ, Python dapat menyimpulkan metode lain. Lihat functoolsuntuk informasi lebih lanjut.
kba
1
@ kba, kurasa itu tidak benar. Ini mungkin berfungsi untuk functoolsmodul, tetapi tidak berfungsi untuk pembanding standar: MyObj1 != Myobj2hanya akan berfungsi jika __ne__()metode ini diterapkan.
Arel
6
tip khusus tentang functools harus menggunakan @functools.total_orderingdekorator di kelas Anda, maka seperti di atas Anda dapat mendefinisikan adil __eq__dan satu lainnya dan sisanya akan diturunkan
Anentropic
7

Terapkan __eq__metode di kelas Anda; sesuatu seperti ini:

def __eq__(self, other):
    return self.path == other.path and self.title == other.title

Sunting: jika Anda ingin objek Anda untuk membandingkan sama jika dan hanya jika mereka memiliki kamus contoh yang sama:

def __eq__(self, other):
    return self.__dict__ == other.__dict__
Kiv
sumber
Mungkin Anda bermaksud self is othermelihat apakah mereka adalah objek yang sama.
S.Lott
2
-1. Bahkan jika ini adalah dua contoh kamus, Python akan membandingkannya dengan kunci / nilai secara otomatis. Ini bukan Java ...
e-satis
Solusi pertama dapat meningkatkan AttributeError. Anda harus memasukkan baris if hasattr(other, "path") and hasattr(other, "title"):(seperti contoh yang bagus di dokumentasi Python).
Maggyero
5

Sebagai ringkasan:

  1. Disarankan untuk mengimplementasikan __eq__daripada __cmp__, kecuali jika Anda menjalankan python <= 2.0 ( __eq__telah ditambahkan pada 2.1)
  2. Jangan lupa juga menerapkan __ne__(harus seperti return not self.__eq__(other)atau return not self == otherkecuali kasus yang sangat khusus)
  3. Jangan lupa bahwa operator harus diimplementasikan di setiap kelas khusus yang ingin Anda bandingkan (lihat contoh di bawah).
  4. Jika Anda ingin membandingkan dengan objek yang bisa menjadi Tidak Ada, Anda harus mengimplementasikannya. Penerjemah tidak dapat menebaknya ... (lihat contoh di bawah)

    class B(object):
      def __init__(self):
        self.name = "toto"
      def __eq__(self, other):
        if other is None:
          return False
        return self.name == other.name
    
    class A(object):
      def __init__(self):
        self.toto = "titi"
        self.b_inst = B()
      def __eq__(self, other):
        if other is None:
          return False
        return (self.toto, self.b_inst) == (other.toto, other.b_inst)
fievel
sumber
2

Tergantung pada kasus spesifik Anda, Anda dapat melakukan:

>>> vars(x) == vars(y)
True

Lihat kamus Python dari bidang objek

pengguna1338062
sumber
Juga menarik, sementara vars mengembalikan dikt, unittest's assertDictEqual tampaknya tidak berfungsi, meskipun tinjauan visual menunjukkan bahwa mereka, pada kenyataannya, sama. Saya menyiasatinya dengan mengubah dicts menjadi string & membandingkannya: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0))))
Ben
2

Dengan Dataclasses di Python 3.7 (dan di atas), perbandingan instance objek untuk kesetaraan adalah fitur bawaan.

Sebuah backport untuk Dataclasses yang tersedia untuk Python 3.6.

(Py37) nsc@nsc-vbox:~$ python
Python 3.7.5 (default, Nov  7 2019, 10:50:52) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from dataclasses import dataclass
>>> @dataclass
... class MyClass():
...     foo: str
...     bar: str
... 
>>> x = MyClass(foo="foo", bar="bar")
>>> y = MyClass(foo="foo", bar="bar")
>>> x == y
True
Sarath Chandra
sumber
Presentasi Raymond Hettinger 2018 PyCon adalah cara terbaik untuk memulai dengan Python Dataclasses.
Sarath Chandra
1

Saat membandingkan instance objek, __cmp__fungsinya disebut.

Jika operator == tidak bekerja untuk Anda secara default, Anda selalu dapat mendefinisikan kembali __cmp__fungsi untuk objek.

Edit:

Seperti yang telah ditunjukkan, __cmp__fungsi ini sudah tidak digunakan lagi sejak 3.0. Sebaliknya, Anda harus menggunakan metode "perbandingan kaya" .

Silfverstrom
sumber
1
Fungsi cmp tidak digunakan lagi untuk 3.0+
Christopher
1

Jika Anda berurusan dengan satu atau beberapa kelas yang tidak dapat Anda ubah dari dalam, ada cara umum dan sederhana untuk melakukan ini yang juga tidak bergantung pada pustaka khusus:

Metode objek termudah, tidak aman untuk objek yang sangat kompleks

pickle.dumps(a) == pickle.dumps(b)

pickleadalah lib serialisasi yang sangat umum untuk objek Python, dan dengan demikian akan dapat membuat serialisasi apa saja, sungguh. Dalam cuplikan di atas saya membandingkan strdari serial adengan yang dari b. Berbeda dengan metode selanjutnya, yang satu ini memiliki keuntungan juga mengetik kelas kustom.

Kerumitan terbesar: karena pemesanan khusus dan metode pengkodean, pickletidak dapat menghasilkan hasil yang sama untuk objek yang sama , khususnya ketika berhadapan dengan yang lebih kompleks (mis. Daftar instance kelas kustom bersarang) seperti Anda akan sering menemukan di beberapa lib pihak ketiga. Untuk kasus-kasus itu, saya akan merekomendasikan pendekatan yang berbeda:

Metode objek yang menyeluruh, aman untuk apa saja

Anda bisa menulis refleksi rekursif yang akan memberi Anda objek serial, dan kemudian membandingkan hasilnya

from collections.abc import Iterable

BASE_TYPES = [str, int, float, bool, type(None)]


def base_typed(obj):
    """Recursive reflection method to convert any object property into a comparable form.
    """
    T = type(obj)
    from_numpy = T.__module__ == 'numpy'

    if T in BASE_TYPES or callable(obj) or (from_numpy and not isinstance(T, Iterable)):
        return obj

    if isinstance(obj, Iterable):
        base_items = [base_typed(item) for item in obj]
        return base_items if from_numpy else T(base_items)

    d = obj if T is dict else obj.__dict__

    return {k: base_typed(v) for k, v in d.items()}


def deep_equals(*args):
    return all(base_typed(args[0]) == base_typed(other) for other in args[1:])

Sekarang tidak masalah apa objek Anda, kesetaraan yang dalam dijamin untuk bekerja

>>> from sklearn.ensemble import RandomForestClassifier
>>>
>>> a = RandomForestClassifier(max_depth=2, random_state=42)
>>> b = RandomForestClassifier(max_depth=2, random_state=42)
>>> 
>>> deep_equals(a, b)
True

Jumlah yang sebanding tidak masalah juga

>>> c = RandomForestClassifier(max_depth=2, random_state=1000)
>>> deep_equals(a, b, c)
False

Kasus penggunaan saya untuk ini adalah memeriksa kesetaraan yang mendalam di antara beragam model Pembelajaran Mesin yang sudah terlatih dalam tes BDD. Model-model tersebut milik beragam set lib pihak ketiga. Tentunya menerapkan __eq__seperti jawaban lain di sini menyarankan bukan pilihan bagi saya.

Menutupi semua pangkalan

Anda mungkin berada dalam skenario di mana satu atau beberapa kelas khusus yang dibandingkan tidak memiliki __dict__implementasi . Itu tidak umum dengan cara apapun, tapi itu adalah kasus subtipe dalam classifier Acak Hutan sklearn ini: <type 'sklearn.tree._tree.Tree'>. Perlakukan situasi ini dalam kasus per kasus - misalnya secara spesifik , saya memutuskan untuk mengganti konten dari jenis yang diderita dengan konten dari metode yang memberi saya informasi yang representatif pada contoh (dalam kasus ini, __getstate__metode). Untuk itu, baris kedua ke terakhir base_typedmenjadi

d = obj if T is dict else obj.__dict__ if '__dict__' in dir(obj) else obj.__getstate__()

Sunting: demi organisasi, saya mengganti dua baris terakhir base_typeddengan return dict_from(obj), dan menerapkan refleksi yang benar-benar umum untuk mengakomodasi lebih banyak lib yang tidak jelas (Saya melihat Anda, Doc2Vec)

def isproperty(prop, obj):
    return not callable(getattr(obj, prop)) and not prop.startswith('_')


def dict_from(obj):
    """Converts dict-like objects into dicts
    """
    if isinstance(obj, dict):
        # Dict and subtypes are directly converted
        d = dict(obj)

    elif '__dict__' in dir(obj):
        d = obj.__dict__

    elif str(type(obj)) == 'sklearn.tree._tree.Tree':
        # Replaces sklearn trees with their state metadata
        d = obj.__getstate__()

    else:
        # Extract non-callable, non-private attributes with reflection
        kv = [(p, getattr(obj, p)) for p in dir(obj) if isproperty(p, obj)]
        d = {k: v for k, v in kv}

    return {k: base_typed(v) for k, v in d.items()}

Jangan pedulikan metode di atas menghasilkan Trueobjek yang berbeda dengan pasangan nilai kunci yang sama tetapi pesanan kunci / nilai yang berbeda, seperti pada

>>> a = {'foo':[], 'bar':{}}
>>> b = {'bar':{}, 'foo':[]}
>>> pickle.dumps(a) == pickle.dumps(b)
False

Tetapi jika Anda mau, Anda bisa menggunakan sortedmetode bawaan Python sebelumnya.

Julio Cezar Silva
sumber
0

Saya menulis ini dan meletakkannya dalam test/utilsmodul di proyek saya. Untuk kasus-kasus ketika itu bukan kelas, hanya merencanakan dict, ini akan melintasi kedua objek dan memastikan

  1. setiap atribut sama dengan mitranya
  2. Tidak ada atribut yang menggantung (attr yang hanya ada pada satu objek)

Besar ... tidak seksi ... tapi oh, boi berhasil!

def assertObjectsEqual(obj_a, obj_b):

    def _assert(a, b):
        if a == b:
            return
        raise AssertionError(f'{a} !== {b} inside assertObjectsEqual')

    def _check(a, b):
        if a is None or b is None:
            _assert(a, b)
        for k,v in a.items():
            if isinstance(v, dict):
                assertObjectsEqual(v, b[k])
            else:
                _assert(v, b[k])

    # Asserting both directions is more work
    # but it ensures no dangling values on
    # on either object
    _check(obj_a, obj_b)
    _check(obj_b, obj_a)

Anda dapat membersihkannya sedikit dengan menghapus _assertdan hanya menggunakan ol ' asserttetapi kemudian pesan yang Anda dapatkan ketika gagal sangat tidak membantu.

rayepps
sumber
0

Anda harus menerapkan metode ini __eq__:

 class MyClass:
      def __init__(self, foo, bar, name):
           self.foo = foo
           self.bar = bar
           self.name = name

      def __eq__(self,other):
           if not isinstance(other,MyClass):
                return NotImplemented
           else:
                #string lists of all method names and properties of each of these objects
                prop_names1 = list(self.__dict__)
                prop_names2 = list(other.__dict__)

                n = len(prop_names1) #number of properties
                for i in range(n):
                     if getattr(self,prop_names1[i]) != getattr(other,prop_names2[i]):
                          return False

                return True
Victor H. De Oliveira Côrtes
sumber
2
Harap edit jawaban Anda dan tambahkan penjelasan lebih lanjut ke kode Anda, jelaskan mengapa itu berbeda dari sepuluh jawaban lainnya. Pertanyaan ini berumur sepuluh tahun , dan sudah memiliki jawaban yang diterima dan beberapa yang sangat berkualitas tinggi. Tanpa detail tambahan, jawaban Anda memiliki kualitas yang jauh lebih rendah dibandingkan yang lain, dan kemungkinan besar akan diturunkan atau dihapus.
Das_Geek
0

Di bawah ini berfungsi (dalam pengujian terbatas saya) dengan melakukan perbandingan mendalam antara dua hierarki objek. Dalam menangani berbagai kasus termasuk kasus-kasus ketika objek itu sendiri atau atributnya adalah kamus.

def deep_comp(o1:Any, o2:Any)->bool:
    # NOTE: dict don't have __dict__
    o1d = getattr(o1, '__dict__', None)
    o2d = getattr(o2, '__dict__', None)

    # if both are objects
    if o1d is not None and o2d is not None:
        # we will compare their dictionaries
        o1, o2 = o1.__dict__, o2.__dict__

    if o1 is not None and o2 is not None:
        # if both are dictionaries, we will compare each key
        if isinstance(o1, dict) and isinstance(o2, dict):
            for k in set().union(o1.keys() ,o2.keys()):
                if k in o1 and k in o2:
                    if not deep_comp(o1[k], o2[k]):
                        return False
                else:
                    return False # some key missing
            return True
    # mismatched object types or both are scalers, or one or both None
    return o1 == o2

Ini adalah kode yang sangat rumit, jadi tolong tambahkan setiap kasus yang mungkin tidak berfungsi untuk Anda dalam komentar.

Shital Shah
sumber
0
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

    def __repr__(self):
        return str(self.value)

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

node1 = Node(1)
node2 = Node(1)

print(f'node1 id:{id(node1)}')
print(f'node2 id:{id(node2)}')
print(node1 == node2)
>>> node1 id:4396696848
>>> node2 id:4396698000
>>> True
tomgtbst
sumber
-1

Jika Anda ingin mendapatkan perbandingan atribut-per-atribut, dan melihat apakah dan di mana gagal, Anda dapat menggunakan pemahaman daftar berikut:

[i for i,j in 
 zip([getattr(obj_1, attr) for attr in dir(obj_1)],
     [getattr(obj_2, attr) for attr in dir(obj_2)]) 
 if not i==j]

Keuntungan tambahan di sini adalah Anda bisa menekannya satu baris dan masukkan di jendela "Evaluate Expression" saat melakukan debug di PyCharm.

DalyaG
sumber
-3

Saya mencoba contoh awal (lihat 7 di atas) dan tidak berhasil di ipython. Perhatikan bahwa cmp (obj1, obj2) mengembalikan "1" ketika diimplementasikan menggunakan dua instance objek yang identik. Anehnya ketika saya memodifikasi salah satu nilai atribut dan membandingkannya, menggunakan cmp (obj1, obj2) objek terus mengembalikan "1". (mendesah...)

Ok, jadi yang perlu Anda lakukan adalah mengulangi dua objek dan membandingkan setiap atribut menggunakan tanda ==.

pengguna2215595
sumber
Setidaknya dalam Python 2.7, objek dibandingkan dengan identitas secara default. Itu berarti untuk CPython dalam kata-kata praktis yang mereka bandingkan dengan alamat memori mereka. Itu sebabnya cmp (o1, o2) mengembalikan 0 hanya ketika "o1 adalah o2" dan secara konsisten 1 atau -1 tergantung pada nilai id (o1) dan id (o2)
yacc143
-6

Contoh kelas jika dibandingkan dengan == datang ke non-sama. Cara terbaik adalah dengan memasukkan fungsi cmp ke kelas Anda yang akan melakukan hal-hal tersebut.

Jika Anda ingin melakukan perbandingan dengan konten Anda cukup menggunakan cmp (obj1, obj2)

Dalam kasus Anda cmp (doc1, doc2) Ini akan mengembalikan -1 jika kontennya sama.

asb
sumber