繁体   English   中英

大多数Pythonic方法通过重复元素拆分数组

[英]Most Pythonic Way to Split an Array by Repeating Elements

我有一个项目列表,我想根据分隔符进行拆分。 我希望删除所有分隔符,并在分隔符出现两次时拆分列表。 例如,如果分隔符为'X' ,则以下列表:

['a', 'b', 'X', 'X', 'c', 'd', 'X', 'X', 'f', 'X', 'g']

会变成:

[['a', 'b'], ['c', 'd'], ['f', 'g']]

请注意,最后一组未拆分。

我写了一些丑陋的代码来做到这一点,但我确信有更好的东西。 如果您可以设置任意长度分隔符(即在看到N个分隔符后拆分列表),则需要额外的分数。

我不认为这会有一个很好的,优雅的解决方案(我当然希望被证明是错误的)所以我会建议一些简单明了的事情:

def nSplit(lst, delim, count=2):
    output = [[]]
    delimCount = 0
    for item in lst:
        if item == delim:
            delimCount += 1
        elif delimCount >= count:
            output.append([item])
            delimCount = 0
        else:
            output[-1].append(item)
            delimCount = 0
    return output

>>> nSplit(['a', 'b', 'X', 'X', 'c', 'd', 'X', 'X', 'f', 'X', 'g'], 'X', 2)
[['a', 'b'], ['c', 'd'], ['f', 'g']]

这是使用itertools.groupby()执行此操作的方法:

import itertools

class MultiDelimiterKeyCallable(object):
    def __init__(self, delimiter, num_wanted=1):
        self.delimiter = delimiter
        self.num_wanted = num_wanted

        self.num_found = 0

    def __call__(self, value):
        if value == self.delimiter:
            self.num_found += 1
            if self.num_found >= self.num_wanted:
                self.num_found = 0
                return True
        else:
            self.num_found = 0

def split_multi_delimiter(items, delimiter, num_wanted):
    keyfunc = MultiDelimiterKeyCallable(delimiter, num_wanted)

    return (list(item
                 for item in group
                 if item != delimiter)
            for key, group in itertools.groupby(items, keyfunc)
            if not key)

items = ['a', 'b', 'X', 'X', 'c', 'd', 'X', 'X', 'f', 'X', 'g']

print list(split_multi_delimiter(items, "X", 2))

我必须说,对于相同的结果,cobbal的解决方案要简单得多。

使用生成器函数通过列表维护迭代器的状态,以及到目前为止看到的分隔符字符数的计数:

l = ['a', 'b', 'X', 'X', 'c', 'd', 'X', 'X', 'f', 'X', 'g'] 

def splitOn(ll, x, n):
    cur = []
    splitcount = 0
    for c in ll:
        if c == x:
            splitcount += 1
            if splitcount == n:
                yield cur
                cur = []
                splitcount = 0
        else:
            cur.append(c)
            splitcount = 0
    yield cur

print list(splitOn(l, 'X', 2))
print list(splitOn(l, 'X', 1))
print list(splitOn(l, 'X', 3))

l += ['X','X']
print list(splitOn(l, 'X', 2))
print list(splitOn(l, 'X', 1))
print list(splitOn(l, 'X', 3))

打印:

[['a', 'b'], ['c', 'd'], ['f', 'g']]
[['a', 'b'], [], ['c', 'd'], [], ['f'], ['g']]
[['a', 'b', 'c', 'd', 'f', 'g']]
[['a', 'b'], ['c', 'd'], ['f', 'g'], []]
[['a', 'b'], [], ['c', 'd'], [], ['f'], ['g'], [], []]
[['a', 'b', 'c', 'd', 'f', 'g']]

编辑:我也是groupby的忠实粉丝,这是我的看法:

from itertools import groupby
def splitOn(ll, x, n):
    cur = []
    for isdelim,grp in groupby(ll, key=lambda c:c==x):
        if isdelim:
            nn = sum(1 for c in grp)
            while nn >= n:
                yield cur
                cur = []
                nn -= n
        else:
            cur.extend(grp)
    yield cur

与我之前的回答没有什么不同,只需让groupby负责迭代输入列表,创建分隔符匹配和非分隔符匹配字符组。 不匹配的字符只是添加到当前元素上,匹配的字符组执行分解新元素的工作。 对于长列表,这可能会更高效,因为groupby在C中完成所有工作,并且仍然只迭代列表一次。

a = ['a', 'b', 'X', 'X', 'c', 'd', 'X', 'X', 'f', 'X', 'g']
b = [[b for b in q if b != 'X'] for q in "".join(a).split("".join(['X' for i in range(2)]))]

这给了

[['a', 'b'], ['c', 'd'], ['f', 'g']]

其中2是您想要的元素数量。 有可能更好的方法来做到这一点。

非常难看,但我想知道我是否可以将其作为一个单行使用,我想我会分享。 我请求你不要将这个解决方案用于任何重要的事情。 末尾的('X', 3)是分隔符和应该重复的次数。

