Skip to content

Commit c8eb6df

Browse files
authored
Merge pull request realpython#483 from realpython/gahjelle-range-update
Materials for Range tutorial
2 parents 4cf4dc6 + 947fc84 commit c8eb6df

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed

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