7

In Python, I can overload an object's __add__ method (or other double-underscore aka "dunder" methods). This allows me to define custom behavior for my objects when using Python operators.

Is it possible to know, from within the dunder method, if the method was called via + or via __add__?

For example, suppose I want to create an object which prints "+" or "__add__" depending on if + was used or if __add__ was called directly.

class MyAdder(object):
    def __add__(self, other):
        print method_how_created()
        return 0


MyAdder() + 7
# prints "+", returns 0

MyAdder().__add__(7)
# prints "__add__", returns 0

Barring the existence of some magic like method_how_created, is there a canonical mapping of symbols to dunder methods? I know there are lists, such as http://www.python-course.eu/python3_magic_methods.php or solutions based on parsing the docstrings of the operator module, as mentioned here: Access operator functions by symbol. Is there a way to find a map between function names and symbols that is a little less hacky than parsing the docstrings or manually creating a list?

Community
  • 1
  • 1
cjrieds
  • 827
  • 8
  • 13
  • 8
    Why would you ever want something like this? – wnnmaw Apr 11 '16 at 19:47
  • 1
    Please correct me if I'm wrong, but doesn't `+` just call `__add__`? – Aaron Christiansen Apr 11 '16 at 19:47
  • 1
    @OrangeFlash81 it does. And for `NotImplemented` calls `__radd__` – Bharel Apr 11 '16 at 19:49
  • `7 + MyAdder()` would return `TypeError`, because `int.__add__` is attempted and `MyAddr.__radd__` is not there. – wim Apr 11 '16 at 19:50
  • `+` does just call `_add_`. But you could still choose to call one or the other. – Jacob Apr 11 '16 at 19:50
  • 1
    If you want to know all special methods, just look into the documentation: https://docs.python.org/2/reference/datamodel.html – Daniel Apr 11 '16 at 19:50
  • 2
    @OrangeFlash81 Technically, `+` maps directly to the `BINARY_ADD` opcode, but the name `__add__` has to be resolved to a function to be called. – chepner Apr 11 '16 at 20:14
  • @wim whoops, I originally had `__radd__`, decided it didn't add anything to the question and just made it more verbose, but then didn't remove `7 + MyAdder()`. Edited to remove that. – cjrieds Apr 12 '16 at 04:15
  • @wnnmaw I'm working on a project (https://github.com/dodger487/dplython/) that involves packaging computation for later use. All this fuss is just to make a nice string method for the object that packages the computation. – cjrieds Apr 12 '16 at 04:29

2 Answers2

5

Yes, but you probably don't want to do this, because it's a bad idea. You have to inspect the interpreter stack. For obvious reasons, this will not work if your code is called from C. Here's the code:

import inspect
import dis

class A(object):
    def __add__(self, other):
        fr = inspect.getouterframes(
            inspect.currentframe(), 1)[1][0]
        opcode = fr.f_code.co_code[fr.f_lasti]
        # opcode = ord(opcode) # Uncomment for Python 2
        is_op = opcode == dis.opmap['BINARY_ADD']
        if is_op:
            print('Called with +')
        else:
            print('Called with __add__')

A() + 1
A().__add__(1)

This is tested and works on Python 3, and requires only slight modifications for Python 2.

Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
  • Could you elaborate on why this would be a bad idea? Is it because this wouldn't work in non-C python implementations, like Jython or PyPy? Is it always a bad idea to inspect the interpreter stack, and if so, why? – cjrieds Apr 12 '16 at 04:26
  • 1
    This would be a bad idea because (1) it creates a function which behaves differently depending on how it's called, which is unusual and surprising, and because (2) the `__add__()` method is supposed to be the same as `+`, and making it behave differently is just going to confuse and anger people. That's right, someone using this code might get angry *at you* for creating such a monster. – Dietrich Epp Apr 12 '16 at 04:28
  • Additional question-- my apologies but it is not obvious to me why this won't work from C. Could you explain, or point me to a link with an explanation? – cjrieds Apr 12 '16 at 04:35
  • In C, there is no such thing as `__add__` and you cannot override `+`, it is a completely different language. – Dietrich Epp Apr 12 '16 at 04:38
  • 1
    A third reason this may, by some, in certain here-to-fore undefined circumstances, be flagged as a "bad idea" is that if you have to dig into the interpreter stack for correct execution, you are now dependent upon the implementation details. While these may be the same across different implementations, you are essentially dependent upon token type names from the lex phase of (i.e., the very first phase!!!) of compilation. In any reasonable compilation model this should not matter. I should be able, in my compiler, to call '+' a BINADD or BINARY_ADD or BOB or... This shouldn't affect execution. – Ben Kushigian Feb 02 '17 at 11:42
4

In cpython, you have some introspection ability by disassembling the referrer frame. For example:

import dis
import inspect

def method_how_created():
    return dis.dis(inspect.currentframe().f_back.f_back.f_code)

class MyAdder(object):
    def __add__(self, other):
        print method_how_created()

x = MyAdder()

Checking the case x + 7, you will see something like this in the disassembly:

         64 LOAD_NAME                5 (x)
         67 LOAD_CONST               5 (7)
         70 BINARY_ADD          
         71 POP_TOP             
         72 LOAD_CONST               1 (None)
         75 RETURN_VALUE        

Using the magic method x.__add__(7), you will see something like this in the disassembly:

         64 LOAD_NAME                5 (x)
         67 LOAD_ATTR                6 (__add__)
         70 LOAD_CONST               5 (7)
         73 CALL_FUNCTION            1
         76 POP_TOP             
         77 LOAD_CONST               1 (None)
         80 RETURN_VALUE        
wim
  • 338,267
  • 99
  • 616
  • 750