From 36645681c8ad9a30d142f1dabb44d1d6c3ed5fab Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Sun, 23 Oct 2011 19:53:01 +0100 Subject: [PATCH] Issue #13201: equality for range objects is now based on equality of the underlying sequences. Thanks Sven Marnach for the patch. --- Doc/library/functions.rst | 12 ++++ Doc/whatsnew/3.3.rst | 6 ++ Lib/test/test_hash.py | 3 +- Lib/test/test_range.py | 52 +++++++++++++++ Misc/ACKS | 1 + Misc/NEWS | 4 ++ Objects/rangeobject.c | 135 +++++++++++++++++++++++++++++++++++++- 7 files changed, 209 insertions(+), 4 deletions(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 886cb8269af..6944e1dcd03 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1077,6 +1077,13 @@ are always available. They are listed here in alphabetical order. >>> r[-1] 18 + Testing range objects for equality with ``==`` and ``!=`` compares + them as sequences. That is, two range objects are considered equal if + they represent the same sequence of values. (Note that two range + objects that compare equal might have different :attr:`start`, + :attr:`stop` and :attr:`step` attributes, for example ``range(0) == + range(2, 1, 3)`` or ``range(0, 3, 2) == range(0, 4, 2)``.) + Ranges containing absolute values larger than :data:`sys.maxsize` are permitted but some features (such as :func:`len`) will raise :exc:`OverflowError`. @@ -1086,6 +1093,11 @@ are always available. They are listed here in alphabetical order. Test integers for membership in constant time instead of iterating through all items. + .. versionchanged:: 3.3 + Define '==' and '!=' to compare range objects based on the + sequence of values they define (instead of comparing based on + object identity). + .. function:: repr(object) diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst index d08ee6288c7..95a5583d34b 100644 --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -186,6 +186,12 @@ and :func:`unicodedata.lookup()` resolves named sequences too. (Contributed by Ezio Melotti in :issue:`12753`) +Equality comparisons on :func:`range` objects now return a result reflecting +the equality of the underlying sequences generated by those range objects. + +(:issue:`13021`) + + New, Improved, and Deprecated Modules ===================================== diff --git a/Lib/test/test_hash.py b/Lib/test/test_hash.py index fea1025e910..779e4850ec1 100644 --- a/Lib/test/test_hash.py +++ b/Lib/test/test_hash.py @@ -107,8 +107,7 @@ class DefaultIterSeq(object): return self.seq[index] class HashBuiltinsTestCase(unittest.TestCase): - hashes_to_check = [range(10), - enumerate(range(10)), + hashes_to_check = [enumerate(range(10)), iter(DefaultIterSeq()), iter(lambda: 0, 0), ] diff --git a/Lib/test/test_range.py b/Lib/test/test_range.py index ede07912b0c..6035e76481c 100644 --- a/Lib/test/test_range.py +++ b/Lib/test/test_range.py @@ -507,6 +507,58 @@ class RangeTest(unittest.TestCase): for k in values - {0}: r[i:j:k] + def test_comparison(self): + test_ranges = [range(0), range(0, -1), range(1, 1, 3), + range(1), range(5, 6), range(5, 6, 2), + range(5, 7, 2), range(2), range(0, 4, 2), + range(0, 5, 2), range(0, 6, 2)] + test_tuples = list(map(tuple, test_ranges)) + + # Check that equality of ranges matches equality of the corresponding + # tuples for each pair from the test lists above. + ranges_eq = [a == b for a in test_ranges for b in test_ranges] + tuples_eq = [a == b for a in test_tuples for b in test_tuples] + self.assertEqual(ranges_eq, tuples_eq) + + # Check that != correctly gives the logical negation of == + ranges_ne = [a != b for a in test_ranges for b in test_ranges] + self.assertEqual(ranges_ne, [not x for x in ranges_eq]) + + # Equal ranges should have equal hashes. + for a in test_ranges: + for b in test_ranges: + if a == b: + self.assertEqual(hash(a), hash(b)) + + # Ranges are unequal to other types (even sequence types) + self.assertIs(range(0) == (), False) + self.assertIs(() == range(0), False) + self.assertIs(range(2) == [0, 1], False) + + # Huge integers aren't a problem. + self.assertEqual(range(0, 2**100 - 1, 2), + range(0, 2**100, 2)) + self.assertEqual(hash(range(0, 2**100 - 1, 2)), + hash(range(0, 2**100, 2))) + self.assertNotEqual(range(0, 2**100, 2), + range(0, 2**100 + 1, 2)) + self.assertEqual(range(2**200, 2**201 - 2**99, 2**100), + range(2**200, 2**201, 2**100)) + self.assertEqual(hash(range(2**200, 2**201 - 2**99, 2**100)), + hash(range(2**200, 2**201, 2**100))) + self.assertNotEqual(range(2**200, 2**201, 2**100), + range(2**200, 2**201 + 1, 2**100)) + + # Order comparisons are not implemented for ranges. + with self.assertRaises(TypeError): + range(0) < range(0) + with self.assertRaises(TypeError): + range(0) > range(0) + with self.assertRaises(TypeError): + range(0) <= range(0) + with self.assertRaises(TypeError): + range(0) >= range(0) + def test_main(): test.support.run_unittest(RangeTest) diff --git a/Misc/ACKS b/Misc/ACKS index e3e788e7c0b..2a62d968645 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -613,6 +613,7 @@ Ken Manheimer Vladimir Marangozov David Marek Doug Marien +Sven Marnach Alex Martelli Anthony Martin Owen Martin diff --git a/Misc/NEWS b/Misc/NEWS index 8e6eff79034..e6ee12006da 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,10 @@ What's New in Python 3.3 Alpha 1? Core and Builtins ----------------- +- Issue #13201: Define '==' and '!=' to compare range objects based on + the sequence of values they define (instead of comparing based on + object identity). + - Issue #1294232: In a few cases involving metaclass inheritance, the interpreter would sometimes invoke the wrong metaclass when building a new class object. These cases now behave correctly. Patch by Daniel Urban. diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 58d373c0b91..c1e9e543030 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -609,6 +609,137 @@ range_contains(rangeobject *r, PyObject *ob) PY_ITERSEARCH_CONTAINS); } +/* Compare two range objects. Return 1 for equal, 0 for not equal + and -1 on error. The algorithm is roughly the C equivalent of + + if r0 is r1: + return True + if len(r0) != len(r1): + return False + if not len(r0): + return True + if r0.start != r1.start: + return False + if len(r0) == 1: + return True + return r0.step == r1.step +*/ +static int +range_equals(rangeobject *r0, rangeobject *r1) +{ + int cmp_result; + PyObject *one; + + if (r0 == r1) + return 1; + cmp_result = PyObject_RichCompareBool(r0->length, r1->length, Py_EQ); + /* Return False or error to the caller. */ + if (cmp_result != 1) + return cmp_result; + cmp_result = PyObject_Not(r0->length); + /* Return True or error to the caller. */ + if (cmp_result != 0) + return cmp_result; + cmp_result = PyObject_RichCompareBool(r0->start, r1->start, Py_EQ); + /* Return False or error to the caller. */ + if (cmp_result != 1) + return cmp_result; + 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; + return PyObject_RichCompareBool(r0->step, r1->step, Py_EQ); +} + +static PyObject * +range_richcompare(PyObject *self, PyObject *other, int op) +{ + int result; + + if (!PyRange_Check(other)) + Py_RETURN_NOTIMPLEMENTED; + switch (op) { + case Py_NE: + case Py_EQ: + result = range_equals((rangeobject*)self, (rangeobject*)other); + if (result == -1) + return NULL; + if (op == Py_NE) + result = !result; + if (result) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; + case Py_LE: + case Py_GE: + case Py_LT: + case Py_GT: + Py_RETURN_NOTIMPLEMENTED; + default: + PyErr_BadArgument(); + return NULL; + } +} + +/* Hash function for range objects. Rough C equivalent of + + if not len(r): + return hash((len(r), None, None)) + if len(r) == 1: + return hash((len(r), r.start, None)) + return hash((len(r), r.start, r.step)) +*/ +static Py_hash_t +range_hash(rangeobject *r) +{ + PyObject *t; + Py_hash_t result = -1; + int cmp_result; + + t = PyTuple_New(3); + if (!t) + return -1; + Py_INCREF(r->length); + PyTuple_SET_ITEM(t, 0, r->length); + cmp_result = PyObject_Not(r->length); + if (cmp_result == -1) + goto end; + if (cmp_result == 1) { + Py_INCREF(Py_None); + Py_INCREF(Py_None); + PyTuple_SET_ITEM(t, 1, Py_None); + PyTuple_SET_ITEM(t, 2, Py_None); + } + else { + PyObject *one; + Py_INCREF(r->start); + PyTuple_SET_ITEM(t, 1, r->start); + one = PyLong_FromLong(1); + if (!one) + goto end; + cmp_result = PyObject_RichCompareBool(r->length, one, Py_EQ); + Py_DECREF(one); + if (cmp_result == -1) + goto end; + if (cmp_result == 1) { + Py_INCREF(Py_None); + PyTuple_SET_ITEM(t, 2, Py_None); + } + else { + Py_INCREF(r->step); + PyTuple_SET_ITEM(t, 2, r->step); + } + } + result = PyObject_Hash(t); + end: + Py_DECREF(t); + return result; +} + static PyObject * range_count(rangeobject *r, PyObject *ob) { @@ -763,7 +894,7 @@ PyTypeObject PyRange_Type = { 0, /* tp_as_number */ &range_as_sequence, /* tp_as_sequence */ &range_as_mapping, /* tp_as_mapping */ - 0, /* tp_hash */ + (hashfunc)range_hash, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ PyObject_GenericGetAttr, /* tp_getattro */ @@ -773,7 +904,7 @@ PyTypeObject PyRange_Type = { range_doc, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ - 0, /* tp_richcompare */ + range_richcompare, /* tp_richcompare */ 0, /* tp_weaklistoffset */ range_iter, /* tp_iter */ 0, /* tp_iternext */