¿Cómo debo manejar los rangos inclusivos en Python?

Estoy trabajando en un dominio en el que los rangos se describen convencionalmente inclusive. Tengo descripciones legibles para los humanos, como from A to B , que representan rangos que incluyen ambos puntos finales, por ejemplo, from 2 to 4 significa 2, 3, 4 .

¿Cuál es la mejor manera de trabajar con estos rangos en el código Python? El siguiente código funciona para generar rangos inclusivos de enteros, pero también necesito realizar operaciones de división inclusiva:

 def inclusive_range(start, stop, step): return range(start, (stop + 1) if step >= 0 else (stop - 1), step) 

La única solución completa que veo es usar explícitamente + 1 (o - 1 ) cada vez que uso el range o la notación de división (por ejemplo, range(A, B + 1) , l[A:B+1] , range(B, A - 1, -1) ). ¿Es esta repetición realmente la mejor manera de trabajar con rangos inclusivos?

Edit: Gracias a L3viathan por responder. Escribir una función inclusive_slice para complementar inclusive_range es ciertamente una opción, aunque probablemente la escribiría de la siguiente manera:

 def inclusive_slice(start, stop, step): ... return slice(start, (stop + 1) if step >= 0 else (stop - 1), step) 

... aquí representa el código para manejar índices negativos, que no son sencillos cuando se usan con cortes. Tenga en cuenta, por ejemplo, que la función de L3viathan da resultados incorrectos si slice_to == -1 .

Sin embargo, parece que una función inclusive_slice sería difícil de usar: ¿es l[inclusive_slice(A, B)] realmente mejor que l[A:B+1] ?

¿Hay alguna mejor manera de manejar los rangos inclusivos?

Edit 2: Gracias por las nuevas respuestas. Estoy de acuerdo con Francis y Corley en que cambiar el significado de las operaciones de corte, ya sea globalmente o para ciertas clases, llevaría a una confusión significativa. Por lo tanto, ahora me inclino por escribir una función inclusive_slice .

Para responder a mi propia pregunta de la edición anterior, llegué a la conclusión de que usar esa función (por ejemplo, l[inclusive_slice(A, B)] ) sería mejor que sumr / restar manualmente 1 (por ejemplo, l[A:B+1] ), ya que permitiría que los casos de borde (como B == -1 y B == None ) se manejen en un solo lugar. ¿Podemos reducir la incomodidad en el uso de la función?

Edición 3: he estado pensando en cómo mejorar la syntax de uso, que actualmente parece l[inclusive_slice(1, 5, 2)] . En particular, sería bueno si la creación de un segmento inclusivo se asemejara a la syntax de sector estándar. Para permitir esto, en lugar de inclusive_slice(start, stop, step) , podría haber una función inclusive que toma una porción como parámetro. La syntax de uso ideal para inclusive sería la línea 1 :

 l[inclusive(1:5:2)] # 1 l[inclusive(slice(1, 5, 2))] # 2 l[inclusive(s_[1:5:2])] # 3 l[inclusive[1:5:2]] # 4 l[1:inclusive(5):2] # 5 

Lamentablemente, Python no permite esto, ya que solo permite el uso de : syntax dentro de [] . Por lo tanto, se debería llamar inclusive utilizando la syntax 2 o 3 (donde s_ actúa como la versión proporcionada por numpy ).

Otras posibilidades son hacer inclusive en un objeto con __getitem__ , permitiendo la syntax 4 , o aplicar inclusive solo al parámetro de stop del segmento, como en la syntax 5 . Desafortunadamente, no creo que se pueda hacer funcionar lo último, ya que lo inclusive requiere conocer el valor del step .

De las syntax viables (la original l[inclusive_slice(1, 5, 2)] , más 2 , 3 y 4 ), ¿cuál sería la mejor para usar? ¿O hay otra opción mejor?

Edición final: Gracias a todos por las respuestas y los comentarios, esto ha sido muy interesante. Siempre he sido un fanático de la filosofía de “una forma de hacerlo” de Python, pero este problema ha sido causado por un conflicto entre “one way” de Python y la “one way” proscrita por el dominio del problema. Definitivamente he ganado algo de aprecio por TIMTOWTDI en el diseño de idiomas.

Por dar la primera respuesta y la más votada, otorgo la recompensa a L3viathan.

Escriba una función adicional para un segmento inclusivo y úselo en lugar de cortar. Si bien sería posible, por ejemplo, crear una lista de subclases e implementar un __getitem__ reaccione a un objeto de división, no lo recomendaría, ya que su código se comportará de forma contraria a la expectativa de cualquier persona, excepto usted, y probablemente también en un año.

