152

I have the following code:

new_index = index + offset
if new_index < 0:
    new_index = 0
if new_index >= len(mylist):
    new_index = len(mylist) - 1
return mylist[new_index]

Basically, I calculate a new index and use that to find some element from a list. In order to make sure the index is inside the bounds of the list, I needed to write those 2 if statements spread into 4 lines. That's quite verbose, a bit ugly... Dare I say, it's quite un-pythonic.

Is there any other simpler and more compact solution? (and more pythonic)

Yes, i know I can use if else in one line, but it is not readable:

new_index = 0 if new_index < 0 else len(mylist) - 1 if new_index >= len(mylist) else new_index

I also know I can chain max() and min() together. It's more compact, but I feel it's kinda obscure, more difficult to find bugs if I type it wrong. In other words, I don't find it very straightforward.

new_index = max(0, min(new_index, len(mylist)-1))

See Pythonic way to replace list values with upper and lower bound (clamping, clipping, thresholding)? for specific technique to process values in a Numpy array.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
Denilson Sá Maia
  • 47,466
  • 33
  • 109
  • 111

9 Answers9

182

This is pretty clear, actually. Many folks learn it quickly. You can use a comment to help them.

new_index = max(0, min(new_index, len(mylist)-1))
S.Lott
  • 384,516
  • 81
  • 508
  • 779
  • 15
    Although I feel it isn't as pythonic as it should be, I also feel this is the best solution we have now. – Denilson Sá Maia Nov 13 '10 at 01:14
  • 83
    `def clamp(n, smallest, largest): return max(smallest, min(n, largest))` – csl Sep 21 '15 at 11:05
  • 7
    @csl Folks always provide these small helper functions, but I never know where to put them. `helperFunctions.py`? A separate module? What if this gets littered with various "helper functions" for completely different things? – Mateen Ulhaq May 25 '17 at 02:01
  • 2
    I dunno, but if you collect a lot of those and categorize them into sensible modules, why not put on GitHub and create a PyPi package out of it? Would probably become popular. – csl May 25 '17 at 11:25
  • 4
    @MateenUlhaq `utils.py` – Wouterr Aug 06 '20 at 15:09
  • 2
    I prefer to rearrange it so that the values are in order: `min(max(0, new_index), len(mylist)-1)`. – Karl Knechtel Jan 29 '23 at 11:45
124
sorted((minval, value, maxval))[1]

for example:

>>> minval=3
>>> maxval=7
>>> for value in range(10):
...   print sorted((minval, value, maxval))[1]
... 
3
3
3
3
4
5
6
7
7
7
John La Rooy
  • 295,403
  • 53
  • 369
  • 502
  • 19
    +1 for creative usage of `sorted()` built-in. Very compact, but it is just a little bit obscure. Anyway, it's always nice to see other creative solutions! – Denilson Sá Maia Nov 04 '10 at 00:06
  • 14
    Very creative, and actually about as fast as the `min(max())` construction. Very slightly faster in the case that the number is in the range and no swaps are needed. – kindall Nov 04 '10 at 00:35
66

many interesting answers here, all about the same, except... which one's faster?

import numpy
np_clip = numpy.clip
mm_clip = lambda x, l, u: max(l, min(u, x))
s_clip = lambda x, l, u: sorted((x, l, u))[1]
py_clip = lambda x, l, u: l if x < l else u if x > u else x
>>> import random
>>> rrange = random.randrange
>>> %timeit mm_clip(rrange(100), 10, 90)
1000000 loops, best of 3: 1.02 µs per loop

>>> %timeit s_clip(rrange(100), 10, 90)
1000000 loops, best of 3: 1.21 µs per loop

>>> %timeit np_clip(rrange(100), 10, 90)
100000 loops, best of 3: 6.12 µs per loop

>>> %timeit py_clip(rrange(100), 10, 90)
1000000 loops, best of 3: 783 ns per loop

paxdiablo has it!, use plain ol' python. The numpy version is, perhaps not surprisingly, the slowest of the lot. Probably because it's looking for arrays, where the other versions just order their arguments.

Gringo Suave
  • 29,931
  • 6
  • 88
  • 75
