Skip to content

Commit 58f503d

Browse files
Update docs for incomplete ordering + inconsistent hashing
1 parent 843a6c8 commit 58f503d

File tree

8 files changed

+49
-114
lines changed

8 files changed

+49
-114
lines changed

python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,40 @@
44
<qhelp>
55

66
<overview>
7-
<p>In order to conform to the object model, classes that define their own equality method should also
8-
define their own hash method, or be unhashable. If the hash method is not defined then the <code>hash</code> of the
9-
super class is used. This is unlikely to result in the expected behavior.</p>
7+
<p>A hashable class has an <code>__eq__</code> method, and a <code>__hash__</code> method that agrees with equality.
8+
When a hash method is defined, an equality method should also be defined; otherwise object identity is used for equality comparisons
9+
which may not be intended.
10+
</p>
1011

11-
<p>A class can be made unhashable by setting its <code>__hash__</code> attribute to <code>None</code>.</p>
12-
13-
<p>In Python 3, if you define a class-level equality method and omit a <code>__hash__</code> method
14-
then the class is automatically marked as unhashable.</p>
12+
<p>Note that defining an <code>__eq__</code> method without defining a <code>__hash__</code> method automatically makes the class unhashable in Python 3.
13+
(even if a superclass defines a hash method).</p>
1514

1615
</overview>
1716
<recommendation>
1817

19-
<p>When you define an <code>__eq__</code> method for a class, remember to implement a <code>__hash__</code> method or set
20-
<code>__hash__ = None</code>.</p>
18+
<p>
19+
If a <code>__hash__</code> method is defined, ensure a compatible <code>__eq__</code> method is also defined.
20+
</p>
21+
22+
<p>
23+
To explicitly declare a class as unhashable, set <code>__hash__ = None</code>, rather than defining a <code>__hash__</code> method that always
24+
raises an exception. Otherwise, the class would be incorrectly identified as hashable by an <code>isinstance(obj, collections.abc.Hashable)</code> call.
25+
</p>
2126

2227
</recommendation>
2328
<example>
24-
<p>In the following example the <code>Point</code> class defines an equality method but
25-
no hash method. If hash is called on this class then the hash method defined for <code>object</code>
26-
is used. This is unlikely to give the required behavior. The <code>PointUpdated</code> class
27-
is better as it defines both an equality and a hash method.
28-
If <code>Point</code> was not to be used in <code>dict</code>s or <code>set</code>s, then it could be defined as
29-
<code>UnhashablePoint</code> below.
29+
<p>In the following example, the <code>A</code> class defines an hash method but
30+
no equality method. Equality will be determined by object identity, which may not be the expected behaviour.
3031
</p>
31-
<p>
32-
To comply fully with the object model this class should also define an inequality method (identified
33-
by a separate rule).</p>
3432

35-
<sample src="EqualsOrHash.py" />
33+
<sample src="examples/EqualsOrHash.py" />
3634

3735
</example>
3836
<references>
3937

4038

4139
<li>Python Language Reference: <a href="http://docs.python.org/reference/datamodel.html#object.__hash__">object.__hash__</a>.</li>
42-
<li>Python Glossary: <a href="http://docs.python.org/2/glossary.html#term-hashable">hashable</a>.</li>
40+
<li>Python Glossary: <a href="http://docs.python.org/3/glossary.html#term-hashable">hashable</a>.</li>
4341

4442

4543
</references>

python/ql/src/Classes/Comparisons/EqualsOrHash.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @name Inconsistent equality and hashing
3-
* @description Defining equality for a class without also defining hashability (or vice-versa) violates the object model.
3+
* @description Defining a hash operation without defining equality may be a mistake.
44
* @kind problem
55
* @tags quality
66
* reliability

python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @name Inconsistent equality and inequality
3-
* @description Defining only an equality method or an inequality method for a class violates the object model.
3+
* @description Class definitions of equality and inequality operators may be inconsistent.
44
* @kind problem
55
* @tags quality
66
* reliability

python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,34 @@
33
"qhelp.dtd">
44
<qhelp>
55
<overview>
6-
<p> A class that implements an ordering operator
7-
(<code>__lt__</code>, <code>__gt__</code>, <code>__le__</code> or <code>__ge__</code>) should implement
8-
all four in order that ordering between two objects is consistent and obeys the usual mathematical rules.
9-
If the ordering is inconsistent with default equality, then <code>__eq__</code> and <code>__ne__</code>
10-
should also be implemented.
6+
<p> A class that implements the rich comparison operators
7+
(<code>__lt__</code>, <code>__gt__</code>, <code>__le__</code>, or <code>__ge__</code>) should ensure that all four
8+
comparison operations <code>&lt;</code>, <code>&lt;=</code>, <code>&gt;</code>, and <code>&gt;=</code> function as expected, consistent
9+
with expected mathematical rules.
10+
In Python 3, this is ensured by implementing one of <code>__lt__</code> or <code>__gt__</code>, and one of <code>__le__</code> or <code>__ge__</code>.
11+
If the ordering is not consistent with default equality, then <code>__eq__</code> should also be implemented.
1112
</p>
1213

