Formato pythonico para índices.

Estoy detrás de un formato de cadena para representar eficientemente un conjunto de índices. Por ejemplo, “1-3,6,8-10,16” produciría [1,2,3,6,8,9,10,16]

Idealmente, también podría representar secuencias infinitas.

¿Hay una manera estándar existente de hacer esto? ¿O una buena biblioteca? ¿O puedes proponer tu propio formato?

¡Gracias!

Edición: ¡Guau! – Gracias por todas las respuestas bien consideradas. Estoy de acuerdo en que debería usar ‘:’ en su lugar. ¿Alguna idea sobre listas infinitas? Estaba pensando en usar “1 ..” para representar todos los números positivos.

El caso de uso es para un carrito de compras. Para algunos productos necesito restringir las ventas de productos a múltiplos de X, para otros cualquier número positivo. Así que estoy después de un formato de cadena para representar esto en la base de datos.

Si estás en algo Pythonic, creo que 1:3,6,8:10,16 sería una mejor opción, ya que x:y es una notación estándar para el rango de índice y la syntax te permite usar esta notación en objetos. Tenga en cuenta que la llamada

 z[1:3,6,8:10,16] 

se traduce en

 z.__getitem__((slice(1, 3, None), 6, slice(8, 10, None), 16)) 

Aunque este es un TypeError si z es un contenedor incorporado, puedes crear la clase que devolverá algo razonable, por ejemplo, como las matrices de NumPy.

También puede decir que por convención 5: y :5 representan intervalos de índice infinitos (esto es un poco extendido, ya que Python no tiene tipos incorporados con índices positivos negativos o infinitamente grandes).

Y aquí está el analizador (una hermosa frase de una línea que sufre el error de slice(16, None, None) describe a continuación):

 def parse(s): return [slice(*map(int, x.split(':'))) for x in s.split(',')] 

Sin embargo, hay un error: 8:10 por definición incluye solo los índices 8 y 9, sin límite superior. Si eso es inaceptable para sus propósitos, ciertamente necesita un formato diferente y 1-3,6,8-10,16 parece bien. El analizador sería entonces

 def myslice(start, stop=None, step=None): return slice(start, (stop if stop is not None else start) + 1, step) def parse(s): return [myslice(*map(int, x.split('-'))) for x in s.split(',')] 

Actualización: aquí está el analizador completo para un formato combinado:

 from sys import maxsize as INF def indices(s: 'string with indices list') -> 'indices generator': for x in s.split(','): splitter = ':' if (':' in x) or (x[0] == '-') else '-' ix = x.split(splitter) start = int(ix[0]) if ix[0] is not '' else -INF if len(ix) == 1: stop = start + 1 else: stop = int(ix[1]) if ix[1] is not '' else INF step = int(ix[2]) if len(ix) > 2 else 1 for y in range(start, stop + (splitter == '-'), step): yield y 

Esto maneja números negativos también, así que

  print(list(indices('-5, 1:3, 6, 8:15:2, 20-25, 18'))) 

huellas dactilares

 [-5, 1, 2, 6, 7, 8, 10, 12, 14, 20, 21, 22, 23, 24, 25, 18, 19] 

Otra alternativa es usar ... (que Python reconoce como la elipsis constante incorporada para que pueda llamar a z[...] lo desea) pero creo que 1,...,3,6, 8,...,10,16 es menos legible.

No necesitas una cadena para eso, esto es lo más simple que puedes obtener:

 from types import SliceType class sequence(object): def __getitem__(self, item): for a in item: if isinstance(a, SliceType): i = a.start step = a.step if a.step else 1 while True: if a.stop and i > a.stop: break yield i i += step else: yield a print list(sequence()[1:3,6,8:10,16]) 

Salida:

 [1, 2, 3, 6, 8, 9, 10, 16] 

Estoy usando la potencia de tipo de segmento de Python para express los rangos de secuencia. También estoy usando generadores para ser eficientes en la memoria.

Tenga en cuenta que estoy agregando 1 a la parada de división, de lo contrario, los rangos serán diferentes porque no se incluye la detención en porciones.

Es compatible con los pasos:

 >>> list(sequence()[1:3,6,8:20:2]) [1, 2, 3, 6, 8, 10, 12, 14, 16, 18, 20] 

