one-liner reduce in Python3

2019-04-13 00:38发布

问题:

In Python3, I am looking for a way to compute in one line a lambda function called on elements two by two. Let’s say I want to compute the LCM of a list of integers, this can be done in one line in Python2:

print reduce(lambda a,b: a * b // gcd(a, b), mylist)

Is it possible to do the same in one line Python3 (implied, without functools.reduce)?

In Python3 I know that filter, map and reduce are gone. I don’t feel I need filter and map anymore because they can be written in Python3 in a shorter and more clear fashion but I thought I could find a nice replacement for reduce as well, except I haven’t found any. I have seen many articles that suggest to use functools.reduce or to “write out the accumulation loop explicitly” but I’d like to do it without importing functools and in one line.

If it makes it any easier, I should mention I use functions that are both associative and commutative. For instance with a function f on the list [1,2,3,4], the result will be good if it either computes:

  • f(1,f(2,f(3,4)))
  • f(f(1,2),f(3,4))
  • f(f(3,f(1,4)),2)
  • or any other order

回答1:

So I actually did come up with something. I do not guarantee the performance though, but it is a one-liner using exclusively lambda functions - nothing from functools or itertools, not even a single loop.

my_reduce = lambda l, f: (lambda u, a: u(u, a))((lambda v, m: None if len(m) == 0 else (m[0] if len(m) == 1 else v(v, [f(m[0], m[1])] + m[2:]))), l)

This is somewhat unreadable, so here it is expanded:

my_reduce = lambda l, f: (
    lambda u, a: u(u, a)) (
        (lambda v, m: None if len(m) == 0
                           else (m[0] if len(m) == 1
                                      else v(v, [f(m[0], m[1])] + m[2:])
                                )
        ),
        l
    )

Test:

>>> f = lambda a,b: a+b
>>> my_reduce([1, 2, 3, 4], f)
10
>>> my_reduce(['a', 'b', 'c', 'd'], f)
'abcd'

Please check this other post for a deeper explanation of how this works.

The principle if to emulate a recursive function, by using a lambda function whose first parameter is a function, and will be itself.

This recursive function is embedded inside of a function that effectively triggers the recursive calling: lambda u, a: u(u, a).

Finally, everything is wrapped in a function whose parameters are a list and a binary function.


Using my_reduce with your code:

my_reduce(mylist, lambda a,b: a * b // gcd(a, b))


回答2:

Assuming you have a sequence that is at least one item long you can simply define reduce recursivly like this:

def reduce(func, seq): return seq[0] if len(seq) == 1 else func(reduce(func, seq[:-1]), seq[-1])

The long version would be slightly more readable:

def reduce(func, seq):
    if len(seq) == 1:
        return seq[0]
    else:
        return func(reduce(func, seq[:-1]), seq[-1])

However that's recursive and python isn't very good at recursive calls (meaning slow and the recursion limit prevents prosessing sequences longer than 300 items). A much faster implementation would be:

def reduce(func, seq):
    tmp = seq[0]
    for item in seq[1:]:
        tmp = func(tmp, item)
    return tmp

But because of the loop it can't be put in one-line. It could be solved using side-effects:

def reduce(func, seq): d = {}; [d.__setitem__('last', func(d['last'], i)) if 'last' in d else d.__setitem__('last', i) for i in seq]; return d['last']

or:

def reduce(func, seq): d = {'last': seq[0]}; [d.__setitem__('last', func(d['last'], i)) for i in seq[1:]]; return d['last']

Which is the equivalent of:

def reduce(func, seq): 
    d = {} 
    for item in seq:
        if 'last' in d:
            d['last'] = func(d['last'], item)
        else:
            d['last'] = item
    return d['last']  # or "d.get('last', 0)"

That should be faster but it's not exactly pythonic because the list-comprehension in the one-line implementation is just used because of the side-effects.