9

I'm trying my hand at converting the following loop to a comprehension.

Problem is given an input_list = [1, 2, 3, 4, 5] return a list with each element as multiple of all elements till that index starting from left to right.

Hence return list would be [1, 2, 6, 24, 120].

The normal loop I have (and it's working):

l2r = list()
for i in range(lst_len):
    if i == 0:
        l2r.append(lst_num[i])
    else:
        l2r.append(lst_num[i] * l2r[i-1])
Georgy
  • 12,464
  • 7
  • 65
  • 73
ridua
  • 123
  • 1
  • 11

7 Answers7

16

Python 3.8+ solution:

lst = [1, 2, 3, 4, 5]

curr = 1
out = [(curr:=curr*v) for v in lst]
print(out)

Prints:

[1, 2, 6, 24, 120]

Other solution (with itertools.accumulate):

from itertools import accumulate

out = [*accumulate(lst, lambda a, b: a*b)]
print(out)
Andrej Kesely
  • 168,389
  • 15
  • 48
  • 91
  • I'm still a little unfamiliar with walrus and don't have 3.8 running here. Are the `( )` necessary? – Mark Jun 07 '20 at 00:52
  • 1
    @MarkMeyer No, in this case they aren't. I just add them out of habit. – Andrej Kesely Jun 07 '20 at 00:53
  • This is really bad style, you shouldn't be relying on side-effects in a list comprehension. – juanpa.arrivillaga Jun 07 '20 at 01:02
  • @juanpa.arrivillaga I agree with you. Personally, I would choose the `accumulate()` version. It's more clear. – Andrej Kesely Jun 07 '20 at 01:03
  • @juanpa, I'm interested in your assertion that it's bad style. Given that the whole intent of the walrus operator was to allow assignment to a value you could use elsewhere, this seems to be a valid use case. Is there something undefined about the behaviour is this case? If not, I'm not entirely certain what the problem is. – paxdiablo Jun 07 '20 at 03:17
  • 1
    @paxdiablo list comprehensions are functional programming constructs intended to express declarative mapping/filtering operations. This creates a weird amalgam of imperative code in a functional style, and it relies on side-effects. It's confusing and unexpected. If you want to do this, you should just use a for-loop. Just because you *can* use assignment in certain contexts doesn't mean you *should*. – juanpa.arrivillaga Jun 07 '20 at 03:31
  • @paxdiablo Now, the walrus is perfectly acceptable for creating a temporary named variable to avoid re-calculating something, like `[frob for x in something if (frob := x.frobnicate())]` because that is still fundamenally declarative – juanpa.arrivillaga Jun 07 '20 at 03:34
  • @juanpa: I think I'll take the opposite approach (though I may not convince you): if something works *well* and the only downside is mixing declarative and functional aspects, I'll probably do it :-) It seems to me that this "confusing" aspect just needs a little more thought to get over. In fact, I already do this in Qt, mixing QML declarative stuff with Javascript procedural stuff, for maximum functionality with minimal code. It seems things like XAML also easily allow for this, with their arbitrarily complex code-behind stuff, despite the fact XAML is declarative. – paxdiablo Jun 07 '20 at 05:23
  • 1
    @juanpa: But at least you've answered my question as to why you think it's less than optimal. I'm just not sure you've convinced me :-) – paxdiablo Jun 07 '20 at 05:24
4

Well, you could do it like this(a):

import math

orig = [1, 2, 3, 4, 5]
print([math.prod(orig[:pos]) for pos in range(1, len(orig) + 1)])

This generates what you wanted:

[1, 2, 6, 24, 120]

and basically works by running a counter from 1 to the size of the list, at each point working out the product of all terms before that position:

pos   values    prod
===  =========  ====
 1   1             1
 2   1,2           2
 3   1,2,3         6
 4   1,2,3,4      24
 5   1,2,3,4,5   120

(a) Just keep in mind that's less efficient at runtime since it calculates the full product for every single element (rather than caching the most recently obtained product). You can avoid that while still making your code more compact (often the reason for using list comprehensions), with something like:

def listToListOfProds(orig):
    curr = 1
    newList = []
    for item in orig:
        curr *= item
        newList.append(curr)
    return newList

print(listToListOfProds([1, 2, 3, 4, 5]))

That's obviously not a list comprehension but still has the advantages in that it doesn't clutter up your code where you need to calculate it.

People seem to often discount the function solution in Python, simply because the language is so expressive and allows things like list comprehensions to do a lot of work in minimal source code.

But, other than the function itself, this solution has the same advantages of a one-line list comprehension in that it, well, takes up one line :-)

In addition, you're free to change the function whenever you want (if you find a better way in a later Python version, for example), without having to change all the different places in the code that call it.

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
1

This should not be made into a list comprehension if one iteration depends on the state of an earlier one!

If the goal is a one-liner, then there are lots of solutions with @AndrejKesely's itertools.accumulate() being an excellent one (+1). Here's mine that abuses functools.reduce():

from functools import reduce

lst = [1, 2, 3, 4, 5]

print(reduce(lambda x, y: x + [x[-1] * y], lst, [lst.pop(0)]))

