In a recent conversation, someone shared some code from a book about technical job interviews. They wanted to know if I agreed that the code was “Pythonic.”
The problem was to find the runs of increasing and decreasing values in a list, and to produce a sequence of the runs, but to reverse the decreasing runs, so that they are also increasing. This was the “Pythonic” code:
import itertools
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())
]
mono_runs_pythonic([1, 2, 3, 2, 1, 4, 5, 6, 7])
# --> [1, 2, 3], [1, 2], [4, 5, 6, 7]
My first response was that I don’t like this code, because I had to read it with my eyebrows. That is, I furrow my brow, and read slowly, and scowl at the code as I puzzle through it. This code is dense and tricky.
Is it Pythonic? I guess in the sense that it uses a number of Python-specific constructs and tools, yes. But not in the sense of Python code being clear and straightforward. It uses Python thoroughly, but misses the spirit.
I tried my hand at my own solution. It came out like this:
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
This code also uses some unusual Python techniques, but is clearer to me. I’m not sure everyone would agree it is clearer. Maybe you have an even better way to do it.
Aside from the question of which code is better, I also didn’t like that this code was presented as a good solution for a job interview. Studying code like this to learn intricate tricks of Python is not a good way to get a job. Or, it might be a good way to get a job, but I don’t like that it might work. Job interviews should be about much deeper concerns than whether you know little-visited corners of the Python standard library.
Comments
got
[[1, 2, 3], [1, 2], [8], [4], [5, 6, 7]]
why not
[[1, 2, 3], [1, 2], [8], [4, 5, 6, 7]]
or
[[1, 2, 3], [1, 2], [4, 8], [ 5, 6, 7]]
[1, 2, 3, 1, 2, 3, 1, 2, 3] should yield [ [1, 2, 3], [1, 2, 3], [1, 2, 3] ]
Yours is more in line with PEP 20, but I also feel that we could get it even friendlier to beginners. I probably won't have time, but if I do, I may try my hand at this same problem!
Really nice write up for the day, great way for me to start Friday!
I have a simple minded solution to the problem, which handles the edge cases as well:
https://gist.github.com/babo/f93fd294a93cc9e4ccbb0723d7a5cb25
Examples:
def mono_runs(seq):
...: keys = itertools.chain([True], map(operator.le, seq, seq[1:]))
...: return [list(group)[::1 if key else -1] for key, group in itertools.groupby(seq, lambda _: next(keys))]
```
I like the Numpy solution. If someone claims it's cheating, I'd reply with "Although practicality beats purity".
Numpy is often the best-scaling solution.
No imports, let's timsort handle the reversal (timsort is good at it), just some list comprehensions, flat, the conditions are not hidden, no edge cases, ...
This is particularly relevant because if we require working against values that carry information that doesn't factor into the comparison, then "reverse via sort" against a decreasing run will not reverse the order of elements that compare equal. So, if they're numeric values, then it's fine, but if there's some kind of "priority-but-FIFO-if-equal-priority" logic baked into a types comparison operations, then explicitly doing the reverse is necessary.
Why would anyone pass that kind of value into this function? I don't know, what's the use case for literally any of this?
But why is the second solution worse? Well, it just has way more conditionals, and way worse variable naming (any 1-2 letter variable should be a red flag for rushed-out code).
Sorry, not sorry. If you interview for a Python job you should have an idea of what's in the stdlib. Access to the python docs (just that) during interview seems like a fair compromise to me - can't reasonably expect anyone to know exactly if it's itertools.groupby or itertools.group_by.
To my understanding there must be at least two items in a run before we can determine whether it's increasing or decreasing. We end each run when the next input item either changes direction or matches the current input item.
I probably should return an iterator and make use of test tools to be more Pythonic, but I was focused on the logic. I don't claim to be a real coder.
Comments welcome, snide remarks encouraged! RESULTS:
Can a sequence of zero elements be in the list? I am not certain that the direction of the sequences are strictly alternating.
Here are my assumptions:
Approaches with iter or generators requires comments as non-beginner friendly, or even for "future us".
This solution address as well the case of multiple mono-item groups like suggested in the first comment by jiamo:
Add a comment: