Skip to content

Commit cd2fb59

Browse files
committed
Materials for Range tutorial
1 parent 4cf4dc6 commit cd2fb59

3 files changed

Lines changed: 154 additions & 0 deletions

File tree

python-range/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Python `range()`: Represent Numerical Ranges
2+
3+
This repository holds the code for Real Python's [Python `range()`: Represent Numerical Ranges](https://realpython.com/python-range/) tutorial.
4+
5+
## PiDigits
6+
7+
The file [`pi_digits.py`](pi_digits.py) shows the implementation of `PiDigits` which is an integer-like type that can be used as arguments to `range()`:
8+
9+
```python
10+
>>> from pi_digits import PiDigits
11+
12+
>>> PiDigits(3)
13+
PiDigits(num_digits=3)
14+
15+
>>> int(PiDigits(3))
16+
314
17+
18+
>>> range(PiDigits(3))
19+
range(0, 314)
20+
```
21+
22+
See [the tutorial](https://realpython.com/python-range/#create-a-range-using-integer-like-parameters) for more details.
23+
24+
## FloatRange
25+
26+
In [`float_range.py`](float_range.py), you'll find an implementation of a custom `FloatRange` class that behaves similarly to the built-in `range()` except that its arguments can be floating point numbers:
27+
28+
```pycon
29+
>>> from float_range import FloatRange
30+
31+
>>> FloatRange(1, 10, 1.2)
32+
FloatRange(start=1, stop=10, step=1.2)
33+
34+
>>> list(FloatRange(1, 10, 1.2))
35+
[1.0, 2.2, 3.4, 4.6, 5.8, 7.0, 8.2, 9.4]
36+
```
37+
38+
The built-in `range()` is implemented in C. However, you can look at the source code of `FloatRange` to get an idea of how `range()` works under the hood.
39+
40+
If you need to work with floating-point ranges, you can use `FloatRange`. However, NumPy's [`arange()`](https://realpython.com/how-to-use-numpy-arange/) will give you better performance, and is probably a better option overall.
41+
42+
## Author
43+
44+
- **Geir Arne Hjelle**, E-mail: [geirarne@realpython.com](geirarne@realpython.com)
45+
46+
## License
47+
48+
Distributed under the MIT license. See [`LICENSE`](../LICENSE) for more information.

python-range/float_range.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import math
2+
from dataclasses import dataclass, field
3+
4+
5+
@dataclass
6+
class FloatRange:
7+
start: float | int
8+
stop: float | int = None
9+
step: float | int = 1.0
10+
11+
def __post_init__(self):
12+
"""Validate parameters."""
13+
# Only one argument is given
14+
if self.stop is None:
15+
self.stop = self.start
16+
self.start = 0
17+
18+
# Validate that all arguments are ints or floats
19+
if not isinstance(self.start, float | int):
20+
raise ValueError("'start' must be a floating point number")
21+
if not isinstance(self.stop, float | int):
22+
raise ValueError("'stop' must be a floating point number")
23+
if not isinstance(self.step, float | int) or self.step == 0:
24+
raise ValueError("'step' must be a non-zero floating point number")
25+
26+
def __iter__(self):
27+
"""Create an iterator based on the range."""
28+
return _FloatRangeIterator(self.start, self.stop, self.step)
29+
30+
def __contains__(self, element):
31+
"""Check if element is a member of the range.
32+
33+
Use isclose() to handle floats.
34+
"""
35+
offset = (element - self.start) % self.step
36+
if self.step > 0:
37+
return self.start <= element < self.stop and (
38+
math.isclose(offset, 0) or math.isclose(offset, self.step)
39+
)
40+
else:
41+
return self.stop < element <= self.start and (
42+
math.isclose(offset, 0) or math.isclose(offset, self.step)
43+
)
44+
45+
def __len__(self):
46+
"""Calculate the number of elements in the range."""
47+
if any(
48+
self.step > 0 and self.stop <= self.start,
49+
self.step < 0 and self.stop >= self.start,
50+
):
51+
return 0
52+
return math.ceil((self.stop - self.start) / self.step)
53+
54+
def __getitem__(self, index):
55+
"""Get an element in the range based on its index."""
56+
if index < 0 or index >= len(self):
57+
raise IndexError(f"range index out of range: {index}")
58+
return self.start + index * self.step
59+
60+
def __reversed__(self):
61+
"""Create a FloatRange with elements in the reverse order."""
62+
cls = type(self)
63+
offset = (1 if self.step > 0 else -1) * min(0.1, abs(self.step) / 2)
64+
return cls(
65+
(self.stop - self.step) + (self.start - self.stop) % self.step,
66+
self.start - offset,
67+
-self.step,
68+
)
69+
70+
def count(self, element):
71+
"""Count number of occurences of element in range."""
72+
return 1 if element in self else 0
73+
74+
def index(self, element):
75+
"""Calculate index of element in range."""
76+
if element not in self:
77+
raise ValueError(f"{element} is not in range")
78+
return round((element - self.start) / self.step)
79+
80+
81+
@dataclass
82+
class _FloatRangeIterator:
83+
start: int
84+
stop: int
85+
step: int
86+
_num_steps: int = field(default=0, init=False)
87+
88+
def __next__(self):
89+
"""Calculate the next element in the iteration."""
90+
element = self.start + self._num_steps * self.step
91+
if any(
92+
self.step > 0 and element >= self.stop,
93+
self.step < 0 and element <= self.stop,
94+
):
95+
raise StopIteration
96+
self._num_steps += 1
97+
return element

python-range/pi_digits.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class PiDigits:
6+
num_digits: int
7+
8+
def __index__(self):
9+
return int("3141592653589793238462643383279"[: self.num_digits])

0 commit comments

Comments
 (0)