Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Know if + or __add__ called on an object

Tags:

python

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?

like image 374
cjrieds Avatar asked Apr 11 '16 19:04

cjrieds


2 Answers

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.

like image 190
Dietrich Epp Avatar answered Sep 28 '22 00:09

Dietrich Epp


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        
like image 27
wim Avatar answered Sep 28 '22 02:09

wim