I am trying to overload several operators at once using the __getattr__ function. In my code, if I call foo.__add__(other) it works as expected, but when I try foo + bar, it does not. Here is a minimal example:
class Foo():
def add(self, other):
return 1 + other
def sub(self, other):
return 1 - other
def __getattr__(self, name):
stripped = name.strip('_')
if stripped in {'sub', 'add'}:
return getattr(self, stripped)
else:
return
if __name__=='__main__':
bar = Foo()
print(bar.__add__(1)) # works
print(bar + 1) # doesn't work
I realize that it would be easier in this example to just define __add__ and __sub__, but that is not an option in my case.
Also, as a small side question, if I replace the line:
if stripped in {'sub', 'add'}:
with
if hasattr(self, name):
the code works, but then my iPython kernel crashes. Why does this happen and how could I prevent it?
This is happening because python operators use an optimization to look up the function implementing the operator. The following lines are roughly equivalent:
foo + 1
type(foo).__add__(foo, 1)
Operators are found specifically on the class object only, never on the instance.
bar.__add__(1) calls __getattr__ to find the missing attribute on bar. This works because it bypasses normal operator lookup procedures.
bar + 1 calls Foo.__add__(bar, 1) followed by int.__radd(1, bar). The first attribute lookup fails, and the second option raises TypeError.
Also, as a small side question, if I replace the line: [...]
hasattr calls __getattr__ under the hood. Which explains what you saw when doing if hasattr(self, name):, you actually enter an infinite recursion since you have overwritten __getattr__.
See for yourself
class O:
def __getattr__(self, attr):
print('yooo')
return super().__getattr__(attr)
# self.__getattr__(attr) -> RecursionError
>>> hasattr(O(), 'ya')
yooo
False
I've found a semi-satisfying work-around that does not involve meta-classes:
class Foo():
def add(self, other):
return 1 + other
def sub(self, other):
return 1 - other
# Add operators to the type object
def get_operator(name):
return getattr(Foo, name.strip('_'))
for op in ['__add__', '__sub__']:
setattr(Foo, op, get_operator(op))
if __name__=='__main__':
bar = Foo()
print(bar + 2)
print(bar - 2)