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()]