7

I am in the process of learning list comprehensions and stumbled into a type of problem that I cannot find resources to adequately understand.

The problem stems from the following question: We have an Array [1,2,3,8,9] and want to create an expression that would return each odd number twice, while even numbers are only returned once.

Note: there's also a hint that I could create nested lists but that so far has not helped me pinpoint how that would serve me.

The output of the appropriate algorithm should be: [1,1,2,3,3,8,9,9]

Using a loop, I could do what I want like this:

OtherNumList = [1, 2, 3, 8, 9]
OtherNumList2 = []
for i in OtherNumList:
    if i%2==1:
        OtherNumList2.append(i)
        OtherNumList2.append(i)
    else:
        OtherNumList2.append(i)
print(OtherNumList2)

I want to do this using just an expression, or otherwise "one-line" it using a list comprehension.

I'm struggling with understanding how to set the comprehension to append twice if X while appending once if Y.

I'd appreciate your help with understanding even just the concept of building the comprehension; I do not expect a spoon-fed solution, and would rather prefer it if you could walk me through your thinking process so that I can better set my own foundations for better list comprehensions in the future! :)

Dkoded
  • 115
  • 6

5 Answers5

10

You can do this in a single list comprehension with no outside tools. You just have to make and walk an inner sequence of values based on the value pulled from the outer sequence:

OtherNumList = [1, 2, 3, 8, 9]
OtherNumList2 = [rep for i in OtherNumList for rep in (i,)*(i%2+1)]
print(OtherNumList2)

The trick here is the second for. It iterates a tuple of one or two copies of i, depending on whether i is even (one copy) or odd (two copies). Conveniently, we don't even need a real boolean check here; (i%2+1) is always 1 for even and 2 for odd already so we can use it to multiply directly. The resulting value is then produced the correct number of times directly, without additional flattening required.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • You are using two loops :O – Slayer Mar 29 '19 at 15:19
  • 3
    @Prerit: Yes, though the inner loop is guaranteed to run precisely 1 or 2 times, so it doesn't affect big-O run time. List comprehensions can contain an arbitrary number of loops within them, with the left most loop cycling the most slowly, and the right most loop cycling the fastest. No solution that doesn't involve a separate generator function or explicit `append`s can avoid the second loop (and even for those, it's basically just an unrolled loop, practically speaking); it might be hidden in `chain` or the like, but it will happen. – ShadowRanger Mar 29 '19 at 15:21
  • Thanks for this, I didn't thought about ever using it in such fashion. :) – Slayer Mar 29 '19 at 15:23
  • I did something like this, but then we need to flatten the list out. `[[value]*(value%2+1) for value in x]` – Slayer Mar 29 '19 at 15:25
  • 1
    @Prerit: Yup. Your solution could make the second loop implicit with `list(itertools.chain.from_iterable([value]*(value%2+1) for value in x))`, with `chain.from_iterable` doing the flattening for you, but it's still an "inner loop", just one done inside `chain` at the C level, not done with `for` at the Python level where you can see it. Note: I chose to multiply a `tuple` rather than a `list` because Python can apply a couple optimizations to `tuple` that don't apply to `list`. Downside is the need for that trailing comma, but it's meaningfully faster here. – ShadowRanger Mar 29 '19 at 15:28
  • Yes, `tuple` is much optimized then list. We can also cast it into a `numpy.ndarray` and then use it's `flatten()` function to make it look a bit cleaner. Again flattening here is O(c*n) operation, where `c = {1, 2}` which still makes the algorithm O(n). – Slayer Mar 29 '19 at 15:39
  • 1
    I'm struggling to come up with a simpler answer. The trick on using the modulo to determine the doubling is neat. Nicely done. – r.ook Mar 29 '19 at 15:42
  • Thank you. While I should have made a note in my original post that I wanted to do this without other "tools", you hit the spot with this answer. Thank you for also explaining the possible running time implications! – Dkoded Mar 29 '19 at 21:50
4

One way could be by generating a nested list, and flattening it afterwards using for instance itertools.chain. The tricky part is creating a flat list straight away, as you'll have to append more than one element at once when the the condition is not satisfied, so you need a little extra work to flatten the resulting list:

from itertools import chain
list(chain.from_iterable([i] if i%2 == 0 else [i]*2 for i in l))

Output

[1, 1, 2, 3, 3, 8, 9, 9]

Although it would seem to me that the optimal way to do this would be with a generator function, or very similarly, the one you've shared, but possibly preferable for large lists:

def my_fun(l):
    for i in l:
        if i%2 == 0:
            yield i
        else:
            yield i
            yield i

list(my_fun(l))
# [1, 1, 2, 3, 3, 8, 9, 9]
yatu
  • 86,083
  • 12
  • 84
  • 139
  • 1
    I'd recommend changing to `chain.from_iterable([i] if i%2 == 0 else [i]*2 for i in l)`, which avoids making many temporary `list`s all at once (unpacking the list comprehension to `chain` requires the whole of `l` to be created and unpacked to a `tuple` of `list`s before any of it can be chained). – ShadowRanger Mar 29 '19 at 15:13
  • True @ShadowRanger, thanks for your suggestion, updated the answer – yatu Mar 29 '19 at 15:17
  • There's also `repeat(i, i%2 + 1)` to replace `[i] if i%2 == 0 else [i] *2`, but that's probably an acquired taste :) (`list(chain.from_iterable(repeat(i, i%2+1) for i in l))`) – chepner Mar 29 '19 at 15:18
  • I sometimes feel that `chain` is one of the most underappreciated things provided by the `itertools` module. – chepner Mar 29 '19 at 15:23
  • @chepner: For something that small, simply multiplying a `list` or `tuple` is likely faster, e.g. `(i,) * (i%2+1)`; `repeat` is great for long repetitions, but the fixed overhead is significantly higher than multiplying short sequences (especially `tuple`, where multiplying by `1` doesn't even produce a new `tuple`, and short `tuple`s are cached in a free list so the length two `tuple` doesn't require a real allocation). – ShadowRanger Mar 29 '19 at 15:23
  • @ShadowRanger Agreed, but this seemed like the answer on which to comment on `repeat`'s existence. – chepner Mar 29 '19 at 15:32
  • Yes @chepner `repeat` is indeed a nice alternative in these cases and worth mentioning, thanks for pointing out – yatu Mar 29 '19 at 15:44
  • 1
    Thank you for your answer; it was my mistake to not mention I wanted to do this without other "tools" like itertools, but you taught me something new so I can only be grateful! – Dkoded Mar 29 '19 at 21:52
