Como fixar um inteiro em algum intervalo?

92

Eu tenho o seguinte código:

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]

Basicamente, calculo um novo índice e o uso para encontrar algum elemento de uma lista. Para ter certeza de que o índice está dentro dos limites da lista, precisei escrever essas 2 ifinstruções espalhadas em 4 linhas. Isso é bastante prolixo, um pouco feio ... Atrevo-me a dizer, é bastante antipitônico .

Existe alguma outra solução mais simples e compacta? (e mais pítônico )

Sim, eu sei que posso usar if elseem uma linha, mas não é legível:

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

Eu também sei que posso acorrentar max()e ficar min()junto. É mais compacto, mas acho meio obscuro, mais difícil de achar bugs se eu digitar errado. Em outras palavras, não acho muito simples.

new_index = max(0, min(new_index, len(mylist)-1))
Denilson Sá Maia
fonte
2
Se parece "meio obscuro", fazer disso uma função?
Santa
1
Sim, posso escrever uma função, mas esse não é o ponto. A questão é como implementar isso (em linha ou em uma função).
Denilson Sá Maia
clamp = lambda value, minv, maxv: max(min(value, maxv), minv)Usando a API de arma.sourceforge.net/docs.html#clamp
Dima Tisnek

Respostas:

119

Isso é muito claro, na verdade. Muitas pessoas aprendem rapidamente. Você pode usar um comentário para ajudá-los.

new_index = max(0, min(new_index, len(mylist)-1))
S.Lott
fonte
12
Embora eu ache que não é tão pitônico quanto deveria ser, também acho que esta é a melhor solução que temos agora.
Denilson Sá Maia
49
def clamp(n, smallest, largest): return max(smallest, min(n, largest))
csl
3
@csl O pessoal sempre fornece essas pequenas funções auxiliares, mas nunca sei onde colocá-las. helperFunctions.py? Um módulo separado? E se isso ficar cheio de várias "funções auxiliares" para coisas completamente diferentes?
Mateen Ulhaq
1
Não sei, mas se você coletar muitos deles e categorizá-los em módulos lógicos, por que não colocar no GitHub e criar um pacote PyPi a partir dele? Provavelmente se tornaria popular.
csl
@MateenUlhaqutils.py
Wouterr
85
sorted((minval, value, maxval))[1]

por exemplo:

>>> 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
fonte
10
1 para uso criativo de sorted()embutido. Muito compacto, mas um pouco obscuro. Enfim, é sempre bom ver outras soluções criativas!
Denilson Sá Maia
10
Muito criativo e, na verdade, quase tão rápido quanto a min(max())construção. Muito ligeiramente mais rápido no caso em que o número está na faixa e nenhuma troca é necessária.
Kindall
40

muitas respostas interessantes aqui, quase iguais, exceto ... qual é mais rápida?

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 tem !, use o python puro e velho. A versão entorpecida é, talvez não surpreendentemente, a mais lenta de todas. Provavelmente porque está procurando por arrays, onde as outras versões apenas ordenam seus argumentos.

SingleNegationElimination
fonte
7
@LenarHoyt não é tão surpreendente, considerando que o desempenho do Numpy é projetado em torno de grandes matrizes, não números únicos. Além disso, ele precisa primeiro converter o número inteiro em um tipo de dados interno e, como aceita vários tipos diferentes de entradas, provavelmente leva um tempo considerável para descobrir de que tipo é a entrada e em que convertê-la. Você verá um desempenho muito melhor do Numpy se alimentá-lo com um array (de preferência não uma lista ou tupla, que ele precisa converter primeiro) de vários milhares de valores.
blubberdiblub
Python é três ordens de magnitude mais lento. 783 ns = 783.000 µs. Já cometi o mesmo erro no passado. A notação é sutil.
Dustin Andrews
5
@DustinAndrews, você entendeu ao contrário. 1 µs é 10 ^ -6 segundos, 1 ns é 10 ^ -9 segundos. o exemplo python completa 1 loop em 0,784 µs. Ou, pelo menos, funcionou na máquina em que testei. Este microbenchmark é tão útil quanto qualquer outro microbenchmark; pode afastá-lo de ideias realmente ruins, mas provavelmente não ajudará muito a encontrar a maneira mais rápida de escrever código útil .
SingleNegationElimination
Há uma pequena sobrecarga na chamada de funções. Eu não fiz os benchmarks, mas é bem possível que mm_clipe py_clipserá igualmente rápido se você usar compilador JIT, como PyPy. Exceto que o primeiro é mais legível e a legibilidade é mais importante na filosofia do Python do que um ligeiro ganho de desempenho na maioria das vezes.
Highstaker
@DustinAndrews Aconselho a excluir seu comentário factualmente incorreto porque você o obteve ao contrário.
Acumenus
38

Veja numpy.clip :

index = numpy.clip(index, 0, len(my_list) - 1)
Neil G
fonte
A documentação diz que o primeiro parâmetro de clipé a, uma “matriz contendo elementos para recortar”. Então você teria que escrever numpy.clip([index], …, não numpy.clip(index, ….
Rory O'Kane
13
@ RoryO'Kane: Você experimentou?
Neil G
1
O Pandas também permite isso em Series e DataFrames e Painéis.
Nour Wolf
17

Encadear max()e min()unir é o idioma normal que já vi. Se você achar difícil de ler, escreva uma função auxiliar para encapsular a operação:

def clamp(minimum, x, maximum):
    return max(minimum, min(x, maximum))
Laurence Gonsalves
fonte
14

O que aconteceu com minha amada linguagem Python legível? :-)

Sério, basta torná-lo uma função:

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

em seguida, basta chamá-lo com algo como:

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

Ou uma solução mais simples e flexível, onde você mesmo faz o cálculo:

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

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

Se você quiser, pode até mesmo fazer do min / max uma lista para que pareça mais "matematicamente puro":

x = restrict(val+7, [0, 42])
paxdiablo
fonte
6
Colocá-lo em uma função é bom (e aconselhável, se você estiver fazendo isso muito), mas eu acho mine maxsão muito mais claros do que um monte de condicionais. (Não sei para que addserve - basta dizer clamp(val + 7, 0, 42).)
Glenn Maynard
1
@GlennMaynard. Não tenho certeza se posso concordar que mínimo e máximo são mais limpos. O objetivo principal de usá-los é poder colocar mais em uma linha, tornando o código menos legível.
Mad Physicist
10

Este parece mais pitônico para mim:

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

Alguns testes:

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

Se o seu código parecer muito pesado, uma função pode ajudar:

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

new_index = clamp(0, new_index, len(mylist)-1)
Greg Hewgill
fonte
2

Evite escrever funções para tarefas tão pequenas, a menos que você as aplique com frequência, pois isso irá bagunçar seu código.

para valores individuais:

min(clamp_max, max(clamp_min, value))

para listas de valores:

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