How to cut a list by specific item?

Viewed 3038

I'm trying to cut a list by specific items in it, for example, I have a list like this:

down = ["a", "b", "c", "d", "b", "e", "r"]

What I want is:

[["a", "b"]["c", "d", "b"] ["e", "r"]]

which is cut after every occurrence of "b".

I wrote something like this:

down = ["a", "b", "c", "d", "b", "e", "r"]
up = []
while down is not []:
    up, down = up.append(down[:(down.index("b") + 1)]), down[(down.index("b") + 1):]

It throws an error:

AttributeError: 'NoneType' object has no attribute 'append'

I can't figure out what's wrong.

8 Answers

You get the error because you assign the result of the list.append() function - which is None - to up in

up, down = up.append(down[: (down.index("b") + 1)]), down[...snipp...] 
#          ^^^^^^^^ returns None

list.append is a "in-place" operation that returns None so up is going to be None in the next iteration.

Keeping closest to what you got you could use

down = ["a", "b", "c", "d", "b", "e", "r"]
up = []
while 'b' in down:
    b_index = down.index('b') + 1
    up.append(down[:b_index])
    down = down[b_index:]
up.append(down)

but simply iterating your original and assembling the sublists in a second list is cleaner in my opinion:

k = ["a", "b", "c", "d", "b", "e", "r"]

result = [[]]
for e in k:
    if e != "b":
        result[-1].append(e)
    else:
        result[-1].append(e)
        result.append([])

if result[-1] == []: 
    result.pop() # thx iBug's comment

print(result) # [['a', 'b'], ['c', 'd', 'b'], ['e', 'r']]

I think that is much clearer then what your code tries to do - your "what I want ["a", "b"]["c", "d", "b"] ["e", "r"]" is not valid python.


A slightly different version of the code would be:

k = ["a", "b", "c", "d", "b", "e", "r"]
b = []
while True:
    try:
        b_idx = k.index("b")
    except: 
        b.append(k)
        break
    else:
        b,k = b+[k[:b_idx+1]],k[b_idx+1:]
print(b) 

But you need far mor searches into your list via .index() and try: except so it has a worse performance then simply iterating the list once.

down = ["a", "b", "c", "d", "b", "e", "r"]
poslist = []
for pos, item in enumerate(down):
    if item == 'b':
        poslist.append(pos)
answerlist = []
for i in range(len(poslist)):
    if i == 0:
        answerlist.append(down[:poslist[i]+1])
    else:
        answerlist.append(down[poslist[i-1]+1:poslist[i]+1])
answerlist.append(down[poslist[i]+1:])
print (answerlist)
# [['a', 'b'], ['c', 'd', 'b'], ['e', 'r']]
down = ["a", "b", "c", "d", "b", "e", "r"]
indices = [i for i, x in enumerate(down ) if x == "b"]
curr=0
master_list=[]
for item in indices:
    master_list.append(down[curr:item+1])
    print(master_list)
    curr=item+1
if curr !=len(down):
    master_list.append(down[curr:len(down)])
print(master_list)
In [59]: k
Out[59]: ['a', 'b', 'c', 'd', 'b', 'e', 'r', 'l', 'f', 'b', 's']

In [60]: indices = [i for i, x in enumerate(k) if x == "b"]  # all index of 'b'

In [61]: aa = k[:indices[0]+1] # first list

In [62]: bb = k[indices[len(indices)-1]+1:]  # last list

In [63]: for i in range(0, len(indices)-1):
    ...:     print(k[indices[i]+1:indices[i+1]+1]) # remaining list
    ...: 
['c', 'd', 'b']
['e', 'r', 'l', 'f', 'b']

In [64]: aa
Out[64]: ['a', 'b']

In [65]: bb
Out[65]: ['s']

list.append(elem) does not return the new list, just modifies the original. That's the reason for your error.

To correct your code without changing the approach:

down = ["a", "b", "c", "d", "b", "e", "r"]
up = []
while True:
    if 'b' in down: # make sure there is a 'b' in the list
        index = down.index("b") # get the index of the first "b"
        up.append(down[:index + 1]) # save first sublist in up
        down = down [index + 1:] # trim the sublist you already saved 
    else:
        up.append(down) # add the rest of down
        break # leave the loop 
print(up)

What would a question like this be without an itertools answer?

You can use groupby in this case, with a custom key that counts past occurrences of 'b':

from itertools import groupby

class CountKey:
    def __init__(self, what):
        self.what = what
        self.count = 0
    def __call__(self, item):
        count = self.count
        if item == self.what:
            self.count += 1
        return count

up = [list(g) for k, g in groupby(down, CountKey('b'))]

Here is a simple one-liner to make the job:

import re

d = ["a", "b", "c", "d", "b", "e", "r"]

s = '(?<=b)'

print ([list(x) for x in re.split(s,"".join(d))])

Output:

[['a', 'b'], ['c', 'd', 'b'], ['e', 'r']]

The difficulty here is to keep the delimiter in the "left" sublist. The re.split() method can do that in association with a positive lookahead.

You can construct a list of indices, then use a list comprehension:

down = ["a", "b", "c", "d", "b", "e", "r"]

n = len(down)
idx = [index+1 for index, value in enumerate(down) if value == 'b']

res = [down[i: j] for i, j in zip([0] + idx, idx + ([n] if idx[-1] != n else []))]

# [['a', 'b'], ['c', 'd', 'b'], ['e', 'r']]

The ternary statement [n] if idx[-1] != n else [] is used to avoid an empty final list if the last value in down is 'b'. This is a two-pass solution, but uses list slicing as opposed to explicitly appending items one at a time.

Related