1

I sometimes find myself in a situation - similar to the following hypothetical - but I can't seem to come up with a satisfactory, Pythonic solution:

Let's say we have a list of numbers:

numbers = [1, 2, 3, 5, 6, 8, 9]

We would like to group the numbers into sub-lists, such that adjacent numbers with an absolute difference of exactly 1 will be part of the same group. The desired result in this case would be:

grouped_numbers = [[1, 2, 3], [5, 6], [8, 9]]

On the surface, it seems like itertools.groupby is the tool for the job. However, this grouping operation is different from the average, trivial application of itertools.groupby, because the key (the callable that returns the distinct "category-objects", which correspond to the distinct groups that a given item should belong to) must retain some kind of state between invocations by itertools.groupby because whether the "current" group terminates or continues depends on the previous item.

One way to achieve this is by using persistent default-arguments. This solution works, but it feels really kludgy:

from itertools import groupby

def key(number, prev_number=[None], prev_key=[object()]):
    if prev_number[0] is None or abs(number - prev_number[0]) != 1:
        prev_key[0] = object()
    prev_number[0] = number
    return prev_key[0]

print([list(group) for _, group in groupby(numbers, key=key)])

Surely, more Python solutions must exist, and my question is: What are they? I would prefer solutions using itertools recipes, or anything that's available in the standard library.

Paul M.
  • 10,481
  • 2
  • 9
  • 15
  • @Manuel That is helpful, but the accepted answer in that thread relies on a third-party module. The other answers are more lengthy than my solution, or they assume that I'm specifically asking about grouping continuous, ascending numbers, which isn't the case - my example is only a hypothetical. I'd like to know if there exists a general solution, using only the standard library, that can be applied to all kinds of similar grouping tasks, where the current group depends on either the immediate previous item, or possibly even multiple previous items in some way. – Paul M. Mar 06 '21 at 23:00
  • Hmm, ok, maybe make it clearer that that's only an example? Perhaps add a different use case as well? – Manuel Mar 06 '21 at 23:07
  • [hacky generator version](https://tio.run/##fZDBTsMwEETv/orhZksGqVSggtRf4NRbVVWEbNItiW2tnUK@PrhJSUFI7Gk1nqeZdejTwbvlKsgwVOJbcCJJ3jcR3AYvCbX4LhS9Uq5rC5KINbYLi3uLpcWDxaPFyuJpp1RJFd6p1@ZZIU8QOu0nKDMv3tFVzras@eJIb0mbUf84cEPYSEcTfp6Z7pmackbnd65@pXAcY@AFr0XUF/X2p8fgZo3FNeG/Rn@PmBY1ecdTlaPPpPNqVBB2SW8bjknXBlVusbeowe77Dy@Noj2zd5FcaXZmGL4A). – Manuel Mar 06 '21 at 23:23
  • [improved version](https://tio.run/##fZDRisIwEEXf8xXXtwSioLLiCv6FbyJi7bRmbZMwSdV@fU2tqwsL3qdJ5szNnfg2npydLz13XcGuhonE0bkqwNTecUTJrvFZK4Rt6ow4YI3tVGOmMdf40lhoLDW@d0LkVOBMrVQrgSTPdNmncxpw2Q8do1Tv@8EstVpDVf5iH8D1ZCrChhsajHp94nuZAocsyCc2/vuIwmiN6dvqU7b/@YZCDOxjO2HpFmUqlfBsbJTbyoQoS4XCMfYaJYz9/bZnoqD72Ukgm6ud6ro7) – Manuel Mar 06 '21 at 23:29
  • @Manuel Sorry if I didn't make it clear initially. Thanks for your feedback - using a coroutine is an interesting approach that I hadn't considered before. I like that the persistent default-arguments can be eliminated, since we get state between invocations for free. Thanks again! – Paul M. Mar 07 '21 at 13:27
  • Yes, that persistence is what made me try that. And then being able to remove the "is-this-the-first-value" check (in the improved version) was a nice bonus. The whole thing still feels quite hacky, though :-) – Manuel Mar 07 '21 at 15:58

0 Answers0