I wanted to demonstrate the usefulness of decorators in python to some people and failed at a simple example: Consider two functions (for sake of simplicity without arguments) f and g.
One can define their sum f+g as the function that returns f() + g(). Of course adding, subtracting etc. of functions is not defined in general. But it is easy to write a decorator that transforms every function into an addable function.
Now I would like to have a decorator that transforms any function into an “operable” function, that is, a function that behaves in the described way for any operator in the standard module operator. My implementation looks as follows:
import operator
class function(object):
def __init__(self, f):
self.f = f
def __call__(self):
return self.f()
def op_to_function_op(op):
def function_op(self, operand):
def f():
return op(self(), operand())
return function(f)
return function_op
binary_op_names = ['__add__', '__and__', '__div__', '__eq__', '__floordiv__', '__ge__', '__gt__', '__le__', '__lt__', '__mod__', '__mul__', '__ne__', '__or__', '__pow__', '__sub__', '__truediv__', '__xor__']
for name in binary_op_names:
type.__setattr__(function, name, op_to_function_op(getattr(operator, name)))
Let’s perform a little test to see if it works:
@function
def a():
return 4
def b():
return 7
c = a + b
print c()
print c() == operator.__add__(4, 7)
Output:
11
True
This is the final version I got after some experimenting.
Now let’s do two small, irrelevant modifications to have a look what I tried before:
First: In the definition of binary_op_names, change the square brackets to round brackets. Suddenly, a (for me) completely unrelated error message comes out:
Traceback (most recent call last):
File "example.py", line 30, in <module>
c = a + b
TypeError: unsupported operand type(s) for +: 'function' and 'function'
Where does this come from??
Second: Write op_to_function_op as a lambda expression:
op = getattr(operator, name)
type.__setattr__(function, name, lambda self, other: function(lambda: op(self(), other())))
Perform a slightly more involved test case:
@function
def a():
return 4
def b():
return 7
c = a + b
print c()
print c() == operator.__add__(4, 7)
print c() == operator.__xor__(4, 7)
Output:
3
False
True
This looks to me like scope leakage, but again I don’t understand why this happens.
For the first issue, I didn’t see any problems when changing
binary_op_namesfrom alistto atuple, not sure why you were seeing that.As for the second issue, all operations will perform an XOR because
__xor__was the last item thatopwas set to. Becauseopis not passed into the lambda when it is created, each time the lambda is called it will look foropin a global scope and always see__xor__.You can prevent this by creating a lambda that acts as a closure, similar to your current
op_tofunction_op, it might end up looking something like this: