2

I have an object that has the following layout:

class Obj1(object):
    def __init__(self, user, password, items=None):
        self._user = user
        self._password = password
        self._items = items

    def add_items(self, item):
        self._items.append(item)

    def has_changed(self, obj2):
        return self != obj2

Now I do the following:

obj1 = Obj1('me', '1234')
obj1.add_item({'name':'george', 'progress':'70'})
#obj2 = obj1 #wont work since they would point to same object
obj2 = copy.copy(obj1)
obj1.add_item({'name':'monica', 'progress':'86'})
print obj2.has_changed(obj1)

Surprisingly this returns me false. Can someone point me out what I am missing here?

Nilesh
  • 20,521
  • 16
  • 92
  • 148
as3rdaccount
  • 3,711
  • 12
  • 42
  • 62

2 Answers2

6

You may just override __eq__ method of the object. When you are just comparing objects only identities are compared (they are not the same object, thus == will result in False):

User-defined classes have __eq__() and __hash__() methods by default; with them, all objects compare unequal (except with themselves) and x.__hash__() returns id(x)

Here's a small example:

>>> class A(object):
...     def __init__(self, i):
...         self.i = i
...
>>>
>>> a = A(1)
>>> b = A(1)
>>> c = A(2)
>>> a == b
False
>>> a == c
False

But if you override comparison you'll get what you need:

>>> class B(object):
...     def __init__(self,i):
...         self.i = i
...     def __eq__(self,o):
...         return self.i == o.i
...     def __ne__(self,o):
...         return self.i != o.i
...
>>> d = B(1)
>>> e = B(1)
>>> f = B(2)
>>> d == e
True
>>> d == f
False
>>>

Also comparing directories does "deep comparison" (so you can compare dictionaries directly):

>>> d1 = {1:2, 3:4}
>>> d2 = {}
>>> d2[1] = 2
>>> d2[3] = 4
>>> d3 = {5:6, 3:4}
>>> d1 == d2
True
>>> d1 == d3
False

Also note that there are some rules[1][2] that you should follow when implementing rich comparison methods, for example:

There are no implied relationships among the comparison operators. The truth of x==y does not imply that x!=y is false. Accordingly, when defining __eq__(), one should also define __ne__() so that the operators will behave as expected.

Arguments to rich comparison methods are never coerced.

A class that overrides __eq__() and does not define __hash__() will have its __hash__() implicitly set to None.


Requested update:

Arguments are never coerced (coercion in python glossary) means that checking input argument (o in my example) is your responsibility, try:

>>> d == 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __eq__
AttributeError: 'int' object has no attribute 'i'

And it's even possible to compare objects of different classes:

>>> d == a
True

And about __hash__ being set to None means, that hash(obj) fails:

>>> hash(d)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'B'

And every collection requiring hashing also fails:

>>> set((d,))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'B'

While it works with A:

>>> set((a,))
{<__main__.A object at 0x7f8a85fe4668>}
Community
  • 1
  • 1
Vyktor
  • 20,559
  • 6
  • 64
  • 96
  • OK just recapping some of the items in our answer now that I have some time... can you give an example may be what you mean by "arguments are never coerced" and what is the implication of __hash__ being set to None? Thanks a lot! – as3rdaccount Jul 08 '14 at 19:17
  • @ArunavDev I've added updated that should answer your question – Vyktor Jul 08 '14 at 21:10
0

Here is a working version of your code. I included the required import of the copy module and added colons to make it valid code.

import copy

class Obj1(object):
    def __init__(self, user, password, items=None):
        self._user = user
        self._password = password
        self._items = [] if items==None else items

    def add_item(self, item):
        self._items.append(item)

    def has_changed(self, obj2):
        return self != obj2

obj1 = Obj1('me', '1234')
obj1.add_item({'name':'george', 'progress':'70'})
#obj2 = obj1 #wont work since they would point to same object
obj2 = copy.copy(obj1)
obj1.add_item({'name':'monica', 'progress':'86'})
print(obj2.has_changed(obj1))
print(obj1.has_changed(obj1))

This appears to work but doesn't really (see below). Note that I added a further test to ensure that the comparison works when both True and False ... but the testing so far simply isn't good enough.

However, you should look at @Viktor's answer, since that explains that object equality checks (which you are inheriting in your class) do not check the equality of any attribute values but simply whether the two are the same object.

holdenweb
  • 33,305
  • 7
  • 57
  • 77
  • -1 this is not a working version. the function `has_changed` is not filling its purpose. – Korem Jul 03 '14 at 06:37
  • A few details of how it fails would be enlightening – holdenweb Jul 03 '14 at 06:38
  • The OP wants to know when the object has changed, that's why he uses `add_item` on obj1. He wants to know if the attributes of the object has changed - not if it's a different instance. In your code, even if you remove the 3rd line from the end - it still returns True on has changed between obj1 and obj2 – Korem Jul 03 '14 at 06:40
  • Thanks, I have updated my answer to explain why apparently working code is not in fact fulfilling its requirements, and pointed to (as well as upvoting) Vyktor's helpful answer. – holdenweb Jul 03 '14 at 06:44