I have multiple ranges and want to find out which range should the number fit between. The ranges are continous within a finited range. Is there any faster way to implement the below algorithm?
def range_checker(x: float) -> str:
for i in range(len(range_list) - 1):
if range_list[i] <= x < range_list[i + 1]:
return f"{x} in the range {range_list[i]}, {range_list[i + 1]}"
return "x is out of range"
if __name__ == "__main__":
range_list = [-0.2, -0.1, -0.5, 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.5]
print(range_checker(-0.23))
# -0.23 in the range -0.5, 0
3 Answers 3
Your code seems to work on the provided example which is great.
Also, it uses the __name__ == "__main__"
guard and type annotations which are a nice touch.
Let's see how things can be improved further.
Globals
Your code relies on the global range_list
to be set. The function would be easier to use, to test, to document, to maintain if this list was provided as a parameter.
Documentation
As briefly mentionned previously, the range_checker
function probably deserves a bit of documentation, for instance as a docstring.
Loops
In Python, there is often a better solution that using range
and len
to iterate over an array. For more explanations about this, I highly recommend Ned Batchelder's talk "Loop like a native".
In your case, it may be easier to first move the "pairing" logic in a function on its own:
def pairwise(a):
for i in range(len(a)-1):
yield a[i], a[i+1]
def range_checker(x: float, range_list) -> str:
for a, b in pairwise(range_list):
if a <= x < b:
return f"{x} in the range {a}, {b}"
return "x is out of range"
This is also a bit of an excuse to learn about the following concepts:
- tuple unpacking
- iterators
- yield
Then, you can replace the pairwise
implementation with any implementation you can find online.
-
\$\begingroup\$ Thank you for your answer, I have a little question about the efficiency of the code, seems to be using yield, and two for loops is O(n^2)? \$\endgroup\$tung– tung2022年11月03日 07:20:34 +00:00Commented Nov 3, 2022 at 7:20
-
\$\begingroup\$ Things are slighly more tricky than this because we do not have nested loops per se. Somehow, it is the same as having
for a, b in zip(range_list, range_list[1:])
which is a single loop (O(n)). The only difference is that the logic is moved elsewhere to be able to change the implementation easily if need be. \$\endgroup\$SylvainD– SylvainD2022年11月03日 08:22:05 +00:00Commented Nov 3, 2022 at 8:22 -
\$\begingroup\$ Thank you for your explanation, I have tested the speed of the code, it seems that the code I provided is faster than your answer in this case, is it the real case? If yes, do you think I should still write it in your way for the good practise of using yield? Also, may I ask where is the good place(book, online tutorial, google etc) I can study these knowledge, for example: how the yield works under the hood which perform (O(n)) etc. Thank you. \$\endgroup\$tung– tung2022年11月03日 18:03:46 +00:00Commented Nov 3, 2022 at 18:03
-
\$\begingroup\$ @tung It is quite literally just the same as your code but abstracted out for clarity at the cost of extra overhead for using
yield
andtuple
s. If you really want a performantpairwise
implementation forCollection
, then you should usedef paireise(collection): iterator = iter(collection); next(iterator, None); return zip(collection, iterator)
, which would 1) remove the overhead of usingyield
by lettingzip
run in C, 2) remove the overhead of indexing by relying entirely on iteration, and 3) remove the overhead of constructing a newtuple
every time due tozip
optimizations. \$\endgroup\$Simply Beautiful Art– Simply Beautiful Art2022年11月04日 05:23:11 +00:00Commented Nov 4, 2022 at 5:23 -
\$\begingroup\$ If you want to lookup how
yield
works, you can Google it alongside "generators", which is the special type of function which usesyield
. \$\endgroup\$Simply Beautiful Art– Simply Beautiful Art2022年11月04日 05:24:34 +00:00Commented Nov 4, 2022 at 5:24
To optimize searches of the form where we want to find the first index i
where x < items[i]
and items
is sorted, the bisect.bisect
function has us covered:
def grade_to_letter(grade):
if grade < 60:
return "F"
elif grade < 70:
return "D"
elif grade < 80:
return "C"
elif grade < 90:
return "B"
else:
return "A"
from bisect import bisect
GRADES = (60, 70, 80, 90)
LETTERS = ("F", "D", "C", "B", "A")
def grade_to_letter(grade):
return LETTERS[bisect(GRADES, grade)]
Note that the first and last cases (i == 0
or i == len(...)
) correspond to your out of range
case.
Instead of repeated index lookups, you could use zip
:
def range_checker(x: float) -> str:
# offset the end ranges by 1 using a slice
for a, b in zip(range_list, range_list[1:]):
if a <= x < b:
return f"{x} in the range {a}, {b}"
return "x is out of range"