เปรียบเทียบอินสแตนซ์ของวัตถุเพื่อความเท่าเทียมกันโดยคุณลักษณะของพวกเขา


244

ฉันมีคลาสMyClassซึ่งมีตัวแปรสมาชิกสองตัวfooและbar:

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

ฉันมีสองกรณีของชั้นนี้แต่ละที่มีค่าเหมือนกันfooและbar:

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

อย่างไรก็ตามเมื่อฉันเปรียบเทียบมันเพื่อความเท่าเทียมกัน Python จะคืนค่าFalse:

>>> x == y
False

ฉันจะทำให้หลามพิจารณาว่าวัตถุทั้งสองนี้เท่ากันได้อย่างไร

คำตอบ:


354

คุณควรใช้วิธีการ__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

ตอนนี้มันออก:

>>> x == y
True

โปรดทราบว่าการใช้งาน__eq__จะทำให้อินสแตนซ์ของชั้นเรียนของคุณไม่สามารถล้างได้ซึ่งหมายความว่าพวกเขาไม่สามารถจัดเก็บเป็นชุดและแสดง หากคุณไม่ได้สร้างแบบจำลองที่ไม่เปลี่ยนรูปแบบ (เช่นถ้าแอตทริบิวต์fooและbarอาจเปลี่ยนค่าภายในอายุการใช้งานของวัตถุของคุณ) ก็แนะนำให้เพียงแค่ปล่อยอินสแตนซ์ของคุณเป็น unhashable

หากคุณกำลังสร้างโมเดลชนิดที่ไม่เปลี่ยนรูปแบบคุณควรใช้ hook ของ datamodel __hash__:

class MyClass:
    ...

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

วิธีแก้ปัญหาทั่วไปเช่นแนวคิดของการวนซ้ำ__dict__และเปรียบเทียบค่าไม่แนะนำ - มันไม่สามารถเป็นเรื่องทั่วไปได้อย่างแท้จริงเพราะ__dict__อาจมีประเภทที่ไม่มีใครเทียบได้หรือ unhashable อยู่ภายใน

หมายเหตุ: ทราบว่าก่อนที่จะหลาม 3 คุณอาจต้องใช้แทน__cmp__ __eq__ผู้ใช้ Python 2 อาจต้องการนำไปใช้__ne__เนื่องจากพฤติกรรมเริ่มต้นที่สมเหตุสมผลสำหรับความไม่เท่าเทียมกัน (เช่นการกลับผลลัพธ์ความเสมอภาค) จะไม่ถูกสร้างขึ้นโดยอัตโนมัติใน Python 2


2
ฉันอยากรู้เกี่ยวกับการใช้return NotImplemented(แทนที่จะเพิ่มNotImplementedError) ครอบคลุมหัวข้อดังกล่าวที่นี่: stackoverflow.com/questions/878943/…
init_js

48

คุณแทนที่ตัวดำเนินการเปรียบเทียบที่รวยในวัตถุของคุณ

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

แบบนี้:

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

3
โปรดทราบว่าในหลาม 2.5 เป็นต้นไปชั้นต้องกำหนด__eq__()แต่เพียงอย่างใดอย่างหนึ่ง__lt__(), __le__(), __gt__()หรือ__ge__()เป็นสิ่งจำเป็นนอกจากนั้น จากนั้น Python สามารถอนุมานวิธีอื่นได้ ดูfunctoolsข้อมูลเพิ่มเติม
kba

1
@kba ฉันไม่คิดว่าจริง สิ่งนี้อาจใช้งานได้กับfunctoolsโมดูล แต่ไม่ทำงานสำหรับตัวเปรียบเทียบมาตรฐาน: MyObj1 != Myobj2จะทำงานได้ก็ต่อเมื่อ__ne__()มีการนำวิธีการไปใช้
Arel

6
เคล็ดลับเฉพาะเกี่ยวกับ functools ควรใช้@functools.total_orderingมัณฑนากรในชั้นเรียนของคุณจากนั้นคุณสามารถกำหนดเพียง__eq__หนึ่งและอื่น ๆ และส่วนที่เหลือจะได้รับ
Anentropic

7

ใช้__eq__วิธีการในชั้นเรียนของคุณ บางสิ่งเช่นนี้

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

