Help needed improving Python code using List Comprehensions

list-comprehension, python

Solution

As other answers pointed out, you don't really need to worry unless your `oil_changes` list is extremely long. However, as a fan of "stream-based" computing, I think it's interesting to point out that `itertools` offers all the tools you need to compute your `next_oil` value in O(1) space (and O(N) time of course!-) no matter how big N, that is, `len(next_oil)`, gets.

`izip` per se is insufficient, because it only reduces a bit the multiplicative constant but leaves your space demands as O(N). The key idea to bring those demands down to O(1) is to pair `izip` with `tee` -- and avoiding the list comprehension, which would be O(N) in space anyway, in favor of a good simple old-fashioned loop!-). Here comes:

  it = iter(oil_changes)
  a, b = itertools.tee(it)
  b.next()
  thesum = 0
  for thelen, (i, j) in enumerate(itertools.izip(a, b)):
    thesum += j - i
  last_one = j
  next_oil = last_one + thesum / (thelen + 1)

Instead of taking slices from the list, we take an iterator on it, tee it (making two independently advanceable clones thereof), and advance, once, one of the clones, `b`. `tee` takes space O(x) where x is the maximum absolute difference between the advancement of the various clones; here, the two clones' advancement only differs by 1 at most, so the space requirement is clearly O(1).

`izip` makes a one-at-a-time "zipping" of the two slightly-askew clone iterators, and we dress it up in `enumerate` so we can track how many times we go through the loop, i.e. the length of the iterable we're iterating on (we need the +1 in the final expression, because `enumerate` starts from 0!-). We compute the sum with a simple `+=`, which is fine for numbers (`sum` is even better, but it wouldn't track the length!-).

It's tempting after the loop to use `last_one = a.next()`, but that would not work because `a` is actually exhausted -- `izip` advances its argument iterables left to right, so it has advanced `a` one last time before it realizes `b` is over!-). That's OK, because Python loop variables are NOT limited in scope to the loop itself -- after the loop, `j` still has the value that was last extracted by advancing `b` before `izip` gave up (just like `thelen` still has the last count value returned by `enumerate`). I'm still naming the value `last_one` rather than using `j` directly in the final expression, because I think it's clearer and more readable.

So there it is -- I hope it was instructive!-) -- although for the solution of the specific problem that you posed this time, it's almost certain to be overkill. We Italians have an ancient proverb -- "Impara l'Arte, e mettila da parte!"... "Learn the Art, and then set it aside" -- which I think is quite applicable here: it's a good thing to learn advanced and sophisticated ways to solve very hard problems, in case you ever meet them, but for all that you need to go for simplicity and directness in the vastly more common case of simple, ordinary problems -- not apply advanced solutions that most likely won't be needed!-)

Problem

I've been writing little Python programs at home to learn more about the language. The most recent feature I've tried to understand are List Comprehensions. I created a little script that estimates when my car needs its next oil change based on how frequently I've gotten the oil changed in the past. In the code snippet below, `oil_changes` is a list of the mileages at which I got the oil changed. ``` # Compute a list of the mileage differences between each oil change. diffs = [j - i for i, j in zip(oil_changes[:-1], oil_changes[1:])] # Use the average difference between oil changes to estimate the next change. next_oil = oil_changes[-1] + sum(diffs) / len(diffs) ``` The code produces the right answer (did the math by hand to check) but it doesn't feel quite Pythonic yet. Am I doing a lot of needless copying of the original list in the first line? I feel like there's a much better way to do this but I don't know what it is.

Original source