1
import numpy as np
num_list = [1, 2, 3, 8, 9]
new_list = []

for x in num_list:
    new_list.extend(np.repeat(x, 2, axis=0)) if x%2 == 1 else new_list.append(x)
Emma Jean
  • 507
  • 3
  • 12
  • Thanks for this Emma :), although as I said to the rest of the guys who (due to my mistake) gave answers using other libraries, I should have mentioned I wanted to achieve this without the aid of other tools/libraries. Nonetheless it's also another way to achieve what I wanted so kudos for the teaching moment :) – Dkoded Mar 29 '19 at 21:59
1

One idea is to first give a list of lists, just as the hint given to you said.

nested_list = [[i] if i % 2 == 0 else [i] * 2 for i in NumList] 

This would give you the following:

[[1, 1,], [2], [3, 3], [8], [9, 9]]

Now you just need to flatten this array in the same line. To do this, I refer to the top answer here: How to make a flat list out of list of lists?

Stefan G.
  • 167
  • 7
  • I believe the hint for nested lints points here, but it occurs to me that it can't be one line (entirely) if we need to flatten the nested lists with a second action. I guess since it's a rather "elementary" practice exercise it's probably best that I learn the plethora of possible ways to arrive at the same outcome. Thanks for taking the time to contribute :) – Dkoded Mar 29 '19 at 21:56
1

I believe this what the author means by using nested list in the hint.

x = [1, 2, 3, 8, 9]
[[value]*(value%2+1) for value in x]
Slayer
  • 832
  • 1
  • 6
  • 21