แก้ไข: หากคุณต้องการให้วัตถุของคุณเปรียบเทียบเท่ากันหากว่าพวกเขามีพจนานุกรมที่เท่ากัน:

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

บางทีคุณอาจself is otherต้องการดูว่าพวกเขาเป็นวัตถุเดียวกัน
S.Lott

2
-1 แม้ว่านี่จะเป็นสองอินสแตนซ์ของพจนานุกรม Python จะทำการเปรียบเทียบโดยใช้คีย์ / ค่าโดยอัตโนมัติ นี่ไม่ใช่จาวา ...
4994 e-ภิรมย์

AttributeErrorวิธีการแก้ปัญหาครั้งแรกที่สามารถยก คุณต้องแทรกบรรทัดif hasattr(other, "path") and hasattr(other, "title"):(เช่นตัวอย่างที่ดีนี้ในเอกสาร Python)
Maggyero

5

โดยสรุป:

  1. แนะนำให้ใช้__eq__มากกว่า__cmp__ยกเว้นถ้าคุณใช้งาน python <= 2.0 (__eq__เพิ่มใน 2.1)
  2. อย่าลืมนำไปใช้ด้วย__ne__(ควรเป็นสิ่งที่ชอบreturn not self.__eq__(other)หรือreturn not self == otherยกเว้นกรณีพิเศษมาก)
  3. อย่าลืมว่าต้องใช้ตัวดำเนินการในแต่ละคลาสแบบกำหนดเองที่คุณต้องการเปรียบเทียบ (ดูตัวอย่างด้านล่าง)
  4. หากคุณต้องการเปรียบเทียบกับวัตถุที่สามารถเป็น None คุณจะต้องใช้มัน ล่ามไม่สามารถเดาได้ ... (ดูตัวอย่างด้านล่าง)

    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)

2

คุณสามารถทำได้:

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

ดูพจนานุกรม Python จากฟิลด์ของวัตถุ


สิ่งที่น่าสนใจเช่นกันในขณะที่ vars ส่งคืนคำบอกเล่า แต่ unertest's assertDictEqual ดูเหมือนจะไม่ทำงานแม้ว่าการตรวจสอบด้วยภาพจะแสดงให้เห็นว่าพวกเขาเป็นจริง ฉันได้สิ่งนี้โดยเปลี่ยน dicts เป็นสตริงและเปรียบเทียบกับ: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0)))
Ben

2

ด้วยDataclasses ใน Python 3.7 (และสูงกว่า) การเปรียบเทียบอินสแตนซ์ของวัตถุเพื่อความเท่าเทียมกันคือคุณสมบัติ inbuilt

ย้ายกลับสำหรับ Dataclassesสามารถใช้ได้สำหรับการหลาม 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

การนำเสนอ PyCon ของ Raymond Hettinger's 2018เป็นวิธีที่ยอดเยี่ยมในการเริ่มต้นกับ Python Dataclasses
Sarath Chandra

1

เมื่อเปรียบเทียบอินสแตนซ์ของวัตถุ__cmp__ฟังก์ชันจะถูกเรียกใช้

หากตัวดำเนินการ == ไม่ทำงานสำหรับคุณโดยค่าเริ่มต้นคุณสามารถกำหนดใหม่ได้ตลอดเวลา __cmp__ฟังก์ชันสำหรับวัตถุได้ตลอดเวลา

แก้ไข:

ตามที่ได้ชี้แจงไว้__cmp__ฟังก์ชั่นจะเลิกใช้ตั้งแต่ 3.0 คุณควรใช้วิธี"การเปรียบเทียบที่สมบูรณ์"แทน


1
cmpฟังก์ชั่นจะเลิกสำหรับ 3.0+
คริสโต

1

หากคุณกำลังจัดการกับคลาสตั้งแต่หนึ่งคลาสขึ้นไปซึ่งคุณไม่สามารถเปลี่ยนจากภายในได้มีวิธีทั่วไปและวิธีง่าย ๆ ที่จะทำเช่นนี้ซึ่งไม่ได้ขึ้นอยู่กับไลบรารีที่แตกต่างกัน:

วิธีที่ง่ายที่สุดไม่ปลอดภัยสำหรับวัตถุที่ซับซ้อนมาก

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