But as far as list comprehensions go, @AndrejKesely's assignment-expression-based solution is the wrong thing to do (-1). Here's a more self contained comprehension that doesn't leak into the surrounding scope:

lst = [1, 2, 3, 4, 5]

seq = [a.append(a[-1] * b) or a.pop(0) for a in [[lst.pop(0)]] for b in [*lst, 1]]

print(seq)

But it's still the wrong thing to do! This is based on a similar problem that also got upvoted for the wrong reasons.

cdlane
  • 40,441
  • 5
  • 32
  • 81
  • By unintended leakage, I assume you're talking about the `curr` variable (accumulating the cached multiplication) in Andre's first suggestion. If that's really of a concern, it's solvable with a simple `del curr` after the comprehension). But I wouldn't generally concern myself with that, especially since this is better done in a function where leakage can be prevented quite easily, and most Python developers are very untidy in the way they just leave variables strewn all over the floor :-) – paxdiablo Jun 07 '20 at 05:34
1

A recursive function could help.

input_list = [ 1, 2, 3, 4, 5]

def cumprod(ls, i=None):
    i = len(ls)-1 if i is None else i
    if i == 0:
        return 1
    return ls[i] * cumprod(ls, i-1)

output_list = [cumprod(input_list, i) for i in range(len(input_list))]

output_list has value [1, 2, 6, 24, 120]


This method can be compressed in python3.8 using the walrus operator

input_list = [ 1, 2, 3, 4, 5]

def cumprod_inline(ls, i=None):
    return 1 if (i := len(ls)-1 if i is None else i) == 0 else ls[i] * cumprod_inline(ls, i-1)

output_list = [cumprod_inline(input_list, i) for i in range(len(input_list))]

output_list has value [1, 2, 6, 24, 120]


Because you plan to use this in list comprehension, there's no need to provide a default for the i argument. This removes the need to check if i is None.

input_list = [ 1, 2, 3, 4, 5]

def cumprod_inline_nodefault(ls, i):
    return 1 if i == 0 else ls[i] * cumprod_inline_nodefault(ls, i-1)

output_list = [cumprod_inline_nodefault(input_list, i) for i in range(len(input_list))]

output_list has value [1, 2, 6, 24, 120]


Finally, if you really wanted to keep it to a single , self-contained list comprehension line, you can follow the approach note here to use recursive lambda calls

input_list = [ 1, 2, 3, 4, 5]

output_list = [(lambda func, x, y: func(func,x,y))(lambda func, ls, i: 1 if i == 0 else ls[i] * func(func, ls, i-1),input_list,i) for i in range(len(input_list))]

output_list has value [1, 2, 6, 24, 120]

It's entirely over-engineered, and barely legible, but hey! it works and its just for fun.

Ron
  • 11
  • 2
  • This is good. To be great, you should explain *how*/*why* the code solves the OP's issue. Providing insight helps visitors learn how to apply essential logic to convert any loop into a recursive function or list comprehension. Teaching someone how to think about three problems & code constructs is where real value, & the most upvotes will come from. You're a good writer. Consider editing to highlight essential aspects of the solution. Working code is essential. Knowing how to think about it is invaluable. – SherylHohman Feb 19 '21 at 22:26
0

For your list, it might not be intentional that the numbers are consecutive, starting from 1. But for cases that that pattern is intentional, you can use the built in method, factorial():

from math import factorial

input_list = [1, 2, 3, 4, 5]
l2r = [factorial(i) for i in input_list]

print(l2r)

Output:

[1, 2, 6, 24, 120]
Red
  • 26,798
  • 7
  • 36
  • 58
  • 1
    I think that, if the content of the list was always sequential integers `1..n`, there'd be absolutely no point in *having* the list, you would just provide `n`. – paxdiablo Jun 07 '20 at 01:46
  • @paxdiablo, this is working for non a sequential list as well. Did you check it with a non sequential list? That gives proper answer, even when the list is changed to non sequential. – Srinika Pinnaduwage Jun 07 '20 at 17:19
  • 1
    @Srinika, it may work to give you the *factorial* of numbers in the list, sequential or otherwise, but it does not, as the question requested, give the "multiple of all elements till that index starting from left to right". Example: `[2, 5, 3]` should give `[2, 10, 30]` but this answer gives `[2, 120, 6]`. – paxdiablo Jun 08 '20 at 20:51
0

The package numpy has a number of fast implementations of list comprehensions built into it. To obtain, for example, a cumulative product:

>>> import numpy as np
>>> np.cumprod([1, 2, 3, 4, 5])
array([  1,   2,   6,  24, 120])

The above returns a numpy array. If you are not familiar with numpy, you may prefer to obtain just a normal python list:

>>> list(np.cumprod([1, 2, 3, 4, 5]))
[1, 2, 6, 24, 120]
John1024
  • 109,961
  • 14
  • 137
  • 171
0

using itertools and operators:

from itertools import accumulate
import operator as op

ip_lst = [1,2,3,4,5]
print(list(accumulate(ip_lst, func=op.mul)))