Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3e124ae

Browse files
committed
Issue #1766304: Optimize membership testing for ranges: 'n in range(...)'
does an O(1) check, if n is an integer. Non-integers aren't affected. Thanks Robert Lehmann.
1 parent 2df8113 commit 3e124ae

4 files changed

Lines changed: 113 additions & 0 deletions

File tree

Doc/library/functions.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,10 @@ are always available. They are listed here in alphabetical order.
925925
>>> list(range(1, 0))
926926
[]
927927

928+
.. versionchanged:: 3.2
929+
Testing integers for membership takes constant time instead of
930+
iterating through all items.
931+
928932

929933
.. function:: repr(object)
930934

Lib/test/test_range.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,56 @@ def test_odd_bug(self):
7878
with self.assertRaises(TypeError):
7979
range([], 1, -1)
8080

81+
def test_types(self):
82+
# Non-integer objects *equal* to any of the range's items are supposed
83+
# to be contained in the range.
84+
self.assertTrue(1.0 in range(3))
85+
self.assertTrue(True in range(3))
86+
self.assertTrue(1+0j in range(3))
87+
88+
class C1:
89+
def __eq__(self, other): return True
90+
self.assertTrue(C1() in range(3))
91+
92+
# Objects are never coerced into other types for comparison.
93+
class C2:
94+
def __int__(self): return 1
95+
def __index__(self): return 1
96+
self.assertFalse(C2() in range(3))
97+
# ..except if explicitly told so.
98+
self.assertTrue(int(C2()) in range(3))
99+
100+
101+
def test_strided_limits(self):
102+
r = range(0, 101, 2)
103+
self.assertTrue(0 in r)
104+
self.assertFalse(1 in r)
105+
self.assertTrue(2 in r)
106+
self.assertFalse(99 in r)
107+
self.assertTrue(100 in r)
108+
self.assertFalse(101 in r)
109+
110+
r = range(0, -20, -1)
111+
self.assertTrue(0 in r)
112+
self.assertTrue(-1 in r)
113+
self.assertTrue(-19 in r)
114+
self.assertFalse(-20 in r)
115+
116+
r = range(0, -20, -2)
117+
self.assertTrue(-18 in r)
118+
self.assertFalse(-19 in r)
119+
self.assertFalse(-20 in r)
120+
121+
def test_empty(self):
122+
r = range(0)
123+
self.assertFalse(0 in r)
124+
self.assertFalse(1 in r)
125+
126+
r = range(0, -10)
127+
self.assertFalse(0 in r)
128+
self.assertFalse(-1 in r)
129+
self.assertFalse(1 in r)
130+
81131
def test_main():
82132
test.support.run_unittest(RangeTest)
83133

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ What's New in Python 3.2 Alpha 1?
1212
Core and Builtins
1313
-----------------
1414

15+
- Issue #1766304: Improve performance of membership tests on range objects.
16+
1517
- Issue #6713: Improve performance of integer -> string conversions.
1618

1719
- Issue #6846: Fix bug where bytearray.pop() returns negative integers.

Objects/rangeobject.c

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,69 @@ range_reduce(rangeobject *r, PyObject *args)
273273
r->start, r->stop, r->step);
274274
}
275275

276+
static int
277+
range_contains(rangeobject *r, PyObject *ob) {
278+
if (PyLong_Check(ob)) {
279+
int cmp1, cmp2, cmp3;
280+
PyObject *tmp1 = NULL;
281+
PyObject *tmp2 = NULL;
282+
PyObject *zero = NULL;
283+
int result = -1;
284+
285+
zero = PyLong_FromLong(0);
286+
if (zero == NULL) /* MemoryError in int(0) */
287+
goto end;
288+
289+
/* Check if the value can possibly be in the range. */
290+
291+
cmp1 = PyObject_RichCompareBool(r->step, zero, Py_GT);
292+
if (cmp1 == -1)
293+
goto end;
294+
if (cmp1 == 1) { /* positive steps: start <= ob < stop */
295+
cmp2 = PyObject_RichCompareBool(r->start, ob, Py_LE);
296+
cmp3 = PyObject_RichCompareBool(ob, r->stop, Py_LT);
297+
}
298+
else { /* negative steps: stop < ob <= start */
299+
cmp2 = PyObject_RichCompareBool(ob, r->start, Py_LE);
300+
cmp3 = PyObject_RichCompareBool(r->stop, ob, Py_LT);
301+
}
302+
303+
if (cmp2 == -1 || cmp3 == -1) /* TypeError */
304+
goto end;
305+
if (cmp2 == 0 || cmp3 == 0) { /* ob outside of range */
306+
result = 0;
307+
goto end;
308+
}
309+
310+
/* Check that the stride does not invalidate ob's membership. */
311+
tmp1 = PyNumber_Subtract(ob, r->start);
312+
if (tmp1 == NULL)
313+
goto end;
314+
tmp2 = PyNumber_Remainder(tmp1, r->step);
315+
if (tmp2 == NULL)
316+
goto end;
317+
/* result = (int(ob) - start % step) == 0 */
318+
result = PyObject_RichCompareBool(tmp2, zero, Py_EQ);
319+
end:
320+
Py_XDECREF(tmp1);
321+
Py_XDECREF(tmp2);
322+
Py_XDECREF(zero);
323+
return result;
324+
}
325+
/* Fall back to iterative search. */
326+
return (int)_PySequence_IterSearch((PyObject*)r, ob,
327+
PY_ITERSEARCH_CONTAINS);
328+
}
329+
276330
static PySequenceMethods range_as_sequence = {
277331
(lenfunc)range_length, /* sq_length */
278332
0, /* sq_concat */
279333
0, /* sq_repeat */
280334
(ssizeargfunc)range_item, /* sq_item */
281335
0, /* sq_slice */
336+
0, /* sq_ass_item */
337+
0, /* sq_ass_slice */
338+
(objobjproc)range_contains, /* sq_contains */
282339
};
283340

284341
static PyObject * range_iter(PyObject *seq);

0 commit comments

Comments
 (0)