6

我追求一种字符串格式来有效地表示一组索引。例如 "1-3,6,8-10,16" 将产生 [1,2,3,6,8,9,10,16]

理想情况下,我也可以表示无限序列。

是否有现有的标准方法来做到这一点?还是一个好的图书馆?或者你能提出你自己的格式吗?

谢谢!

编辑:哇!- 感谢所有深思熟虑的回应。我同意我应该使用 ':' 代替。关于无限列表的任何想法?我正在考虑使用“1..”来表示所有正数。

该用例用于购物车。对于某些产品,我需要将产品销售额限制为 X 的倍数,对于其他产品,我需要将其限制为任何正数。所以我在数据库中使用字符串格式来表示它。

4

5 回答 5

7

你不需要一个字符串,这很简单:

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])

输出:

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

我正在使用 Python 切片类型的能力来表达序列范围。我还使用生成器来提高内存效率。

请注意,我将 1 添加到切片停止,否则范围会有所不同,因为切片中的停止不包括在内。

它支持步骤:

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

和无限序列:

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

如果你必须给它一个字符串,那么你可以结合@ilya n。使用此解决方案的解析器。我会扩展@ilya n。解析器以支持索引和范围:

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]

现在你可以像这样使用它:

>>> print list(sequence()[parser('1-3,6,8-10,16')])
[1, 2, 3, 6, 8, 9, 10, 16]
于 2009-09-26T14:00:28.457 回答
3

如果您喜欢 Pythonic,我认为1:3,6,8:10,16这是一个更好的选择,因为x:y它是索引范围的标准表示法,并且语法允许您在对象上使用这种表示法。注意调用

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

被翻译成

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

即使这是一个TypeErrorifz是一个内置容器,您也可以自由地创建将返回一些合理的类,例如 NumPy 的数组。

您也可能会说,按照惯例5::5表示无限索引范围(这有点牵强,因为 Python 没有带有负索引或无限大正索引的内置类型)。

这是解析器(一个漂亮的单行代码,有slice(16, None, None)如下所述的故障):

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

然而,有一个陷阱:8:10根据定义,只包括索引 8 和 9——没有上限。如果这对您的目的来说是不可接受的,那么您当然需要一种不同的格式并且1-3,6,8-10,16对我来说看起来不错。然后解析器将是

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(',')]

更新:这是组合格式的完整解析器:

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

这也处理负数,所以

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

印刷

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

另一种选择是使用...(Python 将其识别为内置常量 Ellipsis,因此您可以z[...]根据需要调用),但我认为1,...,3,6, 8,...,10,16可读性较差。

于 2009-09-26T13:48:36.193 回答
2

这可能是可以做到的最懒惰的,这意味着即使是非常大的列表也可以:

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))

输出:

[1, 2, 3, 6, 8, 9, 10, 16]
于 2009-09-26T13:48:38.503 回答
1
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]

请注意,我正在使用xrange内置函数来生成序列。起初这似乎很尴尬,因为默认情况下它不包括最大数量的序列,但事实证明它非常方便。您可以执行以下操作:

>>> 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]

这意味着您可以使用step.xrange

于 2009-09-26T19:28:50.470 回答
1

今天早上我的咖啡看起来像是一个有趣的谜题。如果您确定了给定的语法(对我来说看起来不错,最后有一些注释),这里有一个 pyparsing 转换器,它将获取您的输入字符串并返回一个整数列表:

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)

这还会处理任何重叠或重复的条目,例如“3-8,4,6,3,4”,并返回仅包含唯一整数的列表。

解析器负责验证不允许“10-3”之类的范围。如果您真的想允许这样做,并且像“1,5-3,7”这样返回 1,5,4,3,7,那么您可以调整 intrange 和 mergeRanges 解析操作以获得这个更简单的结果(并丢弃validateRange 完全解析操作)。

你很可能在你的表达中得到空格,我认为这并不重要。"1, 2, 3-6" 的处理方式与 "1,2,3-6" 相同。Pyparsing 默认执行此操作,因此您在上面的代码中看不到任何特殊的空格处理(但它就在那里......)

此解析器不处理负索引,但如果也需要,只需将整数的定义更改为:

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

您的示例没有列出任何负面因素,因此我暂时将其省略。

Python 使用 ':' 作为范围分隔符,因此您的原始字符串可能看起来像 "1:3,6,8:10,16",而 Pascal 使用 '..' 作为数组范围,给出 "1..3, 6,8..10,16" - 嗯,就我而言,破折号一样好。

于 2009-09-26T13:50:38.513 回答