2

I am confused by the behavior of the reduce function.

  • In the first example, I get the expected result: (1-1/2) * (1-1/3) = 1/3

    >>> reduce(lambda x, y: (1 - 1.0/x) * (1 - 1.0/y), [2,3])
    0.33333333333333337
    
  • In the second example, I do not get the expected result: (1-1/2) * (1-1/3) * (1-1/5) = 0.2666666

    >>> reduce(lambda x, y: (1 - 1.0/x) * (1 - 1.0/y), [2,3,5])
    -1.5999999999999996
    

Can someone please explain me what I am missing?

MSeifert
  • 145,886
  • 38
  • 333
  • 352
akasolace
  • 572
  • 1
  • 5
  • 17

4 Answers4

11

What you need is a map and reduce:

>>> from functools import reduce
>>> yourlist = [2, 3]
>>> reduce(lambda x, y: x*y, map(lambda x: (1-1/x), yourlist))
0.33333333333333337

>>> yourlist = [2, 3, 5]
>>> reduce(lambda x, y: x*y, map(lambda x: (1-1/x), yourlist))
0.2666666666666667

Because map converts each item to the (1-1/item) and then the reduce multiplies all of them.

Additional remarks:

Instead of the lambda x, y: x * y you could also use the faster operator.mul, for example:

>>> import operator
>>> yourlist = [2, 3, 5]
>>> reduce(operator.mul, map(lambda x: (1-1/x), yourlist))
0.2666666666666667

Thanks @acushner for pointing this out (in the comments).

What went wrong in your function

In this case it's actually quite easy to see what doesn't work, just use a named function and add some prints:

def myfunc(x, y):
    print('x = {}'.format(x))
    print('y = {}'.format(y))
    res = (1 - 1.0/x) * (1 - 1.0/y)
    print('res = {}'.format(res))
    return res

reduce(myfunc, [2, 3])
# x = 2
# y = 3
# res = 0.33333333333333337

reduce(myfunc, [2, 3, 5])
# x = 2
# y = 3
# res = 0.33333333333333337
# x = 0.33333333333333337
# y = 5
# res = -1.5999999999999996

So it uses the last result as "next" x value. That's why it worked for the length-2-list case but for more elements it simply doesn't apply the formula you want.

Alternative

Instead of using map and reduce you could also use a simple for-loop. It's much easier to get them right and most of the times they are more readable (and in some cases it's faster than reduce).

prod = 1
for item in yourlist:
    prod *= 1 - 1 / item
print(prod)

Yes, instead of 1 line it's now 4 lines long but it's easy to understand what is happening (but there might be some edge cases in which that doesn't behave like reduce, for example for empty inputs).

But I generally prefer simple loops over complicated reduce-operations but as always YMMV. :)

MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • 2
    exactly. you could use `operator.mul` here as well – acushner Jun 12 '17 at 19:24
  • excellent thank you !! I now understand my mistake ... I need to wait 5 minutes before I can approve this as the answer ! – akasolace Jun 12 '17 at 19:25
  • 1
    @acushner Thanks for the suggestion. I added it to the answer. – MSeifert Jun 12 '17 at 20:07
  • `operator.mul` won't actually be any faster; it's just defined and available for import from the standard library. – chepner Jun 12 '17 at 20:08
  • @chepner Are you sure? I thought passing C functions (like `operator.mul`) to other C functions (like `reduce`) would result in less function-call overhead and thus better performance. – MSeifert Jun 12 '17 at 20:10
  • `operator.mul` isn't a C function; it is simply defined as `def mul(a, b): return a * b`. – chepner Jun 12 '17 at 20:14
  • (That said, I'm having trouble defining any tests to accurately measure the difference between the two, so I should adjust my claim to be that I wouldn't *expect* `operator.mul` to be any faster.) – chepner Jun 12 '17 at 20:16
  • @chepner Ah, you're talking about CPython without compiled `_operator` module? Normally the `_operator` module is compiled and then the function you mentioned is [replaced](https://github.com/python/cpython/blob/v3.6.1/Lib/operator.py#L412) with the [C function from the `_operator` module](https://github.com/python/cpython/blob/v3.6.1/Modules/_operator.c#L71). – MSeifert Jun 12 '17 at 20:18
  • 1
    @MSeifert Thank you! I've been quite disappointed since I first looked at `operator.py` a few months ago, since I always thought it was exposing built-in functions. I never saw the `import _operator` at the bottom, expecting to have seen it near the top instead. (Having it at the bottom makes sense, now that I give it a moment's thought.) – chepner Jun 12 '17 at 20:22
2

EDIT: I approve map/reduce answer above.

To understand why, read this:

https://docs.python.org/2/library/functions.html#reduce

Reduce recursively calls your function on each element of your list with 2 arguments: an accumulator (value of last call) and current element.

So you get:

(1 - 1.0/( (1 - 1.0/2) * (1 - 1.0/3) )) * (1 - 1.0/5)

With:

reduce(lambda acc, x: (1 - 1.0/acc) * (1 - 1.0/x), [2,3,5])
>>-1.5999999999999996
RaphaMex
  • 2,781
  • 1
  • 14
  • 30
2

To add to why you get -1.5999999999999996 as your result and for completeness we can compute it using https://docs.python.org/2/library/functions.html#reduce as our guide:

The first iteration will be (which takes our first 2 iterator values of 2 and 3 as x and y):

(1 - 1.0 / 2) * (1 - 1.0 / 3)

which becomes:

0.5 * 0.6666666666666667

which yields:

0.33333333333333337.

We then use 0.33333333333333337 to move on to our next iteration which takes this result as x and our next iteration number of 5 as y:

Therefore, our second iteration will be:

(1 - 1.0 / 0.33333333333333337) * (1 - 1.0/5)

which becomes:

-1.9999999999999996 * 0.8

which yields:

-1.5999999999999996

Peter Featherstone
  • 7,835
  • 4
  • 32
  • 64
-1

In your second example you have 3 inputs. You need:

reduce(lambda x, y: (1 - 1.0/x) * (1 - 1.0/y)* (1 - 1.0/z),...
Peter Featherstone
  • 7,835
  • 4
  • 32
  • 64
alexjones
  • 86
  • 1
  • 9
  • Why? The python docs page has 5 inputs, where did `z` come from? https://docs.python.org/2/library/functions.html#reduce - The left argument, x, is the accumulated value and the right argument, y, is the update value from the iterable – Peter Featherstone Jun 12 '17 at 19:23