如何将数字限制在某个范围内?(即夹紧、裁剪或限制数字的取值范围)

155

我有以下代码:

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]

基本上,我计算一个新的索引并使用它来从列表中查找某个元素。为了确保索引在列表的范围内,我需要将这两个if语句写成四行代码。这显得有点啰嗦,有点丑陋... 我敢说,这相当不符合Python的编程风格。

是否有其他更简单、更紧凑的解决方案?(并且更符合Pythonic的编程风格)

是的,我知道可以在一行中使用if else,但那样不易读:

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

我也知道可以把max()min()链接在一起。这样更加紧凑,但是我觉得有点晦涩难懂,如果我输入错误,很难找到错误。换句话说,我觉得这种方法不够直接。

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

请参见Pythonic way to replace list values with upper and lower bound (clamping, clipping, thresholding)?以获取处理NumPy数组中的值的特定技巧。


2
如果感觉“有点模糊”,那就把它封装成一个函数? - Santa
1
是的,我可以写一个函数,但这不是重点。问题是如何实现它(无论是内联还是在函数中)。 - Denilson Sá Maia
clamp = lambda value, minv, maxv: max(min(value, maxv), minv)使用 http://arma.sourceforge.net/docs.html#clamp 的 API。 - Dima Tisnek
9个回答

186

实际上这很清晰明了,很多人可以很快学会。你可以使用注释来帮助他们。

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

15
虽然我感觉它不够符合Python的风格,但我也觉得这是目前我们最好的解决方案。 - Denilson Sá Maia
87
def clamp(n, smallest, largest): return max(smallest, min(n, largest))这段代码的作用是将参数 n 的值限制在范围 [smallest, largest] 之间,如果 n 的值小于 smallest,则取 smallest,如果 n 的值大于 largest,则取 largest,否则保持不变。 - csl
7
大家总是提供这些小的辅助函数,但我不知道应该把它们放在哪里。是 helperFunctions.py 文件中吗?还是单独建一个模块?如果不同的函数混杂在一起,怎么办? - Mateen Ulhaq
2
我不知道,但如果你收集了很多这样的代码,并将它们分类到合理的模块中,为什么不把它们放在GitHub上并创建一个PyPi包呢?这可能会变得很受欢迎。 - csl
4
好的,我会尽力进行翻译,以下是需要翻译的内容:@MateenUlhaq utils.py - Wouterr
2
我更喜欢重新排列它,使值按顺序排列:min(max(0, new_index), len(mylist)-1) - Karl Knechtel

129
sorted((minval, value, maxval))[1]
例如:
>>> 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

22
对于 sorted() 内置函数的创造性使用,点个赞。非常简洁,但有点晦涩。不管怎样,看到其他有创意的解决方案总是很好的! - Denilson Sá Maia
14
非常有创意,实际上与“min(max())”结构的速度几乎相同。在数字在范围内且不需要交换时,速度略微更快。 - kindall
并不真的更快,因为还需要为3元组对象进行额外的堆分配。 - undefined

67

这里有许多有趣的答案,都是关于相同的问题,唯独...哪个更快?

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已经解决了这个问题,使用纯Python即可。NumPy版本可能是最慢的,这也许并不令人惊讶,因为它正在寻找数组,而其他版本只是对其参数进行排序。


8
考虑到Numpy的性能是围绕大型数组而设计的,而不是单个数字,所以这并不令人惊讶。此外,它必须先将整数转换为内部数据类型,由于它接受多种不同类型的输入,因此可能需要相当长的时间来确定输入的类型和要将其转换为何种类型。如果您提供一个包含数千个值的数组(最好不要是列表或元组,因为它们需要首先进行转换),则会看到更好的Numpy性能。 - blubberdiblub
9
@DustinAndrews 你理解错了。1微秒是10^-6秒,1纳秒是10^-9秒。在我测试的机器上,Python示例完成1个循环需要0.784微秒。这个微基准测试跟其他微基准测试一样有用,可以让你避开糟糕的想法,但可能帮不上你太多,去找到写最快的有用代码的真正方法。 - SingleNegationElimination
1
函数调用会带来轻微的开销。虽然我没有进行基准测试,但如果您使用像PyPy这样的JIT编译器,mm_clippy_clip很可能一样快。除了前者更易于阅读,而在Python的哲学中,大多数情况下可读性比轻微的性能提升更重要。 - Highstaker

44

请参见numpy.clip:

index = numpy.clip(index, 0, len(my_list) - 1)

1
文档中提到clip函数的第一个参数是a,表示“包含需要剪裁元素的数组”。因此你需要写成numpy.clip([index], …而不是numpy.clip(index, … - Rory O'Kane
13
你尝试过吗? - Neil G
1
Pandas 还允许在 Series、DataFrames 和 Panels 上进行此操作。 - Nour Wolf

30

我心爱的易读的Python语言怎么了?:-)

说正经的,将它变成一个函数:

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

那么只需使用类似以下的方法调用:

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

或者更简单、更灵活的解决方案,您可以自行进行计算:

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

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

如果你想的话,你甚至可以将min/max作为一个列表,这样看起来更“数学纯粹”:

x = restrict(val+7, [0, 42])

7
将其放进一个函数中是可行的(如果您经常这样做,我建议这么做),但我认为使用“min”和“max”比一堆条件语句更清晰。我不知道“add”是用来干什么的——只需说“clamp(val + 7, 0, 42)”即可。 - Glenn Maynard
5
不确定我是否同意min和max更简洁的观点。使用它们的整个目的是为了能够将更多内容塞到一行中,从而使代码不那么易读。 - Mad Physicist

29

在我看来,将max()min()链接起来是正常的用法。如果你发现它难以阅读,请编写一个帮助函数来封装此操作:

def clamp(minimum, x, maximum):
    return max(minimum, min(x, maximum))

20

对我来说,这个更符合 Pythonic 风格:

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

几个测试:

>>> clip(5, 2, 7)
5
>>> clip(1, 2, 7)
2
>>> clip(8, 2, 7)
7

15

如果你的代码看起来太笨重,可以考虑使用函数:

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

new_index = clamp(0, new_index, len(mylist)-1)

1
避免为这样的小任务编写函数,除非您经常使用它们,否则会使您的代码混乱。
对于单个值:
min(clamp_max, max(clamp_min, value))

对于值列表:

map(lambda x: min(clamp_max, max(clamp_min, x)), values)

7
我不同意“避免为这样的小任务编写函数”的建议,这种建议会导致函数变得非常冗长(1k行)。给概念命名并封装它有其好处。 - alwaysmpe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接