Questions about CPython's behavior on addition operator -- binary_op1 (abstract.c) and nb_add (typeobject.c)

Hi all, I'm looking at CPython's behavior when an addition is called. From what I understand, binary_op1 <https://github.com/python/cpython/blob/3.8/Objects/abstract.c#L786> is eventually called, and it calls either slotv or slotw, which seems to be the binaryfunc defined as nb_add in the field tp_as_number of respectively v / w. I have a few questions: 1) In the default case, tp_as_number->nb_add is defined by the function slot_nb_add <https://github.com/python/cpython/blob/3.8/Objects/typeobject.c#L6312> itself stemming from the macro expansion SLOT1BINFULL <https://github.com/python/cpython/blob/3.8/Objects/typeobject.c#L6140> defined in typeobject.c. Both binary_op1(v, w) and slot_nb_add(v, w) appear to perform similar checks (if their second argument is a subtype of the first, etc), to decide if v's add or w's reverse add must be called and in which order. I find this repetition weird, and I guess I'm missing something... Any ideas? 2) From the SLOT1BINFULL macro, both __add__ and __radd__ are defined by the slot_nb_add function (with some argument swapping done by wrap_binaryfunc_l / wrap_binaryfunc_r). If I want to define a different behavior for the reverse operator during a definition with a PyTypeObject, I guess I should add an "__radd__" method? 3) If I create a user-defined class A, having different methods __add__ and __radd__, these methods are added in A's dictionary. From what I understand, the function update_one_slot <https://github.com/python/cpython/blob/3.8/Objects/typeobject.c#L7209> is then called to change A's tp_as_number->nb_add to point to the methods defined by __add__ and __radd__? From the code documentation, I think that "a wrapper for the special methods is installed". Where exactly is this wrapper applied, and how does it know when to dispatch to __add__ or __radd__? Kind regards, Raphaël

On 16/01/2020 11:27, Raphaël Monat wrote:
Hi all,
I'm looking at CPython's behavior when an addition is called. From what I understand, binary_op1 <https://github.com/python/cpython/blob/3.8/Objects/abstract.c#L786> is eventually called, and it calls either slotv or slotw, which seems to be the binaryfunc defined as nb_add in the field tp_as_number of respectively v / w.
I'm also keen to understand this and think I can elucidate a little, from my study of the code. The perspective of someone who *didn't* write it might help, but I defer to the authors' version when it appears. A crucial observation is that there is only one nb_add slot in a type definition. Think about adding a int(1) + float(2). Where it lands in long_add (v->ob_type->tp_as_number->nb_add) will return NotImplemented, because it does not understand the float right-hand argument, but float_add (w->ob_type->tp_as_number->nb_add) is able to give an answer, since it can float the int, behaving as float.__radd__(f, i). So the slots have to implement both __add__ and __radd__.
I have a few questions: 1) In the default case, tp_as_number->nb_add is defined by the function slot_nb_add <https://github.com/python/cpython/blob/3.8/Objects/typeobject.c#L6312> itself stemming from the macro expansion SLOT1BINFULL <https://github.com/python/cpython/blob/3.8/Objects/typeobject.c#L6140> defined in typeobject.c. Both binary_op1(v, w) and slot_nb_add(v, w) appear to perform similar checks (if their second argument is a subtype of the first, etc), to decide if v's add or w's reverse add must be called and in which order. I find this repetition weird, and I guess I'm missing something... Any ideas?
The logic is the same as binary_op1, but the function has to deal with the possibility that one or other type may already provide a special function wrapper function in its nb_add slot, which is what the test tp_as_number->SLOTNAME == TESTFUNC is about. TESTFUNC is nearly always the same as FUNCNAME. Then it also deals with quite a complex decision in method_is_overloaded(). The partial repetition of the logic, which I think is now nested (because binary_op1() may have called slot_nb_add) is necessary to insert the more complex version into the decision tree. But this is roughly where my ability to visualise the paths runs out.
2) From the SLOT1BINFULL macro, both __add__ and __radd__ are defined by the slot_nb_add function (with some argument swapping done by wrap_binaryfunc_l / wrap_binaryfunc_r). If I want to define a different behavior for the reverse operator during a definition with a PyTypeObject, I guess I should add an "__radd__" method?
I would not say they are "defined" by the slot_nb_add function. Rather, if one or other has been defined (in Python), v.__add__ or w.__radd__ is called by the single function slot_nb_add. They cannot be called directly from C, but call_maybe() supplies the mechanism. A confusing factor is that for types defined in C and filling the nb_add slot, the slot function (float_add, or whatever) has to be wrapped by two descriptors that can then sit in the type's dictionary as "__add__" and "__radd__". In that case these *are* defined by a wrapped C function, but that function is the function in the implementation of the type (float_add, say), not slot_nb_add. There are two kinds of wrapper: one used "Python-side out", so Python-calling "__add__" leads to nb_add's behaviour, and one "C-side out" so C-calling via nb_add leads to "__add__".
3) If I create a user-defined class A, having different methods __add__ and __radd__, these methods are added in A's dictionary. From what I understand, the function update_one_slot <https://github.com/python/cpython/blob/3.8/Objects/typeobject.c#L7209> is then called to change A's tp_as_number->nb_add to point to the methods defined by __add__ and __radd__? From the code documentation, I think that "a wrapper for the special methods is installed". Where exactly is this wrapper applied, and how does it know when to dispatch to __add__ or __radd__?
I *think* it is changed to contain slot_nb_add as defined by the macro. I hope that is somewhere near accurate. Jeff Allen
participants (2)
-
Jeff Allen
-
Raphaël Monat