I am really surprised at the resistance against defining `__eq__` on the
target objects. Every time this problem has cropped up in code I was
working on (including code part of very large corporate code bases) the
obvious solution was to define `__eq__`. The only reason I can think of why
you are so resistant to this would be due to poor development practices,
e.g. adding tests long after the "main" code has already been deployed, or
having a separate team write tests.

Regarding `__hash__`, it is a very bad idea to call `super().__hash__()`!
Unless your `__eq__` also just calls `super().__eq__(other)` (and what
would be the point of that?), defining `__hash__` that way will cause
irreproducible behavior where *sometimes* an object that is equal to a dict
key will not be found in the dict even though it is already present,
because the two objects have different hash values. Defining `__hash__` as
`id(self)` is no better. In fact, defining `__hash__` as returning the
constant `42` is better, because it is fine if two objects that *don't*
compare equal still have the same hash value (but not the other way around).

The right way to define `__hash__` is to construct a tuple of all the
attributes that are considered by `__eq__` and return the `hash()` of that
tuple. (In some cases you can make it faster by leaving some expensive
attribute out of the tuple -- again, that's fine, but don't consider
anything that's not used by `__eq__`.)

Finally, dataclasses get you all this for free, and they are the future.

On Sun, Jul 26, 2020 at 7:48 PM Henry Lin <hlin...@gmail.com> wrote:

> @Steven D'Aprano <st...@pearwood.info> All good ideas ☺ I'm in agreement
> that we should be building solutions which are generalizable.
>
> Are there more concerns people would like to bring up when considering the
> problem of object equality?
>
> On Sun, Jul 26, 2020 at 9:25 PM Steven D'Aprano <st...@pearwood.info>
> wrote:
>
>> On Sun, Jul 26, 2020 at 11:12:39PM +0200, Alex Hall wrote:
>>
>> > There's another reason people might find this useful - if the objects
>> have
>> > differing attributes, the assertion can show exactly which ones,
>> instead of
>> > just saying that the objects are not equal.
>>
>> That's a good point.
>>
>> I sat down to start an implementation, when a fundamental issue with
>> this came to mind. This proposed comparison is effectively something
>> close to:
>>
>>     vars(actual) == vars(expected)
>>
>> only recursively and with provision for objects with `__slots__` and/or
>> no `__dict__`. And that observation lead me to the insight that as tests
>> go, this is a risky, unreliable test.
>>
>> A built-in example:
>>
>>
>>     actual = lambda: 1  # simulate some complex object
>>     expected = lambda: 2  # another complex object
>>     vars(actual) == vars(expected)  # returns True
>>
>>
>> So this is a comparison that needs to be used with care. It is easy for
>> the test to pass while the objects are nevertheless not what you expect.
>>
>> Having said that, another perspective is that unittest already has a
>> smart test for comparing dicts, assertDictEqual, which is automatically
>> called by assertEqual.
>>
>>
>> https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertDictEqual
>>
>> So it may be sufficient to have a utility function that copies
>> an instance's slots and dict into a dict, and then compare dicts. Here's
>> a sketch:
>>
>>     d1 = vars(actual).copy()
>>     d1.update({key: value for key in actual.__slots__})
>>     # Likewise for d2 from expected
>>     self.assertEqual(d1, d2)
>>
>> Make that handle the corner cases where objects have no instance dict or
>> slots, and we're done.
>>
>> Thinking aloud here.... I see this as a kind of copy operation, and
>> think this would be useful outside of testing. I've written code to copy
>> attributes from instances on multiple occasions. So how about a new
>> function in the `copy` module to do so:
>>
>>     copy.getattrs(obj, deep=False)
>>
>> that returns a dict. Then the desired comparison could be a thin
>> wrapper:
>>
>>     def assertEqualAttrs(self, actual, expected, msg=None):
>>         self.assertEqual(getattrs(actual), getattrs(expected))
>>
>>
>> I'm not keen on a specialist test function, but I'm warming to the idea
>> of exposing this functionality in a more general, and hence more useful,
>> form.
>>
>>
>> --
>> Steven
>> _______________________________________________
>> Python-ideas mailing list -- python-ideas@python.org
>> To unsubscribe send an email to python-ideas-le...@python.org
>> https://mail.python.org/mailman3/lists/python-ideas.python.org/
>> Message archived at
>> https://mail.python.org/archives/list/python-ideas@python.org/message/MLRFS6RO7WF2UAEOS4YMH2FXRQHJUGWU/
>> Code of Conduct: http://python.org/psf/codeofconduct/
>>
> _______________________________________________
> Python-ideas mailing list -- python-ideas@python.org
> To unsubscribe send an email to python-ideas-le...@python.org
> https://mail.python.org/mailman3/lists/python-ideas.python.org/
> Message archived at
> https://mail.python.org/archives/list/python-ideas@python.org/message/U6RQ6GA7RXYHTEORTD3VBZ4KMJMXMB5O/
> Code of Conduct: http://python.org/psf/codeofconduct/
>


-- 
--Guido van Rossum (python.org/~guido)
*Pronouns: he/him **(why is my pronoun here?)*
<http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/TGGKBEDJSFPUXMQKYK5AFJFM2ZMJHF67/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to