SingleNegationElimination
  • 151,563
  • 33
  • 264
  • 304
  • 8
    @LenarHoyt it's not that surprising, considering that Numpy's performance is designed around large arrays, not single numbers. Also, it has to convert the integer to an internal datatype first and as it accepts several different kinds of inputs, it probably takes considerable time to figure out what type the input is and what to convert it into. You will see much better Numpy performance if you feed it an array (preferably not a list or tuple, which it has to convert first) of several thousands of values. – blubberdiblub Jan 26 '17 at 21:34
  • 9
    @DustinAndrews you've got that backwards. 1 µs is 10^-6 seconds, 1 ns is 10^-9 seconds. the python example completes 1 loop in 0.784 µs. Or at least, it did on the machine I tested it on. This microbenchmark is about as useful as any other microbenchmark; it can point you away from really bad ideas but probably won't help you much find the actually fastest way to write *useful* code. – SingleNegationElimination Jan 03 '18 at 22:03
  • 1
    There is a slight overhead on functions' call. I haven't done the benchmarks, but it's quite possible that `mm_clip` and `py_clip` will be equally fast if you use JIT compiler, like PyPy. Except the former is more readable, and readability is more important in Python's philosophy than a slight performance gain most of the time. – Highstaker Sep 19 '18 at 08:36
43

See numpy.clip:

index = numpy.clip(index, 0, len(my_list) - 1)
Neil G
  • 32,138
  • 39
  • 156
  • 257
  • 1
    [The docs](http://docs.scipy.org/doc/numpy/reference/generated/numpy.clip.html) say the first parameter of `clip` is `a`, an “array containing elements to clip”. So you would have to write `numpy.clip([index], …`, not `numpy.clip(index, …`. – Rory O'Kane Aug 27 '13 at 21:20
  • 13
    @RoryO'Kane: Did you try it? – Neil G Aug 28 '13 at 00:33
  • 1
    Pandas also allows this on Series and DataFrames, and Panels. – Nour Wolf Jul 06 '17 at 09:58
30

Whatever happened to my beloved readable Python language? :-)

Seriously, just make it a function:

def addInRange(val, add, minval, maxval):
    newval = val + add
    if newval < minval: return minval
    if newval > maxval: return maxval
    return newval

then just call it with something like:

val = addInRange(val, 7, 0, 42)

Or a simpler, more flexible, solution where you do the calculation yourself:

def restrict(val, minval, maxval):
    if val < minval: return minval
    if val > maxval: return maxval
    return val

x = restrict(x+10, 0, 42)

If you wanted to, you could even make the min/max a list so it looks more "mathematically pure":

x = restrict(val+7, [0, 42])
Gringo Suave
  • 29,931
  • 6
  • 88
  • 75
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • 7
    Putting it in a function is fine (and advised, if you're doing it a lot), but I think `min` and `max` are much clearer than a bunch of conditionals. (I don't know what `add` is for--just say `clamp(val + 7, 0, 42)`.) – Glenn Maynard Nov 03 '10 at 23:40
  • 5
    @GlennMaynard. Not sure that I can agree that min and max are cleaner. The whole point of using them is to be able to stuff more onto one line, effectively making the code less legible. – Mad Physicist Aug 01 '18 at 14:25
28

Chaining max() and min() together is the normal idiom I've seen. If you find it hard to read, write a helper function to encapsulate the operation:

def clamp(minimum, x, maximum):
    return max(minimum, min(x, maximum))
Laurence Gonsalves
  • 137,896
  • 35
  • 246
  • 299
20

This one seems more pythonic to me:

>>> def clip(val, min_, max_):
...     return min_ if val < min_ else max_ if val > max_ else val

A few tests:

>>> clip(5, 2, 7)
5
>>> clip(1, 2, 7)
2
>>> clip(8, 2, 7)
7
Jens
  • 8,423
  • 9
  • 58
  • 78
15

If your code seems too unwieldy, a function might help:

def clamp(minvalue, value, maxvalue):
    return max(minvalue, min(value, maxvalue))

new_index = clamp(0, new_index, len(mylist)-1)
Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
0

Avoid writing functions for such small tasks, unless you apply them often, as it will clutter up your code.

for individual values:

min(clamp_max, max(clamp_min, value))

for lists of values:

map(lambda x: min(clamp_max, max(clamp_min, x)), values)
  • 4
    I disagree with the "avoid writing functions for such small tasks", this kind of advice leads to functions that are 1k lines long. Naming a concept and encapsulating it has it's benefits. – alwaysmpe Jul 28 '21 at 12:44