The NumPy ufunc machinery has built-in hooks to customize how objects are treated by ufuncs. In this particular case, the numpy.exp
ufunc calls the Series's __array__
method to get an array to work with, computes the exponential over the array, and then calls the Series's __array_wrap__
method on the resulting array to post-process it.
__array__
is how the ufunc gets an object it knows how to work with, and __array_wrap__
is how the result gets converted back to a Series instead of an array.
You can see the same mechanisms in action by writing your own class with those methods:
In [9]: class ArrayWrapper(object):
...: def __init__(self, arr):
...: self.arr = arr
...: def __repr__(self):
...: return 'ArrayWrapper({!r})'.format(self.arr)
...: def __array__(self):
...: return self.arr
...: def __array_wrap__(self, arr):
...: return ArrayWrapper(arr)
...:
In [10]: numpy.exp(ArrayWrapper(numpy.array([1, 2, 3])))
Out[10]: ArrayWrapper(array([ 2.71828183, 7.3890561 , 20.08553692]))