5

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?

keepAlive
  • 6,369
  • 5
  • 24
  • 39
drmaettu
  • 71
  • 4
  • I'm pretty sure, but cannot prove at the moment, that the discrepancy between `bar.__add__(1)` and `bar + 1` is because the latter behavior invokes the behavior in the method `__add__()` directly, without actually accessing it the usual way. I think this is something that the interpreter does for _most_ builtins - at least in cpython, other implementations might do differently. – Green Cloak Guy Oct 14 '21 at 17:37
  • 1
    "if I replace the line ... the code works, but then my iPython kernel crashes" - that replacement will not make anything work. You must have misinterpreted what you saw. – user2357112 Oct 14 '21 at 17:37
  • 3
    Also, Python bypasses normal attribute lookup for operations like this, so no possible `__getattr__` implementation will achieve your goal. Just implement `__add__` and `__sub__`. – user2357112 Oct 14 '21 at 17:38
  • 1
    I also think (but can't prove) that vital operations like addition/subtraction circumvent the `__getattr__` machinery, so your `__getattr__` isn't being called by `bar + 1` – ForceBru Oct 14 '21 at 17:39
  • The reason why I don't want to implement `__add__` and `__sub__` is that I want to do this for a whole bunch of operators and I would like to avoid the redundant code. But I guess that this is the way to go... – drmaettu Oct 14 '21 at 18:21
  • Also to clarify the line "if I replace the line ... the code works, but then my iPython kernel crashes", I thought the code ran the same way as for `if stripped in {'sub', 'add'}:`, but didn't notice the infinite recursion it was in. – drmaettu Oct 14 '21 at 18:25
  • Is `1`-instead-of-`self` the trick you want to generalize for all transformation ? Which group's properties would you like your object to exhibit ? e.g. do you want your transformations to be commutative ? These are the questions that come to my mind when reading your post. Your wish to avoid redundant code makes indeed total sense. – keepAlive Oct 17 '21 at 09:29
  • The example I show here is just a toy example. My actual problem is that I want to write a wrapper class W to the class C. If W doesn't implement a method, the method of C should be used but the result converted to W if possible. I got this to work for non-magic methods with `__getattr__`, but would like it to work on operators as well. But in the mean-time I've stumbled across [this](https://stackoverflow.com/questions/9057669/how-can-i-intercept-calls-to-pythons-magic-methods-in-new-style-classes/9059858#9059858) which should solve my problems :) – drmaettu Oct 18 '21 at 10:54

3 Answers3

7

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.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • Thanks, I wasn't aware of this difference. Do you see a different way to go around this? – drmaettu Oct 14 '21 at 18:23
  • 3
    @drmaettu. You could try playing games with your metaclass. That would let you charge the way lookup works on the class object maybe. Depends on what you're really trying to do. I don't see the point of what you're trying to accomplish here, given that python already has a pretty solid inheritance mechanism. – Mad Physicist Oct 14 '21 at 18:32
  • Thanks for the tip, here's probably how to go about this (for reference): https://stackoverflow.com/questions/9057669/how-can-i-intercept-calls-to-pythons-magic-methods-in-new-style-classes/9059858#9059858 – drmaettu Oct 18 '21 at 10:47
  • 1
    @drmaettu. I'm glad you were able to find that. Hopefully it provides the workaround you need, or convinces you not to do it. – Mad Physicist Oct 18 '21 at 12:53
4

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
keepAlive
  • 6,369
  • 5
  • 24
  • 39
0

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)
drmaettu
  • 71
  • 4