【问题标题】:Pythonic way to convert a list of integers into a string of comma-separated ranges将整数列表转换为逗号分隔范围字符串的 Pythonic 方法
【发布时间】:2011-03-26 15:55:09
【问题描述】:

我有一个整数列表,需要将其解析为范围字符串。

例如:

 [0, 1, 2, 3] -> "0-3"
 [0, 1, 2, 4, 8] -> "0-2,4,8"

等等。

我还在学习更多处理列表的pythonic方法,这对我来说有点困难。我最近的想法是创建一个跟踪配对数字的列表:

[ [0, 3], [4, 4], [5, 9], [20, 20] ]

然后我可以遍历这个结构,将每个子列表打印为一个范围或单个值。

我不喜欢在两次迭代中执行此操作,但我似乎无法跟踪每次迭代中的每个数字。我的想法是做这样的事情:

这是我最近的尝试。它有效,但我并不完全满意;我一直在想,有一个更优雅的解决方案完全让我无法理解。字符串处理迭代不是最好的,我知道——对我来说现在还很早:)

def createRangeString(zones):
        rangeIdx = 0
        ranges   = [[zones[0], zones[0]]]
        for zone in list(zones):
            if ranges[rangeIdx][1] in (zone, zone-1):
                ranges[rangeIdx][1] = zone
            else:
                ranges.append([zone, zone])
                rangeIdx += 1

        rangeStr = ""
        for range in ranges:
            if range[0] != range[1]:
                rangeStr = "%s,%d-%d" % (rangeStr, range[0], range[1])
            else:
                rangeStr = "%s,%d" % (rangeStr, range[0])

        return rangeStr[1:]

有没有一种直接的方法可以将它合并到一个迭代中?我还能做些什么让它更 Pythonic?

【问题讨论】:

  • 查看more_itertools.consecutive_groups 工具here 的演示。

标签: list python


【解决方案1】:
>>> from itertools import count, groupby
>>> L=[1, 2, 3, 4, 6, 7, 8, 9, 12, 13, 19, 20, 22, 23, 40, 44]
>>> G=(list(x) for _,x in groupby(L, lambda x,c=count(): next(c)-x))
>>> print ",".join("-".join(map(str,(g[0],g[-1])[:len(g)])) for g in G)
1-4,6-9,12-13,19-20,22-23,40,44

这里的想法是将每个元素与 count() 配对。然后 value 和 count() 之间的差异对于连续值是恒定的。 groupby() 完成剩下的工作

正如 Jeff 建议的那样,count() 的替代方法是使用 enumerate()。这增加了一些需要在打印语句中去除的额外内容

G=(list(x) for _,x in groupby(enumerate(L), lambda (i,x):i-x))
print ",".join("-".join(map(str,(g[0][1],g[-1][1])[:len(g)])) for g in G)

更新:对于此处给出的示例列表,使用 enumerate 的版本比在我的计算机上使用 count() 的版本慢约 5%