Y secuencias infinitas:

 sequence()[1:3,6,8:] 1, 2, 3, 6, 8, 9, 10, ... 

Si tienes que darle una cadena, entonces puedes combinar @ilya n. Analizador con esta solución. Voy a extender @ilya n. analizador para soportar índices, así como rangos:

 def parser(input): ranges = [a.split('-') for a in input.split(',')] return [slice(*map(int, a)) if len(a) > 1 else int(a[0]) for a in ranges] 

Ahora puedes usarlo así:

 >>> print list(sequence()[parser('1-3,6,8-10,16')]) [1, 2, 3, 6, 8, 9, 10, 16] 

Probablemente esto sea lo más perezoso que se pueda hacer, lo que significa que estará bien incluso para listas muy grandes:

 def makerange(s): for nums in s.split(","): # whole list comma-delimited range_ = nums.split("-") # number might have a dash - if not, no big deal start = int(range_[0]) for i in xrange(start, start + 1 if len(range_) == 1 else int(range_[1]) + 1): yield i s = "1-3,6,8-10,16" print list(makerange(s)) 

salida:

 [1, 2, 3, 6, 8, 9, 10, 16] 
 import sys class Sequencer(object): def __getitem__(self, items): if not isinstance(items, (tuple, list)): items = [items] for item in items: if isinstance(item, slice): for i in xrange(*item.indices(sys.maxint)): yield i else: yield item >>> s = Sequencer() >>> print list(s[1:3,6,8:10,16]) [1, 2, 6, 8, 9, 16] 

Tenga en cuenta que estoy usando el xrange para generar la secuencia. Esto parece incómodo al principio porque no incluye el número superior de secuencias por defecto, sin embargo, resulta ser muy conveniente. Puedes hacer cosas como:

 >>> print list(s[1:10:3,5,5,16,13:5:-1]) [1, 4, 7, 5, 5, 16, 13, 12, 11, 10, 9, 8, 7, 6] 

Lo que significa que puedes usar la parte del step de xrange .

Parecía un divertido rompecabezas para acompañar mi café esta mañana. Si se conforma con su syntax dada (que me parece bien, con algunas notas al final), aquí hay un convertidor de parámetros que tomará su cadena de entrada y devolverá una lista de enteros:

 from pyparsing import * integer = Word(nums).setParseAction(lambda t : int(t[0])) intrange = integer("start") + '-' + integer("end") def validateRange(tokens): if tokens.from_ > tokens.to: raise Exception("invalid range, start must be <= end") intrange.setParseAction(validateRange) intrange.addParseAction(lambda t: list(range(t.start, t.end+1))) indices = delimitedList(intrange | integer) def mergeRanges(tokens): ret = set() for item in tokens: if isinstance(item,int): ret.add(item) else: ret += set(item) return sorted(ret) indices.setParseAction(mergeRanges) test = "1-3,6,8-10,16" print indices.parseString(test) 

Esto también se ocupa de cualquier duplicación o duplicación de entradas, como "3-8,4,6,3,4", y devuelve una lista de los únicos enteros únicos.

El analizador se encarga de validar que los rangos como "10-3" no están permitidos. Si realmente quería permitir esto, y tiene algo como "1,5-3,7", devuelva 1,5,4,3,7, entonces podría ajustar las acciones de análisis intrange y mergeRanges para obtener este resultado más simple (y descartarlo). la acción de análisis validateRange en total).

Es muy probable que tenga espacios en blanco en sus expresiones, asumo que esto no es significativo. "1, 2, 3-6" se manejaría igual que "1,2,3-6". Pyparsing hace esto de forma predeterminada, por lo que no ve ningún manejo especial de espacios en blanco en el código anterior (pero está ahí ...)

Este analizador no maneja índices negativos, pero si eso fuera necesario también, simplemente cambie la definición de entero a:

 integer = Combine(Optional('-') + Word(nums)).setParseAction(lambda t : int(t[0])) 

Tu ejemplo no incluía ningún aspecto negativo, por lo que lo dejé fuera por ahora.

Python usa ':' para un delimitador de rango, por lo que su cadena original podría haber parecido "1: 3,6,8: 10,16", y Pascal usó '..' para los rangos de matriz, dando "1..3, 6,8..10,16 "- meh, los guiones son tan buenos en lo que a mí respecta.