Pythonic способ преобразования списка целых чисел в строку разделенных запятыми диапазонов
У меня есть список целых чисел, которые мне нужно проанализировать в строке диапазонов.
Например:
[0, 1, 2, 3] -> "0-3"
[0, 1, 2, 4, 8] -> "0-2,4,8"
И так далее.
Я все еще изучаю больше питонических способов обработки списков, и мне это немного сложно. Моя последняя мысль заключалась в создании списка списков, который отслеживает парные номера:
[ [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?
Ответы
Ответ 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(). Тогда разница между значением и count() является постоянной для последовательных значений. groupby() выполняет оставшуюся часть работы
Как полагает Джефф, альтернативой 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)
Обновление: для приведенного здесь списка примеров, версия с перечислением работает примерно на 5% медленнее, чем версия с использованием count() на моем компьютере
Ответ 2
Является ли это питоническим для обсуждения. Но это очень компактно. Реальное мясо находится в функции Rangify()
. Там еще место для улучшения, если вы хотите эффективности или питонизма.
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)
-
Rangify()
выполняется ли уменьшение этих параметров
-
b
- база или аккумулятор
-
p
- предыдущий диапазон запуска
-
z
- номер зоны
-
d
- deltali >
Ответ 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 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 будет работать на любом итераторе, который возвращает целые числа и лениво возвращает последовательность строк, поэтому их можно использовать для бесконечных последовательностей.
edit: Я обновил код для обработки отдельных номеров, которые не являются частью диапазона.
edit2: реорганизован диапазон rangeStr для удаления дублирования.
Ответ 6
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)
Ответ 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]