Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

__ipow__ raising TypeError when left-hand-side object returns NotImplemented

If I have two objects A and B, I can return NotImplemented for A's __iadd__ method and have B modify A using it's __radd__ method.

>>> class A():
...     def __init__(self, val):
...         self.val = val
...     def __iadd__(self, other):
...         return NotImplemented
...     def __ipow__(self, other):
...         return NotImplemented 
... 
>>> class B():
...     def __init__(self, val):
...         self.val = val
...     def __radd__(self, other):
...         return A(other.val + self.val)
...     def __rpow__(self, other):
...         return A(other.val ** self.val) 
... 
>>> a = A(2)
>>> b = B(2)
>>> a += b
>>> a.val
4

This seems to work for all inplace operators with the exception of __ipow__ where a TypeError is raised.

>>> a **= b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for ** or pow(): 'A' and 'B'

Why is the behavior different here? Is this failing because pow() requires numeric data? What is the best workaround for this?

like image 818
dshanahan Avatar asked Apr 17 '26 07:04

dshanahan


1 Answers

Python 3.10+

This bug is fixed in Python 3.10

Due to a bug in the dispatching mechanism for **=, a class that defines __ipow__() but returns NotImplemented would fail to fall back to x.__pow__(y) and y.__rpow__(x). This bug is fixed in Python 3.10.

The snippet now works as expected:

>>> a = A(2)
>>> b = B(2)
>>> a **= b
>>> a.val
4

This looks like a bug due to an inconsistency between the code for binary and ternary operations (with **= being handled by the ternary operation logic due to sharing code with 3-argument pow). Binary in-place operations go through binary_iop1, which has code to fall back to the non-in-place routine if the in-place handler returns NotImplemented:

static PyObject *
binary_iop1(PyObject *v, PyObject *w, const int iop_slot, const int op_slot)
{
    PyNumberMethods *mv = v->ob_type->tp_as_number;
    if (mv != NULL) {
        binaryfunc slot = NB_BINOP(mv, iop_slot);
        if (slot) {
            PyObject *x = (slot)(v, w);
            if (x != Py_NotImplemented) {
                return x;
            }
            Py_DECREF(x);
        }
    }
    return binary_op1(v, w, op_slot);
}

but because of code differences necessary for 3-argument pow, **= can't go through that code path, so it has its own ad-hoc handling:

PyObject *
PyNumber_InPlacePower(PyObject *v, PyObject *w, PyObject *z)
{
    if (v->ob_type->tp_as_number &&
        v->ob_type->tp_as_number->nb_inplace_power != NULL) {
        return ternary_op(v, w, z, NB_SLOT(nb_inplace_power), "**=");
    }
    else {
        return ternary_op(v, w, z, NB_SLOT(nb_power), "**=");
    }
}

This ad-hoc handling commits to either the in-place or non-in-place side, with no fallback if the in-place handler can't handle it.

like image 119
user2357112 supports Monica Avatar answered Apr 20 '26 11:04

user2357112 supports Monica



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!