inclusive_slice podría verse así:

 def inclusive_slice(myList, slice_from=None, slice_to=None, step=1): if slice_to is not None: slice_to += 1 if step > 0 else -1 if slice_to == 0: slice_to = None return myList[slice_from:slice_to:step] 

Lo que yo haría personalmente, es usar la solución “completa” que mencionaste ( range(A, B + 1) , l[A:B+1] ) y comentar bien.

Como en Python, el índice final siempre es exclusivo, vale la pena considerar usar siempre los valores de “convención de Python” internamente. De esta manera, te evitarás mezclar los dos en tu código.

Solo trate con la “representación externa” a través de subrutinas de conversión dedicadas:

 def text2range(text): m = re.match(r"from (\d+) to (\d+)",text) start,end = int(m.groups(1)),int(m.groups(2))+1 def range2text(start,end): print "from %d to %d"%(start,end-1) 

Alternativamente, puede marcar las variables que contienen la representación “inusual” con la notación húngara verdadera .

Si no desea especificar el tamaño del paso sino el número de pasos, existe la opción de usar numpy.linspace que incluye el punto de inicio y final

 import numpy as np np.linspace(0,5,4) # array([ 0. , 1.66666667, 3.33333333, 5. ]) 

Creo que la respuesta estándar es usar solo +1 o -1 en todos los casos en que sea necesario.

No desea cambiar globalmente la forma en que se entienden los segmentos (que dividirá un montón de código), pero otra solución sería crear una jerarquía de clases para los objetos para los que desea que los segmentos sean inclusivos. Por ejemplo, para una list :

 class InclusiveList(list): def __getitem__(self, index): if isinstance(index, slice): start, stop, step = index.start, index.stop, index.step if index.stop is not None: if index.step is None: stop += 1 else: if index.step >= 0: stop += 1 else: if stop == 0: stop = None # going from [4:0:-1] to [4::-1] since [4:-1:-1] wouldn't work else: stop -= 1 return super().__getitem__(slice(start, stop, step)) else: return super().__getitem__(index) >>> a = InclusiveList([1, 2, 4, 8, 16, 32]) >>> a [1, 2, 4, 8, 16, 32] >>> a[4] 16 >>> a[2:4] [4, 8, 16] >>> a[3:0:-1] [8, 4, 2, 1] >>> a[3::-1] [8, 4, 2, 1] >>> a[5:1:-2] [32, 8, 2] 

Por supuesto, usted quiere hacer lo mismo con __setitem__ y __delitem__ .

(Utilicé una list pero eso funciona para cualquier Sequence o Sequence MutableSequence ).

Sin escribir tu propia clase, la función parece ser el camino a seguir. Lo que más se me ocurre es no almacenar listas reales, solo devolver generadores para el rango que te interesa. Ya que estamos hablando de la syntax de uso, aquí está lo que podría hacer.

 def closed_range(slices): slice_parts = slices.split(':') [start, stop, step] = map(int, slice_parts) num = start if start <= stop and step > 0: while num <= stop: yield num num += step # if negative step elif step < 0: while num >= stop: yield num num += step 

Y luego usar como

 list(closed_range('1:5:2')) [1,3,5] 

Por supuesto, también deberá verificar otras formas de entrada incorrecta si alguien más va a utilizar esta función.

Iba a comentar, pero es más fácil escribir código como respuesta, así que …

NO escribiría una clase que redefine la división, a menos que sea MUY clara. Tengo una clase que representa ints con rebanado de bits. En mis contextos, ‘4: 2’ es claramente inclusivo, y los ints no tienen ningún uso para cortar, por lo que es (apenas) aceptable (imho, y algunos no estarían de acuerdo).

Para listas, tienes el caso de que harás algo como

 list1 = [1,2,3,4,5] list2 = InclusiveList([1,2,3,4,5]) 

y luego en tu código

 if list1[4:2] == test_list or list2[4:2] == test_list: 

y ese es un error muy fácil de cometer, ya que la lista ya TIENE un uso bien definido … se ven idénticos, pero actúan de manera diferente, por lo que la depuración será muy confusa, especialmente si no se escribió.

Eso no significa que esté completamente perdido … cortar es conveniente, pero después de todo, es solo una función. Y puedes agregar esa función a algo como esto, por lo que esta podría ser una forma más fácil de acceder a ella:

 class inc_list(list): def islice(self, start, end=None, dir=None): return self.__getitem__(slice(start, end+1, dir)) l2 = inc_list([1,2,3,4,5]) l2[1:3] [0x3, 0x4] l2.islice(1,3) [0x3, 0x4, 0x5] 

