Reflections on “Pythonic monotonic”

Ned Batchelder published an interesting blog post about “pythonic” code. It was about splitting a list of numbers into monotonic series in a sorted way. For Ned surprise was that the “pythonic” version of the solution was not clear for him. Below you have this version.

def mono_runs_pythonic(seq):
    class Monotonic:
        def __init__(self):
            self._last = float("-inf")

        def __call__(self, curr):
            res = curr < self._last
            self._last = curr
            return res

    return [
        list(group)[::-1 if is_decreasing else 1]
        for is_decreasing, group in itertools.groupby(seq, Monotonic())
    ]

So Ned to prepare something clearer for him. And here is this version below.

def mono_runs_simpler(seq):
    seqit = iter(seq)
    run = [next(seqit)]
    up = True
    for v in seqit:
        good = (v > run[-1]) if up else (v < run[-1])
        if good:
            run.append(v)
        else:
            yield run if up else run[::-1]
            run = [v]
            up = not up
    if run:
        yield run

Reflections

My first reaction to both of these examples was that I have no idea what is expected result in both of them. One is using pseudo object just to persist the state of processing. Second if very procedural describing steps of processing, but even variable names give me no knowledge about the domain of the problem. In comments under the blog post, there are many proposals with a functional approach, but none of them are concentrating on readability for someone who just has a code, and want to understand what is expected result.

Proposal

So let’s try to give some proper language into this code and some context. I found out from the Wikipedia page that the monotonic function is preserving ordering. It can be increasing or decreasing. In our case, we want to split the list of numbers into monotonous in a weak manner. Weakly monotonic means that the next element in the list can be equal to the previous.

So for me, we have some domain policies about Monotonic list, and processing function that is splitting the list into sorted monotonous.

Below you have my try on this. Hope it tells a little more about context and domain.

class Monotonic(UserList):
    @property
    def populated(self) -> bool:
        return len(self.data) >= 2

    @property
    def increasing(self) -> bool:
        if not self.populated:
            return True
        first, *rest = self.data
        not_equals = (item for item in rest if item != first)
        to_compare = next(not_equals, None)
        return to_compare is None or first < to_compare

    def order_preserving(self, item: float) -> bool:
        if not self.populated:
            return True

        last = self.data and self.data[-1] or float('-inf')
        if item == last:
            return True

        if item > last if self.increasing else item < last:
            return True

        return False


def split_into_monotonous(seq: Sequence[float]) -> List[List[float]]:
    """
    >>> split_into_monotonous([1, 2, 3, 2, 1, 4, 5, 6, 7])
    [[1, 2, 3], [1, 2], [4, 5, 6, 7]]
    >>> split_into_monotonous([1, 2, 3, 1, 2, 3, 1, 2, 3])
    [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
    >>> split_into_monotonous([1, 2, 3, 2, 1, 8, 4, 5, 6, 7])
    [[1, 2, 3], [1, 2], [4, 8], [5, 6, 7]]
    """
    def _split() -> Iterator[List[float]]:
        processing = []
        for item in seq:
            if Monotonic(processing).order_preserving(item):
                processing.append(item)
            else:
                yield processing
                processing = [item]
        yield processing

    return [sorted(monotonic) for monotonic in _split()]

Hi there 👋
It’s nice to meet you.

Sign up to join my mailing list.

I don’t spam!