Why do ranges which are initialized with different values compare equal to one another in Python 3?
When I execute the following commands in my interpreter:
>>> r1 = range(0)
>>> r2 = range(2, 2, 2)
>>> r1 == r2
True
The result is True
. Why is this so? Why are two different range
objects with different parameter values treated as equal?
range
objects are special:Python will compare range
objects as Sequences. What that essentially means is that the comparison doesn't evaluate how they represent a given sequence but rather what they represent.
The fact that the start
, stop
and step
parameters are completely different plays no difference here because they all represent an empty list when expanded:
For example, the first range
object:
list(range(0)) # []
and the second range
object:
list(range(2, 2, 2)) # []
Both represent an empty list and since two empty lists compare equal (True
) so will the range
objects that represent them.
As a result, you can have completely different looking range
objects; if they represent the same sequence they will compare equal:
range(1, 5, 100) == range(1, 30, 100)
Both represent a list with a single element [1]
so these two will also compare equal.
range
objects are really special:Do note, though, that even though the comparison doesn't evaluate how they represent a sequence the result of comparing can be achieved using solely the values of start
, step
along with the len
of the range
objects; this has very interesting implications with the speed of comparisons:
r0 = range(1, 1000000)
r1 = range(1, 1000000)
l0 = list(r0)
l1 = list(r1)
Ranges compares super fast:
%timeit r0 == r1
The slowest run took 28.82 times longer than the fastest. This could mean that an intermediate result is being cached
10000000 loops, best of 3: 160 ns per loop
on the other hand, the lists..
%timeit l0 == l1
10 loops, best of 3: 27.8 ms per loop
Yeah..
As @SuperBiasedMan noted, this only applies to the range objects in Python 3. Python 2 range()
is a plain ol' function that returns a list while the 2.x
xrange
object doesn't have the comparing capabilies (and not only these..) that range
objects have in Python 3.
Look at @ajcr's answer for quotes directly from the source code on Python 3 range
objects. It's documented in there what the comparison between two different ranges actually entails: Simple quick operations. The range_equals
function is utilized in the range_richcompare
function for EQ
and NE
cases and assigned to the tp_richcompare
slot for PyRange_Type
types.
I believe the implementation of range_equals
is pretty readable (because it is nice as simple) to add here:
/* r0 and r1 are pointers to rangeobjects */
/* Check if pointers point to same object, example:
>>> r1 = r2 = range(0, 10)
>>> r1 == r2
obviously returns True. */
if (r0 == r1)
return 1;
/* Compare the length of the ranges, if they are equal
the checks continue. If they are not, False is returned. */
cmp_result = PyObject_RichCompareBool(r0->length, r1->length, Py_EQ);
/* Return False or error to the caller
>>> range(0, 10) == range(0, 10, 2)
fails here */
if (cmp_result != 1)
return cmp_result;
/* See if the range has a lenght (non-empty). If the length is 0
then due to to previous check, the length of the other range is
equal to 0. They are equal. */
cmp_result = PyObject_Not(r0->length);
/* Return True or error to the caller.
>>> range(0) == range(2, 2, 2) # True
(True) gets caught here. Lengths are both zero. */
if (cmp_result != 0)
return cmp_result;
/* Compare the start values for the ranges, if they don't match
then we're not dealing with equal ranges. */
cmp_result = PyObject_RichCompareBool(r0->start, r1->start, Py_EQ);
/* Return False or error to the caller.
lens are equal, this checks their starting values
>>> range(0, 10) == range(10, 20) # False
Lengths are equal and non-zero, steps don't match.*/
if (cmp_result != 1)
return cmp_result;
/* Check if the length is equal to 1.
If start is the same and length is 1, they represent the same sequence:
>>> range(0, 10, 10) == range(0, 20, 20) # True */
one = PyLong_FromLong(1);
if (!one)
return -1;
cmp_result = PyObject_RichCompareBool(r0->length, one, Py_EQ);
Py_DECREF(one);
/* Return True or error to the caller. */
if (cmp_result != 0)
return cmp_result;
/* Finally, just compare their steps */
return PyObject_RichCompareBool(r0->step, r1->step, Py_EQ);
I've also scattered some of my own comments here; look at @ajcr's answer for the Python equivalent.