Sin embargo, esta solución, como muchas otras, (además de ser incompleta … ya sé) tiene el talón de Aquiles en el sentido de que no es tan simple como la simple notación de corte … es un poco más simple que pasar la lista como un argumento, pero aún más difícil que solo [4: 2]. La única manera de hacer que eso suceda es pasar algo diferente a la porción, que podría interpelarse de manera diferente, para que el usuario sepa lo que hizo, y aún podría ser tan simple.

Una posibilidad … números de punto flotante. Son diferentes, por lo que puedes verlos, y no son mucho más difíciles que la syntax “simple”. No está incorporado, por lo que todavía hay algo de “magia” involucrada, pero en lo que respecta al azúcar sintáctica, no está mal …

 class inc_list(list): def __getitem__(self, x): if isinstance(x, slice): start, end, step = x.start, x.stop, x.step if step == None: step = 1 if isinstance(end, float): end = int(end) end = end + step x = slice(start, end, step) return list.__getitem__(self, x) l2 = inc_list([1,2,3,4,5]) l2[1:3] [0x2, 0x3] l2[1:3.0] [0x2, 0x3, 0x4] 

El 3.0 debería ser suficiente para decirle a cualquier progtwigdor de Python ‘hey, algo inusual está sucediendo allí’ … no necesariamente lo que está sucediendo, pero al menos no es de extrañar que actúe ‘raro’.

Tenga en cuenta que no hay nada único sobre eso en las listas … usted podría escribir fácilmente un decorador que podría hacer esto para cualquier clase:

 def inc_getitem(self, x): if isinstance(x, slice): start, end, step = x.start, x.stop, x.step if step == None: step = 1 if isinstance(end, float): end = int(end) end = end + step x = slice(start, end, step) return list.__getitem__(self, x) def inclusiveclass(inclass): class newclass(inclass): __getitem__ = inc_getitem return newclass ilist = inclusiveclass(list) 

o

 @inclusiveclass class inclusivelist(list): pass 

Sin embargo, la primera forma es probablemente más útil.

Centrándose en su solicitud de la mejor syntax, ¿qué pasa con la orientación:

 l[1:UpThrough(5):2] 

Puedes lograr esto usando el método __index__ :

 class UpThrough(object): def __init__(self, stop): self.stop = stop def __index__(self): return self.stop + 1 class DownThrough(object): def __init__(self, stop): self.stop = stop def __index__(self): return self.stop - 1 

Ahora ni siquiera necesita una lista especializada (y tampoco necesita modificar la definición global):

 >>> l = [1,2,3,4] >>> l[1:UpThrough(2)] [2,3] 

Si usa mucho, puede usar los nombres cortos upIncl , downIncl o incluso In e InRev .

También puede crear estas clases para que, además del uso en el segmento, actúen como el índice real:

 def __int__(self): return self.stop 

Es difícil y, probablemente, no es prudente sobrecargar tales conceptos básicos. con una nueva clase inclusivista, len (l [a: b]) en b-a + 1 que puede llevar a confusiones.
Para preservar el sentido natural de Python, al tiempo que proporciona legibilidad en un estilo BÁSICO, simplemente defina

 STEP=FROM=lambda x:x TO=lambda x:x+1 if x!=-1 else None DOWNTO=lambda x:x-1 if x!=0 else None 

entonces puedes administrar como quieras, manteniendo la lógica natural de python:

 >>>>l=list(range(FROM(0),TO(9))) >>>>l [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>>l[FROM(9):DOWNTO(3):STEP(-2)] == l[9:2:-2] True 

En lugar de crear una API que no sea convencional o ampliar los tipos de datos, como la lista, sería ideal crear una función de Slice un envoltorio sobre el sector integrado para que pueda pasarlo a cualquier lugar, lo que requiere un sector. Python tiene soporte para este enfoque para algunos casos excepcionales, y el caso que usted tiene puede justificar para ese caso de excepción. Como ejemplo, una porción inclusiva se vería como

 def islice(start, stop = None, step = None): if stop is not None: stop += 1 if stop == 0: stop = None return slice(start, stop, step) 

Y puedes usarlo para cualquier tipo de secuencia.

 >>> range(1,10)[islice(1,5)] [2, 3, 4, 5, 6] >>> "Hello World"[islice(0,5,2)] 'Hlo' >>> (3,1,4,1,5,9,2,6)[islice(1,-2)] (1, 4, 1, 5, 9, 2) 

Finalmente, también puede crear un rango inclusivo llamado irange para complementar la porción inclusiva (escrita en líneas de OPs).

 def irange(start, stop, step): return range(start, (stop + 1) if step >= 0 else (stop - 1), step)