Python address index +1 with list comprehension

Posted on

Problem

Task from CodingBat:

Return the sum of the numbers in the array, returning 0 for an empty array. Except the number 13 is very unlucky, so it does not count and numbers that come immediately after a 13 also do not count.

My original answer to the problem:

def sum13(nums):
  sum = 0
  for idx,val in enumerate(nums):
    if val == 13 or (idx != 0 and nums[idx-1] == 13):
      pass
    else:
      sum = sum + val

  return sum    

Doing this with list comprehension, I came up with

return sum([x if x!=13 and nums[idx-1 if idx >0 else 0] !=13 else 0 for idx,x in enumerate(nums)])

Is there a way to make this cleaner?

Solution

For the record, I think your original answer is quite clean and readable. My only suggestion would be to consider using if not (predicate): (do something), as opposed to if (predicate): pass; else: (do something):

def sum13(nums):
    sum = 0
    for idx,val in enumerate(nums):
        if not (val == 13 or (idx != 0 and nums[idx-1] == 13)):
            sum += val
    return sum

I like @Josay’s suggestion of iterating over pairs of consecutive items. The easiest way to do this is by zipping the list with the list starting from index 1 — i.e., zip(L, L[1:]). From there, it’s just a matter of taking the second item of each pair, unless either of the items == 13. In order to consider the very first item in the list, we’ll prepend 0 onto the beginning of the list, so that the first pair is [0, first-item]. In other words, we are going to zip together [0] + L (the list with 0 prepended) and L (the list itself). Here are two slightly different versions of this approach:

Version 1, which is more similar to your original answer:

def sum13(nums):
    sum = 0
    for first, second in zip([0] + nums, nums):
        if not 13 in (first, second):
            sum += second
    return sum

Version 2, a functional approach using a list comprehension:

def sum13(nums):
    pairs = zip([0] + nums, nums)
    allowed = lambda x, y: 13 not in (x, y) # not 13 or following a 13
    return sum(y for x, y in pairs if allowed(x, y))

EDIT: As pointed out by an anonymous user, my first version did not skip numbers that follow an even number of 13’s.

Use an iterator. While you for loop over an iterator you can skip items with next.

def lucky_nums(nums):
    nums = iter(nums)
    for i in nums:
        if i == 13:
            while next(nums) == 13:
                pass
        else:
            yield i

print sum(lucky_nums([12,13,14,15]))

It’s a little “unclean” checking the previous element each time. You can maintain the loop index yourself to avoid this:

def sum13(nums):
    sum = i = 0
    while i < len(nums):
        if nums[i] == 13:
            i += 2  # Exclude this element, and the next one too.
        else:
            sum += nums[i]
            i += 1
    return sum

This is similar to the iterator/generator answer.

A few simple comments about your original code : you could rewrite if A: pass else do_stuff() without the pass just writing if not A: do_stuff(). In your case, using De Morgan’s laws, your code becomes :

def sum13(nums):
    sum = 0
    for idx,val in enumerate(nums):
        if val != 13 and (idx == 0 or nums[idx-1] != 13):
            sum = sum + val
    return sum

Please note that you have different ways of avoiding accessing the array using indices :

  • Save previous item

For instance :

def sum13(nums):
    sum = 0
    prev = None # or any value different from 13
    for val in nums:
        if val != 13 and prev != 13:
            sum = sum + val
        prev = val
    return sum

Now, a quick comment about your new code : you are summin x if condition else 0 to sum all values matching the condition. You could just use if in your list comprehension to filter out elements you don’t want.

def sum13(nums):
    return sum([x if x!=13 and nums[idx-1 if idx >0 else 0] !=13 else 0 for idx,x in enumerate(nums)])

becomes :

def sum13(nums):
    return sum([x for idx,x in enumerate(nums) if x!=13 and nums[idx-1 if idx >0 else 0] !=13])

Also, your code creates a temporary list which is not really required. You could simply write :

def sum13(nums):
    return sum(x for idx,x in enumerate(nums) if x!=13 and nums[idx-1 if idx >0 else 0] !=13)

Now it seems like an other answer has been given so I don’t have much to say.

Noting your initial response to the problem

def sum13(nums):
  sum = 0
  for idx,val in enumerate(nums):
    if val == 13 or (idx != 0 and nums[idx-1] == 13):
      pass
    else:
      sum = sum + val

  return sum    

you really should write it like this

def sum13(nums):
  sum = 0
  for idx,val in enumerate(nums):
    if not(val == 13 or (idx != 0 and nums[idx-1] == 13)):
      sum = sum + val
  return sum    

there is no reason to add an extra block to an if statement if you don’t have to, I know that a lot of people don’t like the negatives, but if it is writing a negative if statement or writing an empty if statement, you should write the negative if statement, in this case it is straight to the point

Leave a Reply

Your email address will not be published. Required fields are marked *