(lambda delim, count: map(lambda x:filter(lambda y:y != delim, x), reduce(lambda x, y: (x[-1].append(y) if y != delim or x[-1][-count+1:] != [y]*(count-1) else x.append([])) or x, ['a', 'b', 'X', 'X', 'c', 'd', 'X', 'X', 'f', 'X', 'g'], [[]])))('X', 2)

编辑

这是一个故障。 我还删除了一些冗余代码,这些代码在写出来时更加明显。 (也改为上面)

# Wrap everything in a lambda form to avoid repeating values
(lambda delim, count:
    # Filter all sublists after construction
    map(lambda x: filter(lambda y: y != delim, x), reduce(
        lambda x, y: (
            # Add the value to the current sub-list
            x[-1].append(y) if
                # but only if we have accumulated the
                # specified number of delimiters
                y != delim or x[-1][-count+1:] != [y]*(count-1) else

                # Start a new sublist
                x.append([]) or x,
        ['a', 'b', 'X', 'X', 'c', 'd', 'X', 'X', 'f', 'X', 'g'], [[]])
    )
)('X', 2)

这是一个使用zip和发电机的干净漂亮的解决方案

#1 define traditional sequence split function 
#if you only want it for lists, you can use indexing to make it shorter
def split(it, x):
    to_yield = []
    for y in it:
        if x == y:
            yield to_yield
            to_yield = []
        else:
            to_yield.append(y)
    if to_yield:
        yield to_yield

#2 zip the sequence with its tail 
#you could use itertools.chain to avoid creating unnecessary lists
zipped = zip(l, l[1:] + [''])

#3. remove ('X',not 'X')'s from the resulting sequence, and leave only the first position of each
# you can use list comprehension instead of generator expression
filtered = (x for x,y in zipped if not (x == 'X' and y != 'X'))

#4. split the result using traditional split
result = [x for x in split(filtered, 'X')]

这种方式split()更可重用。

令人惊讶的是,python没有内置的。

编辑:

您可以轻松调整它以获得更长的分割序列,重复步骤2-3并使用l [i:]进行压缩过滤,以获得0 <i <= n。

import re    
map(list, re.sub('(?<=[a-z])X(?=[a-z])', '', ''.join(lst)).split('XX'))

这会执行list - > string - > list转换,并假定非分隔符字符都是小写字母。

这是另一种方法:

def split_multi_delimiter(items, delimiter, num_wanted):
    def remove_delimiter(objs):
        return [obj for obj in objs if obj != delimiter]

    ranges = [(index, index+num_wanted) for index in xrange(len(items))
              if items[index:index+num_wanted] == [delimiter] * num_wanted]

    last_end = 0
    for range_start, range_end in ranges:
        yield remove_delimiter(items[last_end:range_start])
        last_end = range_end

    yield remove_delimiter(items[last_end:])

items = ['a', 'b', 'X', 'X', 'c', 'd', 'X', 'X', 'f', 'X', 'g']
print list(split_multi_delimiter(items, "X", 2))
In [6]: input = ['a', 'b', 'X', 'X', 'cc', 'XX', 'd', 'X', 'ee', 'X', 'X', 'f']

In [7]: [s.strip('_').split('_') for s in '_'.join(input).split('X_X')]
Out[7]: [['a', 'b'], ['cc', 'XX', 'd', 'X', 'ee'], ['f']]

这假设您可以使用输入中找不到的保留字符,例如_

太聪明了一半,只提供了因为显而易见的正确方法似乎是如此蛮力和丑陋:

class joiner(object):
  def __init__(self, N, data = (), gluing = False):
    self.data = data
    self.N = N
    self.gluing = gluing
  def __add__(self, to_glue):
    # Process an item from itertools.groupby, by either
    # appending the data to the last item, starting a new item,
    # or changing the 'gluing' state according to the number of
    # consecutive delimiters that were found.
    N = self.N
    data = self.data
    item = list(to_glue[1])
    # A chunk of delimiters;
    # return a copy of self with the appropriate gluing state.
    if to_glue[0]: return joiner(N, data, len(item) < N)
    # Otherwise, handle the gluing appropriately, and reset gluing state.
    a, b = (data[:-1], data[-1] if data else []) if self.gluing else (data, [])
    return joiner(N, a + (b + item,))

def split_on_multiple(data, delimiter, N):
  # Split the list into alternating groups of delimiters and non-delimiters,
  # then use the joiner to join non-delimiter groups when the intervening
  # delimiter group is short.
  return sum(itertools.groupby(data, delimiter.__eq__), joiner(N)).data

正则表达式,我选择你!

import re

def split_multiple(delimiter, input):
    pattern = ''.join(map(lambda x: ',' if x == delimiter else ' ', input))
    filtered = filter(lambda x: x != delimiter, input)
    result = []
    for k in map(len, re.split(';', ''.join(re.split(',',
        ';'.join(re.split(',{2,}', pattern)))))):
        result.append([])
        for n in range(k):
            result[-1].append(filtered.__next__())
    return result

print(split_multiple('X',
    ['a', 'b', 'X', 'X', 'c', 'd', 'X', 'X', 'f', 'X', 'g']))

哦,你说的是Python,而不是Perl。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM