1

I am trying to create a custom object that passes all non-existent method calls down to a member attribute. This works under normal custom method invocations, but fails when attempting to call arithmetic operators.

Below is a console snippet of an example class, a test function, and a cleaned up disassembly of the test function.

>>> class NoAdd(object):
...    member = 0
...    def __getattr__(self, item):
...        print('NoAdd __getattr__')
...        # return getattr(self.member, item)
...        if item == '__add__':
...            return self.member.__add__
>>> def test():
...    print('Call __add__ directly.')
...    NoAdd().__add__(5)  # 5
...    print('Implicit __add__.')
...    NoAdd() + 5  # TypeError
>>> dis(test)
  3           8 LOAD_GLOBAL              1 (NoAdd)
             10 CALL_FUNCTION            0
             12 LOAD_ATTR                2 (__add__)
             14 LOAD_CONST               2 (5)
             16 CALL_FUNCTION            1
             18 POP_TOP
  5          28 LOAD_GLOBAL              1 (NoAdd)
             30 CALL_FUNCTION            0
             32 LOAD_CONST               2 (5)
             34 BINARY_ADD
             36 POP_TOP
             38 LOAD_CONST               0 (None)
             40 RETURN_VALUE
>>> test()
Call __add__ directly.
NoAdd __getattr__
Implicit __add__.
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 5, in test
TypeError: unsupported operand type(s) for +: 'NoAdd' and 'int'

I thought that the Python interpreter would look for the __add__ method using the standard procedure of invoking __getattr__ when the method was not found in the object's method list, before looking for __radd__ in the int. This is apparently not what is happening.

Can someone help me out by explaining what is going on or helping me find out where in the Python source code I can find what BINARY_ADD is doing? I'm not sure if I can fix this, but a workaround would be appreciated.

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
Yos233
  • 2,396
  • 2
  • 15
  • 15
  • If you really need a transparent object proxy where only override certain methods, with the rest forwarded on to the wrapped object, use the ``wrapt`` package. http://wrapt.readthedocs.io/en/latest/wrappers.html – Graham Dumpleton Mar 19 '18 at 06:21
  • Dunder methods like \_\_add__ are only looked up in the class, never the instance through an operator. You need to define \_\_getattr__ on the proxy's metaclass if you want this to work. – Mad Physicist Mar 20 '18 at 02:26
  • This is an even better dupe, but I wanted to make the longest possible chain: https://stackoverflow.com/q/38133096/2988730 – Mad Physicist Mar 20 '18 at 02:33

3 Answers3

4

The methods __getattr__ and __getattribute__ are only used for recovering attributes when you call then explicitly, by example when you do foo.bar. They are not used for implicit invocation.

This behaviour is specified in the documentation

Note: This method may still be bypassed when looking up special methods as the result of implicit invocation via language syntax or built-in functions.

The reason for such an implementation is explained here.

Bypassing the __getattribute__() machinery in this fashion provides significant scope for speed optimisations within the interpreter, at the cost of some flexibility in the handling of special methods

In conclusion what you are trying to do, i.e. using __getattr__ to redirect implicit special method calls, has been voluntarily sacrificed in favor of speed.

Inheritance

The behaviour you are describing can be achieved by class inheritance. Although, passing in arguments to your class constructor will require the following fidling with the __new__ method.

class NoAdd(int):

    def __new__(cls, x, *args, **kwargs):
        return super().__new__(cls, x)

    def __init__(self, x, *args, **kwargs):
        ...

x = NoAdd(0)

x + 5 # 5
x * 2 # 0

Metaclass

Now, suppose you really need to catch implicit call to special methods. I see very little case where this could be useful, but it is a fun exercise. In this case we can rely on metaclass to fill in missing methods with the ones from member.

class ProxyToMember(type):

    def __init__(cls, name, bases, name_space):
        super().__init__(name, bases, name_space)

        if hasattr(cls, 'member'):
            proxy_attrs = (attr for attr in dir(cls.member) if not hasattr(cls, attr))

            def make_proxy(attr):

                attr_value = getattr(cls.member, attr)

                def proxy(_, *args, **kwargs):
                    return attr_value(*args, **kwargs)

                if callable(attr_value):
                    return proxy
                else:
                    return property(lambda _: getattr(cls.member, attr))

            for attr in proxy_attrs:
                setattr(cls, attr, make_proxy(attr))

class A(metaclass=ProxyToMember):
    member = 0

class B(metaclass=ProxyToMember):
    member = 'foo'

A() + 1 # 1
B().startswith('f') # True
Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
  • Your explanation is *almost* correct – Mad Physicist Mar 20 '18 at 02:27
  • The specific optimization here is that when you do `a + b`, it always calls `type(a).__add__(a, b)` instead of `a.__getattribute__('__add__')(b)`. The first one technically should call `type(type(a)).__getattribute__(type(a), '__add__')`, so you can make the proxy work by messing with the metaclass. – Mad Physicist Mar 20 '18 at 02:37
  • @MadPhysicist I'll go read about this sublety. When you say almost correct, do you mean that something is wrong or that I missed that detail? – Olivier Melançon Mar 20 '18 at 03:28
  • The explanation you provide shows that the bypass is documented, but does not really explain why it happens, so it is not of much value. I am sure that at least one of the answers to the dupes I voted with has a link to the docs that explain the specific case I refer to above. – Mad Physicist Mar 20 '18 at 03:58
  • FWIW, your section on inheritance is a much nicer way to go about doing this than metaclasses, but technically does not provide a true proxy. – Mad Physicist Mar 20 '18 at 03:59
  • @MadPhysicist I see, I never knew there actually was a way to catch the implicit calls of magic methods. I'll read the dups you found, thanks! – Olivier Melançon Mar 20 '18 at 04:52
  • I'm still trying to see if there is a way to make this work though. Even if we were to make `NoAdd() + 1` work with metaclass, I don't see how `1 + NoAdd()` can work since we are then calling `int.__add__`. I really do not think OP has the good approach. – Olivier Melançon Mar 20 '18 at 05:11
  • 1
    That actually works out of the box. `int.__add__` will raise a `NotImplementedError` since it does not recognize the type and addition will be deferred to your custom type. I'll find you the doc momentarily. If you think about how adding a numpy array to a scalar works, you will see that this is quite intuitive... – Mad Physicist Mar 20 '18 at 05:13
  • Correct, I just figured it out. No need to find the doc, it just slipped out of my mind that `__add__` would return `NotImplemented` (and not raise `NotImplementedError` if I am correct). – Olivier Melançon Mar 20 '18 at 05:13
  • https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types. Basically, yes, exactly. Then is calls `NoAdd().__radd__(1)` if possible, and defaults to `NoAdd().__add__(1)` if not. – Mad Physicist Mar 20 '18 at 05:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/167134/discussion-between-olivier-melancon-and-mad-physicist). – Olivier Melançon Mar 20 '18 at 05:23
-1

I don't understand you.

Why you can't do something like:

class MyInt(int):
    pass

def test():
    print(MyInt(5) + 5)

test()
robinCTS
  • 5,746
  • 14
  • 30
  • 37
Zheka Koval
  • 525
  • 4
  • 10
-1

After some more intense research (and following some initially unlikely trails) I found my answer. My thought process didn't come up with the same search keywords, which is why I didn't find these before. My question could probably be considered a duplicate of one of the ones linked below.

I found the simplest/best explanation for why this happens here. I found some good references for resolving this here and here. The links in Oliver's answer are also helpful.

In summary, Python does not use the standard method lookup process for the magic methods such as __add__ and __str__. It looks like the workaround is to make a Wrapper class from here.

Yos233
  • 2,396
  • 2
  • 15
  • 15
  • You can just vote to close your own question. As it stands, your "answer" is link-only at best. It has no content of its own. – Mad Physicist Mar 20 '18 at 04:54
  • As you should be able to see, I have already done that, and marked my question as a duplicate. My answer is to provide closure to this question. – Yos233 Mar 20 '18 at 04:59
  • It looks like you flagged your own answer instead of clicking on the "anwers my question" link that appears in the yellow box. – Mad Physicist Mar 20 '18 at 05:10