pickleคือการทำให้เป็นอนุกรมทั่วไปสำหรับวัตถุ Python และจะสามารถทำให้เป็นอนุกรมอะไรก็ได้จริง ๆ ในตัวอย่างข้างต้นผมเปรียบเทียบstrจากอันดับหนึ่งจากa bวิธีนี้แตกต่างจากวิธีถัดไปวิธีนี้มีข้อดีของการตรวจสอบคลาสแบบกำหนดเองด้วยเช่นกัน

ความยุ่งยากที่ใหญ่ที่สุด: เนื่องจากการสั่งซื้อเฉพาะและวิธีการเข้ารหัส [de / en] pickleอาจไม่ให้ผลลัพธ์ที่เหมือนกันสำหรับวัตถุที่เท่ากันโดยเฉพาะอย่างยิ่งเมื่อต้องจัดการกับวัตถุที่ซับซ้อนมากขึ้น (เช่นรายการของอินสแตนซ์คลาสแบบกำหนดเองซ้อน) ใน libs บุคคลที่สามบางอย่าง สำหรับกรณีเหล่านั้นฉันขอแนะนำวิธีการอื่น:

วิธีการที่ปลอดภัยสำหรับวัตถุใด ๆ อย่างละเอียด

คุณสามารถเขียนภาพสะท้อนแบบเรียกซ้ำซึ่งจะให้วัตถุที่เป็นอนุกรมแล้วเปรียบเทียบผลลัพธ์

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:])

ตอนนี้ไม่สำคัญว่าวัตถุของคุณจะเป็นอะไร

>>> 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

จำนวนของการเปรียบเทียบไม่สำคัญเช่นกัน

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

กรณีการใช้งานของฉันคือการตรวจสอบความเท่าเทียมกันอย่างลึกล้ำระหว่างชุดการเรียนรู้ของเครื่องเรียนรู้ที่ผ่านการฝึกอบรมแล้วในการทดสอบ BDD โมเดลเป็นของ libs บุคคลที่สามที่หลากหลาย ดำเนินการอย่างแน่นอน__eq__อย่างเหมือนกับคำตอบอื่น ๆ ที่นี่แนะนำไม่ใช่ตัวเลือกสำหรับฉัน

ครอบคลุมทุกฐาน

คุณอาจจะอยู่ในสถานการณ์ที่หนึ่งหรือมากกว่าหนึ่งของการเรียนที่กำหนดเองที่มีการเทียบไม่ได้มี__dict__การดำเนินการ นั่นคือไม่ธรรมดาโดยวิธีใด ๆ แต่มันเป็นกรณีของการย่อยภายใน sklearn <type 'sklearn.tree._tree.Tree'>ของสุ่มป่าลักษณนามที่: ปฏิบัติต่อสถานการณ์เหล่านี้เป็นกรณี ๆ ไป - เช่นโดยเฉพาะฉันตัดสินใจที่จะแทนที่เนื้อหาของประเภทที่เป็นปัญหาด้วยเนื้อหาของวิธีการที่ให้ข้อมูลตัวแทนแก่ฉันในอินสแตนซ์ (ในกรณีนี้คือ__getstate__วิธีการ) สำหรับเช่นแถวที่สองถึงครั้งสุดท้ายในbase_typedกลายเป็น

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

แก้ไข: เพื่อประโยชน์ขององค์กรฉันได้แทนที่สองบรรทัดสุดท้ายbase_typedด้วยreturn dict_from(obj)และใช้การสะท้อนทั่วไปเพื่อรองรับ libs ที่คลุมเครือมากขึ้น (ฉันกำลังมองหาคุณ 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()}

อย่าคำนึงถึงวิธีการใด ๆ ข้างต้นที่ให้ผลลัพธ์Trueสำหรับออบเจ็กต์ที่ต่างกันด้วยคู่คีย์ - ค่าเดียวกัน

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

แต่ถ้าคุณต้องการให้คุณใช้sortedวิธีการในตัวของ Python ล่วงหน้า


0

ฉันเขียนสิ่งนี้และวางไว้ในtest/utilsโมดูลในโครงการของฉัน สำหรับกรณีที่ไม่ได้เป็นคลาสเพียงวางแผน ol 'dict นี้จะสำรวจวัตถุทั้งสองและให้แน่ใจว่า

  1. ทุกแอตทริบิวต์เท่ากับของมัน
  2. ไม่มีแอตทริบิวต์ที่ห้อยอยู่ (attrs ที่มีอยู่ในวัตถุเดียวเท่านั้น)

