I am writing a class for which objects are initialised with two parameters (a
, b
). The intention is to assign instances of this class to variables so that I can have an equation written symbolically in Python code, but have operator overloading perform unique operations on a
and b
.
import numpy as np
class my_class(object):
def __init__(self, a, b):
self.value1 = a
self.value2 = b
# Example of an overloaded operator that works just fine
def __mul__(self, other):
new_a = self.value1 * other
new_b = self.value2 * np.absolute(other)
return my_class(new_a, new_b)
if __name__ == "__main__":
my_object = my_class(100, 1)
print(np.exp(my_object)) # This doesn't work!
In running the above example code, I encountered the following output:
TypeError: loop of ufunc does not support argument 0 of type my_class which has no callable exp method
Through guesswork, I was able to see that a complaint about no callable exp method
probably meant I needed to define a method using:
def exp(self):
...
which ended up working just fine. But now I will have to write another method for np.expm1()
and so on as I require. Thankfully I only need np.exp()
and np.log()
to work, but I also tried math.exp()
on my object and I started getting a type error.
So now my question is:
The custom exp
method in the class seemed to work for overloading the NumPy function, but how am I supposed to handle math.exp()
not working? It must be possible because somehow when calling math.exp()
on a NumPy array, NumPy understands that a 1-element array can be turned into a scalar and then passed to math.exp()
without issue.
I mean I guess this technically is about overloading a function, but before I realised defining a new exp
was the fix to my first problem, I had no idea why a method like __rpow__
wasn't being called.
CodePudding user response:
First, on why math.exp
works with 1-element Numpy arrays: the numpy.ndarray
class has a __float__
method; this method is part of the Python data model for numeric types, and is used when float(x)
is called. I couldn't spot anything in the math
docs that says that math.exp
casts its argument to float
, but it's not unreasonable behaviour.
As for customizing behaviour of Numpy's ufuncs: the recommended way to implement "array-like" objects that override ufunc behaviour is somewhat complicated. I couldn't find documentation on providing exp
, log
, etc. methods to customize ufuncs. Supplying methods like this doesn't work in all cases, for example np.heaviside
; this example
import numpy as np
class foo:
def exp(self):
return foo()
def heaviside(self, other):
return foo()
print(f'{np.exp(foo()) = }')
print(f'{np.heaviside(foo(), foo()) = }')
gives this output:
np.exp(foo()) = <__main__.foo object at 0x7efcdbba1ac0>
Traceback (most recent call last):
File "/home/rory/hack/stackoverflow/q70312146/heaviside.py", line 14, in <module>
print(f'{np.heaviside(foo(), foo()) = }')
TypeError: ufunc 'heaviside' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
CodePudding user response:
np.exp(my_object)
is implemented as np.exp(np.array(my_object))
.
np.array(my_object)
is a object dtype array. np.exp
tries elmt.exp()
for each element of the array. That doesn't work for most classes, since they don't implement such a method.
Same applies for operators and other ufunc
.
math.exp
is an unrelated implementation. It apparently works for something that gives a single numeric value, but I haven't explored that much. numpy
will raise an error if it can't do that.
Implementing *
with a class __mul__
is done by interpreter.
Same error message when using array in math.exp
and with __float__()
In [52]: math.exp(np.array([1,2,3]))
Traceback (most recent call last):
File "<ipython-input-52-40503a52084a>", line 1, in <module>
math.exp(np.array([1,2,3]))
TypeError: only size-1 arrays can be converted to Python scalars
In [53]: np.array([1,2,3]).__float__()
Traceback (most recent call last):
File "<ipython-input-53-0bacdf9df4e7>", line 1, in <module>
np.array([1,2,3]).__float__()
TypeError: only size-1 arrays can be converted to Python scalars
Similarly when an array is used in a boolean context (e.g if
), we can get an error generated with
In [55]: np.array([1,2,3]).__bool__()
Traceback (most recent call last):
File "<ipython-input-55-04aca1612817>", line 1, in <module>
np.array([1,2,3]).__bool__()
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
Similarly using a sympy
Relational in an if
results in the error produced by
In [110]: (x>0).__bool__()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-110-d88b76ce6b22> in <module>
----> 1 (x>0).__bool__()
/usr/local/lib/python3.8/dist-packages/sympy/core/relational.py in __bool__(self)
396
397 def __bool__(self):
--> 398 raise TypeError("cannot determine truth value of Relational")
399
400 def _eval_as_set(self):
TypeError: cannot determine truth value of Relational
pandas
Series produce a similar error.