1314
</overview>
1415
<recommendation>
15-
<p>Ensure that all four ordering comparisons are implemented as well as <code>__eq__</code> and <code>
16-
__ne__</code> if required.</p>
16+
<p>Ensure that at least one of <code>__lt__</code> or <code>__gt__</code> and at least one of <code>__le__</code> or <code>__ge__</code> is defined.
17+
</p>
1718

18-
<p>It is not necessary to manually implement all four comparisons,
19-
the <code>functools.total_ordering</code> class decorator can be used.</p>
19+
<p>
20+
The <code>functools.total_ordering</code> class decorator can be used to automatically implement all four comparison methods from a single one,
21+
which is typically the cleanest way to ensure all necessary comparison methods are implemented consistently.</p>
2022

2123
</recommendation>
2224
<example>
23-
<p>In this example only the <code>__lt__</code> operator has been implemented which could lead to
24-
inconsistent behavior. <code>__gt__</code>, <code>__le__</code>, <code>__ge__</code>, and in this case,
25-
<code>__eq__</code> and <code>__ne__</code> should be implemented.</p>
26-
<sample src="IncompleteOrdering.py" />
25+
<p>In the following example, only the <code>__lt__</code> operator has been implemented, which would lead to unexpected
26+
errors if the <code>&lt;=</code> or <code>&gt;=</code> operators are used on <code>A</code> instances.
27+
The <code>__le__</code> method should also be defined, as well as <code>__eq__</code> in this case.</p>
28+
<sample src="examples/IncompleteOrdering.py" />
2729

2830
</example>
2931
<references>
3032

31-
<li>Python Language Reference: <a href="http://docs.python.org/2/reference/datamodel.html#object.__lt__">Rich comparisons in Python</a>.</li>
33+
<li>Python Language Reference: <a href="http://docs.python.org/3/reference/datamodel.html#object.__lt__">Rich comparisons in Python</a>.</li>
3234

3335

3436
</references>

python/ql/src/Classes/Comparisons/IncompleteOrdering.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @name Incomplete ordering
3-
* @description Class defines one or more ordering method but does not define all 4 ordering comparison methods
3+
* @description Class defines ordering comparison methods, but does not define both strict and nonstrict ordering methods, to ensure all four comparison operators behave as expected.
44
* @kind problem
55
* @tags quality
66
* reliability
Lines changed: 8 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,8 @@
1-
# Incorrect: equality method defined but class contains no hash method
2-
class Point(object):
3-
4-
def __init__(self, x, y):
5-
self._x = x
6-
self._y = y
7-
8-
def __repr__(self):
9-
return 'Point(%r, %r)' % (self._x, self._y)
10-
11-
def __eq__(self, other):
12-
if not isinstance(other, Point):
13-
return False
14-
return self._x == other._x and self._y == other._y
15-
16-
17-
# Improved: equality and hash method defined (inequality method still missing)
18-
class PointUpdated(object):
19-
20-
def __init__(self, x, y):
21-
self._x = x
22-
self._y = y
23-
24-
def __repr__(self):
25-
return 'Point(%r, %r)' % (self._x, self._y)
26-
27-
def __eq__(self, other):
28-
if not isinstance(other, Point):
29-
return False
30-
return self._x == other._x and self._y == other._y
31-
32-
def __hash__(self):
33-
return hash(self._x) ^ hash(self._y)
34-
35-
# Improved: equality method defined and class instances made unhashable
36-
class UnhashablePoint(object):
37-
38-
def __init__(self, x, y):
39-
self._x = x
40-
self._y = y
41-
42-
def __repr__(self):
43-
return 'Point(%r, %r)' % (self._x, self._y)
44-
45-
def __eq__(self, other):
46-
if not isinstance(other, Point):
47-
return False
48-
return self._x == other._x and self._y == other._y
49-
50-
#Tell the interpreter that instances of this class cannot be hashed
51-
__hash__ = None
52-
1+
class A:
2+
def __init__(self, a, b):
3+
self.a = a
4+
self.b = b
5+
6+
# No equality method is defined
7+
def __hash__(self):
8+
return hash((self.a, self.b))

python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,3 @@ def __ne__(self, other): # Improved: equality and inequality method defined (ha
3131
return not self == other
3232

3333

34-
35-
class A:
36-
def __init__(self, a):
37-
self.a = a
38-
39-
def __eq__(self, other):
40-
print("A eq")
41-
return self.a == other.a
42-
43-
def __ne__(self, other):
44-
print("A ne")
45-
return self.a != other.a
46-
47-
class B(A):
48-
def __init__(self, a, b):
49-
self.a = a
50-
self.b = b
51-
52-
def __eq__(self, other):
53-
print("B eq")
54-
return self.a == other.a and self.b == other.b
55-
56-
print(B(1,2) != B(1,3))
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
class IncompleteOrdering(object):
1+
class A:
22
def __init__(self, i):
33
self.i = i
44

5+
# BAD: le is not defined, so `A(1) <= A(2) would result in an error.`
56
def __lt__(self, other):
6-
return self.i < other.i
7+
return self.i < other.i
8+

0 commit comments

Comments
 (0)