【讨论】:

  • 尝试 L = [1,2,3,4,7,9,11,13,14,15]。如果我理解正确,它可能会中断。
  • 我可以收回它吗?绝妙的解决方案,我向你致敬。
  • @Muhammad,当然 :) 查看更新版本 - 它不再需要 izip
  • @gnibbler,这看起来很棒。我喜欢你在 print 语句中使用 map 。我不认为我遇到过 next() 方法。你可以将任何可迭代的项目传递给它吗?
  • @gnibbler:令人印象深刻。你可以在我把它挂在墙上之前使用G=(list(x) for _,x in .. 吗?这些天我正在研究 itertools 并寻找这样的宝石。
【解决方案2】:

这是否是 Pythonic 尚有待商榷。但它非常紧凑。真正的肉在Rangify() 函数中。如果您想要效率或 Python 主义,仍有改进的空间。

def CreateRangeString(zones):
    #assuming sorted and distinct
    deltas = [a-b for a, b in zip(zones[1:], zones[:-1])]
    deltas.append(-1)
    def Rangify((b, p), (z, d)):
        if p is not None:
            if d == 1: return (b, p)
            b.append('%d-%d'%(p,z))
            return (b, None)
        else:
            if d == 1: return (b, z)
            b.append(str(z))
            return (b, None)
    return ','.join(reduce(Rangify, zip(zones, deltas), ([], None))[0])

描述参数:

  • deltas 是到下一个值的距离(灵感来自 SO 上的 answer
  • Rangify() 减少这些参数
    • b - 基数或累加器
    • p - 之前的开始范围
    • z - 区号
    • d - 三角洲

【讨论】:

  • 你可以假设是不同的,但不一定是排序的。然而,由于列表是数字的,sort() 方法使得解决假设变得相当简单。
  • 实际上,我收回了这一点——我将列表推送到函数中的代码执行了预排序。没关系:)
【解决方案3】:

要连接字符串,您应该使用','.join。这将删除第二个循环。

def createRangeString(zones):
        rangeIdx = 0
        ranges   = [[zones[0], zones[0]]]
        for zone in list(zones):
            if ranges[rangeIdx][1] in (zone, zone-1):
                ranges[rangeIdx][1] = zone
            else:
                ranges.append([zone, zone])
                rangeIdx += 1

       return ','.join(
                map(
                  lambda p: '%s-%s'%tuple(p) if p[0] != p[1] else str(p[0]),
                  ranges
                )
              )

虽然我更喜欢更通用的方法:

from itertools import groupby

# auxiliary functor to allow groupby to compare by adjacent elements.
class cmp_to_groupby_key(object):
  def __init__(self, f):
    self.f = f
    self.uninitialized = True
  def __call__(self, newv):
    if self.uninitialized or not self.f(self.oldv, newv):
      self.curkey = newv
      self.uninitialized = False
    self.oldv = newv
    return self.curkey

# returns the first and last element of an iterable with O(1) memory.
def first_and_last(iterable):
  first = next(iterable)
  last = first
  for i in iterable:
    last = i
  return (first, last)

# convert groups into list of range strings
def create_range_string_from_groups(groups):
  for _, g in groups:
    first, last = first_and_last(g)
    if first != last:
      yield "{0}-{1}".format(first, last)
    else:
      yield str(first)

def create_range_string(zones):
  groups = groupby(zones, cmp_to_groupby_key(lambda a,b: b-a<=1))
  return ','.join(create_range_string_from_groups(groups))

assert create_range_string([0,1,2,3]) == '0-3'
assert create_range_string([0, 1, 2, 4, 8]) == '0-2,4,8'
assert create_range_string([1,2,3,4,6,7,8,9,12,13,19,20,22,22,22,23,40,44]) == '1-4,6-9,12-13,19-20,22-23,40,44'

【讨论】:

    【解决方案4】:

    这更冗长,主要是因为我使用了我拥有的泛型函数,并且是 itertools 函数和配方的微小变体:

    from itertools import tee, izip_longest
    def pairwise_longest(iterable):
        "variation of pairwise in http://docs.python.org/library/itertools.html#recipes"
        a, b = tee(iterable)
        next(b, None)
        return izip_longest(a, b)
    
    def takeuntil(predicate, iterable):
        """returns all elements before and including the one for which the predicate is true
        variation of http://docs.python.org/library/itertools.html#itertools.takewhile"""
        for x in iterable:
            yield x
            if predicate(x):
                break
    
    def get_range(it):
        "gets a range from a pairwise iterator"
        rng = list(takeuntil(lambda (a,b): (b is None) or (b-a>1), it))
        if rng:
            b, e = rng[0][0], rng[-1][0]
            return "%d-%d" % (b,e) if b != e else "%d" % b
    
    def create_ranges(zones):
        it = pairwise_longest(zones)
        return ",".join(iter(lambda:get_range(it),None))
    
    k=[0,1,2,4,5,7,9,12,13,14,15]
    print create_ranges(k) #0-2,4-5,7,9,12-15
    

    【讨论】:

      【解决方案5】:
      def createRangeString(zones):
          """Create a string with integer ranges in the format of '%d-%d'
          >>> createRangeString([0, 1, 2, 4, 8])
          "0-2,4,8"
          >>> createRangeString([1,2,3,4,6,7,8,9,12,13,19,20,22,22,22,23,40,44])
          "1-4,6-9,12-13,19-20,22-23,40,44"
          """
          buffer = []
      
          try:
              st = ed = zones[0]
              for i in zones[1:]:
                  delta = i - ed
                  if delta == 1: ed = i
                  elif not (delta == 0):
                      buffer.append((st, ed))
                      st = ed = i
              else: buffer.append((st, ed))
          except IndexError:
              pass
      
          return ','.join(
                  "%d" % st if st==ed else "%d-%d" % (st, ed)
                  for st, ed in buffer)
      

      【讨论】:

        【解决方案6】:

        这是我的解决方案。当您遍历列表并创建结果时,您需要跟踪各种信息 - 这对我来说是尖叫生成器。所以这里是:

        def rangeStr(start, end):
            '''convert two integers into a range start-end, or a single value if they are the same''' 
            return str(start) if start == end else "%s-%s" %(start, end)
        
        def makeRange(seq):
            '''take a sequence of ints and return a sequence
            of strings with the ranges
            '''
            # make sure that seq is an iterator
            seq = iter(seq)
            start = seq.next()
            current = start
            for val in seq:
                current += 1
                if val != current:
                    yield rangeStr(start, current-1)
                    start = current = val
            # make sure the last range is included in the output
            yield rangeStr(start, current)
        
        def stringifyRanges(seq):
            return ','.join(makeRange(seq))
        
        >>> l = [1,2,3, 7,8,9, 11, 20,21,22,23]
        >>> l2 = [1,2,3, 7,8,9, 11, 20,21,22,23, 30]
        >>> stringifyRanges(l)
        '1-3,7-9,11,20-23'
        >>> stringifyRanges(l2)
        '1-3,7-9,11,20-23,30'
        

        如果给定一个空列表,我的版本将正常工作,我认为其他一些不会。

        >>> stringifyRanges( [] )
        ''
        

        makeRanges 将适用于任何返回整数并延迟返回字符串序列的迭代器,因此可用于无限序列。

        编辑:我已更新代码以处理不属于范围的单个数字。

        edit2:重构了 rangeStr 以消除重复。

        【讨论】:

        • OP did 指定当单个数字出现在列表中时会发生什么。 ` [0, 1, 2, 4, 8] -> "0-2,4,8"`.
        • 谢谢肯尼,我错过了。我已更新代码以正确处理单个数字。
        【解决方案7】:

        这个烂摊子怎么样...

        def rangefy(mylist):
            mylist, mystr, start = mylist + [None], "", 0
            for i, v in enumerate(mylist[:-1]):
                    if mylist[i+1] != v + 1:
                            mystr += ["%d,"%v,"%d-%d,"%(start,v)][start!=v]
                            start = mylist[i+1]
            return mystr[:-1]
        

        【讨论】:

        • 有谁知道如何使用 join 重写或如何避免求和 None 或如何使上述代码更短?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-02-04
        • 1970-01-01
        • 1970-01-01
        • 2012-01-17
        相关资源
        最近更新 更多