Skip to content

Commit beff3a7

Browse files
authored
Materials for range membership Q&A (#402)
1 parent 375f6a2 commit beff3a7

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Why Are Membership Tests So Fast for `range()` in Python?
2+
3+
This repository holds the code for Real Python's [Why Are Membership Tests So Fast for `range()` in Python?](https://realpython.com/python-range-membership-test/) tutorial.
4+
5+
In [`range_tools.py`](range_tools.py), you'll find an implementation of a custom `Range` class that behaves similarly to the built-in `range()`:
6+
7+
```pycon
8+
>>> from range_tools import Range
9+
10+
>>> Range(start=1, stop=10, step=2)
11+
Range(start=1, stop=10, step=2)
12+
13+
>>> list(Range(start=1, stop=10, step=2))
14+
[1, 3, 5, 7, 9]
15+
```
16+
17+
While `range()` is implemented in C, you can look at the source code of `Range` to get an idea of how `range()` works under the hood.
18+
19+
## Author
20+
21+
- **Geir Arne Hjelle**, E-mail: [[email protected]]([email protected])
22+
23+
## License
24+
25+
Distributed under the MIT license. See [`LICENSE`](../LICENSE) for more information.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import math
2+
from dataclasses import dataclass, field
3+
4+
5+
@dataclass
6+
class Range:
7+
start: int
8+
stop: int
9+
step: int = 1
10+
11+
def __post_init__(self):
12+
"""Validate parameters."""
13+
if not isinstance(self.start, int):
14+
raise ValueError("'start' must be an integer")
15+
if not isinstance(self.stop, int):
16+
raise ValueError("'stop' must be an integer")
17+
if not isinstance(self.step, int) or self.step <= 0:
18+
raise ValueError("'step' must be a positive integer")
19+
20+
def __iter__(self):
21+
"""Create an iterator based on the range."""
22+
return _RangeIterator(self.start, self.stop, self.step)
23+
24+
def __contains__(self, element):
25+
"""Check if element is a member of the range."""
26+
return (
27+
self.start <= element < self.stop
28+
and (element - self.start) % self.step == 0
29+
)
30+
31+
def __len__(self):
32+
"""Calculate the number of elements in the range."""
33+
if self.stop <= self.start:
34+
return 0
35+
return math.ceil((self.stop - self.start) / self.step)
36+
37+
def __getitem__(self, index):
38+
"""Get an element in the range based on its index."""
39+
if index < 0 or index >= len(self):
40+
raise IndexError(f"range index out of range: {index}")
41+
return self.start + index * self.step
42+
43+
def count(self, element):
44+
"""Count number of occurences of element in range."""
45+
return 1 if element in self else 0
46+
47+
def index(self, element):
48+
"""Calculate index of element in range."""
49+
if element not in self:
50+
raise ValueError(f"{element} not in range")
51+
return (element - self.start) // self.step
52+
53+
54+
@dataclass
55+
class _RangeIterator:
56+
start: int
57+
stop: int
58+
step: int
59+
_state: int | None = field(default=None, init=False)
60+
61+
def __next__(self):
62+
"""Calculate the next element in the iteration."""
63+
if self._state is None:
64+
self._state = self.start
65+
else:
66+
self._state += self.step
67+
if self._state >= self.stop:
68+
raise StopIteration
69+
return self._state

0 commit comments

Comments
 (0)