มันใหญ่ ... มันไม่เซ็กซี่ ... แต่โอ้ boi ใช้งานได้!

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)

คุณสามารถทำความสะอาดได้เล็กน้อยโดยลบ_assertและเพียงแค่ใช้ ol ธรรมดา ' assertแต่ข้อความที่คุณได้รับเมื่อมันล้มเหลวจะไม่ช่วยเหลือมาก


0

คุณควรใช้วิธีการ__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

2
โปรดแก้ไขคำตอบของคุณและเพิ่มคำอธิบายเพิ่มเติมลงในรหัสของคุณอธิบายว่าเพราะเหตุใดจึงแตกต่างจากคำตอบอื่น ๆ สิบข้อ คำถามนี้มีอายุสิบปีแล้วและมีคำตอบที่ยอมรับแล้วและมีคำตอบคุณภาพสูงหลายข้อ หากไม่มีรายละเอียดเพิ่มเติมคำตอบของคุณจะมีคุณภาพต่ำกว่ามากเมื่อเปรียบเทียบกับคำตอบอื่น ๆ และมักจะถูกลดระดับหรือลบ
Das_Geek

0

ผลงานด้านล่าง (ในการทดสอบที่ จำกัด ของฉัน) โดยทำการเปรียบเทียบอย่างลึกระหว่างสองลำดับชั้นวัตถุ ในการจัดการกรณีต่างๆรวมถึงกรณีเมื่อวัตถุตัวเองหรือคุณลักษณะของพวกเขาเป็นพจนานุกรม

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

นี่เป็นรหัสที่ยุ่งยากมากดังนั้นโปรดเพิ่มกรณีใด ๆ ที่อาจไม่เหมาะกับคุณในความคิดเห็น


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

-1

หากคุณต้องการได้รับการเปรียบเทียบคุณสมบัติแบบแอตทริบิวต์และดูว่าและที่ไหนมันล้มเหลวคุณสามารถใช้รายการความเข้าใจต่อไปนี้:

[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]

ข้อได้เปรียบพิเศษที่นี่คือคุณสามารถบีบมันหนึ่งบรรทัดและป้อนในหน้าต่าง "ประเมินการแสดงออก" เมื่อทำการดีบั๊กใน PyCharm


-3

ฉันลองตัวอย่างเริ่มต้น (ดู 7 ด้านบน) และมันไม่ทำงานใน ipython โปรดทราบว่า cmp (obj1, obj2) จะส่งกลับค่า "1" เมื่อใช้งานโดยใช้สองอินสแตนซ์ของวัตถุที่เหมือนกัน ผิดปกติพอเมื่อฉันปรับเปลี่ยนค่าแอตทริบิวต์อย่างใดอย่างหนึ่งและ recompare โดยใช้ cmp (obj1, obj2) วัตถุยังคงส่งคืน "1" (ถอนหายใจ ... )

ตกลงดังนั้นสิ่งที่คุณต้องทำคือทำซ้ำสองวัตถุและเปรียบเทียบแต่ละคุณลักษณะโดยใช้เครื่องหมาย ==


ใน Python 2.7 เป็นอย่างน้อยวัตถุจะถูกเปรียบเทียบตามเอกลักษณ์โดยค่าเริ่มต้น นั่นหมายถึง CPython ในคำศัพท์เชิงปฏิบัติที่เปรียบเทียบโดยที่อยู่หน่วยความจำ นั่นเป็นสาเหตุที่ cmp (o1, o2) ส่งคืนค่า 0 เฉพาะเมื่อ "o1 คือ o2" และสม่ำเสมอ 1 หรือ -1 ขึ้นอยู่กับค่าของ id (o1) และ id (o2)
yacc143

-6

อินสแตนซ์ของคลาสเมื่อเปรียบเทียบกับ == มาไม่เท่ากัน วิธีที่ดีที่สุดคือการแบ่งฟังก์ชั่นcmpให้กับชั้นเรียนของคุณซึ่งจะทำสิ่งต่างๆ

หากคุณต้องการเปรียบเทียบเนื้อหาคุณสามารถใช้ cmp (obj1, obj2)

ในกรณีของคุณ cmp (doc1, doc2) มันจะกลับ -1 ถ้าเนื้อหาฉลาดพวกเขาเหมือนกัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.