2

Suppose I have a function which I have written as a binary-operator (binop), how do I extend it to a multi-operator (multiop) which takes an arbitrary number of arguments? Is there such a decorator in a library (e.g. in functools)?

For example (I want a decorator to give this behaviour):

@binop_to_multiop
def mult(a,b):
    return a*b

mult(2,3,4) # 2*3*4 = 24
mult(7) # 7
mult(2,3) # 6

Obviously I can't ask a question about decorators without mentioning, this answer.

.

I've tried writing my own, but can't quite get it working, any explanation of where I'm going wrong would also be welcome:

def binop_to_multiop(f):
    @functools.wraps(f)
    def wrapper(*args, **kwds):
        if len(args) == 1: return args[0] # fails
        return f(args[0],(f(*args[1:], **kwds)), **kwds) #recursion attempt fails
    return wrapper

Gives a TypeError: mult() takes exactly 2 arguments (N given) (for various N!=2).

Community
  • 1
  • 1
Andy Hayden
  • 359,921
  • 101
  • 625
  • 535
  • 1
    Your recursion should call `wrapper(*args[1:], **kwds)`, not `f(...)`. – Martijn Pieters Sep 24 '12 at 22:59
  • 1
    One thing: You're creating a multi-parameter _function_, not _operator_. In Python, an operator is a special kind of function called when special syntax is encountered (e.g., `2 * 3`). There's really no way to make a multi-operand version of an operator, but then you usually don't have to, because the obvious syntax (e.g., `2 * 3 * 4 * 5`) already just works. – abarnert Sep 24 '12 at 23:23
  • @abarnert Good nit-pick! :). I was thinking lisp style `(* 2 3 4)` which might make more sense (although [all operators are just functions in lisp](http://en.wikipedia.org/wiki/Operator_(programming)#cite_note-LispOperators-0)). – Andy Hayden Sep 24 '12 at 23:33

3 Answers3

4

reduce() comes to mind:

from functools import wraps

def binop_to_multiop(binop):
    @wraps(binop)
    def multiop(x, *xs):
        return reduce(binop, xs, x)
    return multiop

# ...

@binop_to_multiop
def mult(a, b):
    return a * b

print mult(2, 3, 4)
print mult(7)
print mult(2, 3)

Result:

$ python multiop.py
24
7
6
pillmuncher
  • 10,094
  • 2
  • 35
  • 33
  • Thanks for the answer, I'm a fan of reduce! Does this deal with `**kwds`? – Andy Hayden Sep 24 '12 at 23:14
  • @hayden: No, it does not, but I don't quite see the point, since a binary operator that takes two arguments plus an unspecified number of additional keyword arguments ceases to be a binary operator, does it not? – pillmuncher Sep 24 '12 at 23:18
  • +1 for using `wraps`, unlike the otherwise-equivalent answer (yours was also earlier, and has slightly better names). – abarnert Sep 24 '12 at 23:21
  • hmm, actually mine was earlier before I edited it with "if args else None' part. – swang Sep 25 '12 at 01:08
3

Your attempt to code it yourself was very close to working. You just need to change the recursive step to recurse on wrapper rather passing all but one argument to f:

def binop_to_multiop(f):
    @functools.wraps(f)
    def wrapper(*args, **kwds):
        if len(args) == 1: return args[0]
        return f(args[0], wrapper(*args[1:], **kwds), **kwds)
    return wrapper

I didn't have any problems with the base case, so I'm not sure what your comment #fails was about.

You may also need to think about which end of the list you start solving from (that is, does your operator have left or right associativity). For operators like multiplication and addition it won't matter since (a+b)+c = a+(b+c), but for other you may get strange results. For instance, subtraction may not work as you might expect:

@binop_to_multiop
def sub(a, b):
    return a - b

With the decorator defined above, sub(a, b, c) will give a different result than a-b-c (it will do a-(b-c) instead of (a-b)-c). If you want them to behave the same way, you can redefine the decorator to be left associative (like most mathematical operators do in most computer languages) like this:

def left_associative_binop_to_multiop(f):
    @functools.wraps(f)
    def wrapper(*args, **kwds):
        if len(args) == 1: return args[0]
        return f(wrapper(*args[:-1], **kwds), args[-1], **kwds)
    return wrapper

A more sophisticated approach would be to make the associativity be a parameter to the decorator, but that gets tricky if you don't want the parameter to be required.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • That's the insight I was missing! Thanks. (I thought it "fails" as `mult(4)` gave an error.) – Andy Hayden Sep 24 '12 at 23:05
  • +1. The reduce implementation is probably better, but this implementation is probably easier to understand for the OP, because it's just a fixed version of what he already tried. – abarnert Sep 24 '12 at 23:25
3
def binop_to_multiop(f):
    def wrapper(*args):
        return reduce(f, args) if args else None
    return wrapper

@binop_to_multiop
def mult(a, b):
    return a*b

print mult(2,3,4)
print mult(7)
print mult(2,3)
print mult(4,5,6,7)

gives 24 7 6 840

swang
  • 219
  • 1
  • 3