4

From an iterable, I'd like to generate an iterable of its prefixes (including the original iterable itself).

for prefix in prefixes(range(5)):
    print(tuple(prefix))

should result in

(0,)
(0, 1)
(0, 1, 2)
(0, 1, 2, 3)
(0, 1, 2, 3, 4)

or in

()
(0,)
(0, 1)
(0, 1, 2)
(0, 1, 2, 3)
(0, 1, 2, 3, 4)

and

for prefix in prefixes('Hello'):
    print(''.join(prefix))

should result in

H
He
Hel
Hell
Hello

or in


H
He
Hel
Hell
Hello

(Whether the empty prefix is part of the result doesn't matter too much for me, nor does the exact type of the inner or outer resulting iterables.)

I was able to devise several ways to implement this, but all feel at least slightly clunky:

using slicing & len:

(works if the iterable is a sequence)

def prefixes(seq):
    for i in range(len(seq)):
        yield seq[:i + 1]

or using a list comprehension:

def prefixes(seq):
    return [seq[:i + 1] for i in range(len(seq))]

... or a generator expression

def prefixes(seq):
    return (seq[:i + 1] for i in range(len(seq)))

(These don't yield the empty prefix. To include it, replace [i + 1] by just [i] and range(len(seq)) by range(len(seq) + 1) in any of the above.)

These feel clunky:

  • because they don't work for all kinds iterable inputs
  • because of the need for the + 1 offset
  • calling range on the len of something (though enumerate wouldn't make it better here)

using concatenation

def prefixes(iterable):
    result = ()
    for elem in iterable:
        result += (elem,)
        yield result

(Doesn't include the empty prefix. This can be changed by yielding result already once before the for-loop.)

or using itertools.accumulate

from itertools import accumulate as acc

def prefixes(iterable):
    return acc(iterable, lambda t, elem: t + (elem,), initial=())

or a bit more readable:

from itertools import accumulate

def _append(iterable, elem):
    return iterable + (elem,)

def prefixes(iterable):
    return accumulate(iterable, _append, initial=())

(These two include the empty prefix. Drop it if unwanted.)

These feel clunky due to the need to pack elements into length-one containers just to concatenate them to an existing one.

Solutions that are more elegant?

I feel like I must be missing something from itertools, functools, operator or more-itertools that would allow for a slightly or even significantly less clunky implementation. I mean, this is eerily similar to more_itertools.powerset, just a, well, rather specific subset of it.

das-g
  • 9,718
  • 4
  • 38
  • 80
  • 3
    Isn't this question very opinion-based? – Riccardo Bucco Jan 05 '22 at 00:05
  • 1
    I voted to close this as opinion-based because what you have is a problem that you already know how to solve in several different ways, and the question is only which one we think is subjectively most elegant or least clunky. If there is something specifically bad about all of these solutions and you're looking for a new solution which doesn't have that specific thing wrong with it, then that could be a question with a definite answer - but you haven't really identified anything specifically bad about your solutions. Or rather, you've identified multiple bad things about each solution but ... – kaya3 Jan 05 '22 at 00:23
  • 1
    ... no specific criteria for what would make a different solution better. – kaya3 Jan 05 '22 at 00:24
  • Would you advise to delete this question or to try to augment it with criteria for what "more elegant" shall mean? The latter might be difficult. I do know that I consider mkrieger's `itertools.accumulate(map(lambda x: (x,), iterable))` a tremendous improvement over my approaches, but other than it lacking (all but one of) the pain points I listed and being much more consise, I can't point my finger on what makes it that much better. – das-g Jan 05 '22 at 00:32
  • 1
    I think the problem is that that `map` solution *does* lack all but one of your pain points, yet it *isn't* really better, which means your question doesn't really get at what you're trying to achieve. Generally, "what's the most elegant solution to X" is off-topic on Stack Overflow. That doesn't necessarily mean your question needs to be deleted, so long as it can be edited to make it not opinion-based, though personally I can't think of an objective criterion for what you're looking for in this specific question (otherwise I would have proposed an edit instead of voting to close). – kaya3 Jan 05 '22 at 00:47

3 Answers3

2

Similar to your first concatenation example, but building a list instead of a tuple:

def prefixes(iterable):
    result = []
    for elem in iterable:
        result.append(elem)
        yield result

This eliminates the necessity of creating temporary one-element tuples.

Paul Cornelius
  • 9,245
  • 1
  • 15
  • 24
  • 3
    This only works if each result is always used and discarded before the next one is requested, and if the result is never mutated. Otherwise, if you do something like `list(prefixes(...))` then you will get the wrong thing. So for most purposes you want `yield tuple(result)` instead of just `yield result`. – kaya3 Jan 05 '22 at 00:18
  • @kaya3 "for most purposes" - Maybe. But not for the purposes shown in the question... – Kelly Bundy Jan 05 '22 at 18:13
  • Although `yield islice(result, len(result))` might be good. Same time complexity, and avoids the possible issues @kaya3 mentioned. More costly than *"creating temporary one-element tuples"*, but I don't think that's the real advantage of this answer anyway (which in my opinion is the O(1) time per prefix). – Kelly Bundy Jan 05 '22 at 19:13
  • Or a "view" of the current prefix, in case a user doesn't want to iterate each prefix but for example only access the last three elements. – Kelly Bundy Jan 05 '22 at 19:15
2

It may be considered elegant to write the prefixes function in any generalized way that works, put it in a module, and then import it in the code where it is needed, so that it doesn't matter how it is implemented.

On the other hand, requiring an extra import can be perceived as less elegant than a short local function that is less generic but more tailored to the specific use case.

This is one possible quite generic solution:

def prefixes(iterable):
    return itertools.accumulate(map(lambda x: (x,), iterable))

There are reasons for it to be considered elegant:

  • It uses a function that is already available in the standard library and achieves the primary goal,
  • it does not explicitly mention the concatenation which accumulate already does implicitly,
  • it does not require the initial argument to accumulate.

But some find using map and lambda to be less elegant than a for loop.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
0

This isn't fully fleshed-out, and it's also a bit dorky:

def prefixes(iterable):
    from itertools import tee, islice
    iterator = iter(iterable)
    length = len(iterable)
    for slice_length, it in enumerate(tee(iterator, length), start=1):
        yield islice(it, slice_length)


for prefix in prefixes(range(5)):
    print(tuple(prefix))

for prefix in prefixes("Hello"):
    print("".join(prefix))

Output:

(0,)
(0, 1)
(0, 1, 2)
(0, 1, 2, 3)
(0, 1, 2, 3, 4)
H
He
Hel
Hell
Hello

You end up making n+1 independent iterators of the iterable. You also need to know the length of the iterable in advance, or be able to take the length of it (so you can't pass in a generator.)

Paul M.
  • 10,481
  • 2
  • 9
  • 15