# Python functions taking float and array-like arguments

Published Thursday 26 October 2023 at 12:30 WEST

I often find myself wanting to write a Python function that accepts scalars and array-like objects for each argument. In these cases I have to make a decision about that function's return type. Typically the function performs Numpy operations on the arguments passed to it. But the return type of Numpy functions often surprises me. If a scalar or 0d array-like object is passed to such a Numpy function then it returns a `numpy.float64`

object. Only otherwise does it return a `numpy.ndarray`

object. For example, using `numpy.sin`

I find the following.

>>> import numpy as np >>> w = np.sin(1); x = np.sin(1.); y = np.sin(np.array(1.)); z = np.sin([1., 1.]) >>> type(w); type(x); type(y); type(z) <class 'numpy.float64'> <class 'numpy.float64'> <class 'numpy.float64'> <class 'numpy.ndarray'>

Any operation on a `numpy.float64`

object returns another `numpy.float64`

object. More surprisingly, any operation on a 0d `numpy.ndarray`

object also returns a `numpy.float64`

object. Even multiplying by one does this.

>>> x = np.array(1.) >>> x; type(x); x.ndim; x.shape array(1.) <class 'numpy.ndarray'> 0 () >>> x = 1.*x >>> x; type(x); x.ndim; x.shape 1.0 <class 'numpy.float64'> 0 ()

So what should I do? A home-made function that is just some sequence of Numpy functions will always behave in the same way. Let's imagine that I define such a function, `func`

. It will return the appropriate Numpy type because it *is* a Numpy function, albeit a composite one.

def func(x): return some_function_of(x)

However, there is a good case to be made for a function always returning the same type of object, in this case a `numpy.ndarray`

. To do so I must stop floats and 0d arrays from being returned as `numpy.float64`

objects. The trick is to cast both of these types as 1d `numpy.ndarray`

objects and then squeeze them to become 0d `numpy.ndarray`

objects. This code does the trick.

def func(x): if np.isscalar(x): x = np.atleast_1d(x) scalar_flag = True else: scalar_flag = False res = some_function_of(x) if scalar_flag: return res.squeeze() return res

In this way, scalar and 0d array-like arguments become 0d Numpy arrays. Other arguments become Numpy arrays while preserving their dimension. Let's say that `some_function_of(x)`

is `numpy.log(x)`

. The Numpy function itself works as follows.

>>> x = np.log(1.) >>> x; type(x); x.ndim; x.shape 0.0 <class 'numpy.float64'> 0 () >>> x = log([1.]) >>> x; type(x); x.ndim; x.shape array([1.]) <class 'numpy.ndarray'> 1 (1,)

But `func`

works differently.

>>> x = func(1.) >>> x; type(x); x.ndim; x.shape array(0.) <class 'numpy.ndarray'> 0 () >>> x = func([1.]) >>> x; type(x); x.ndim; x.shape array([0.]) <class 'numpy.ndarray'> 1 (1,)

However, I prefer my functions to be consistent with Numpy's, so I need not worry about any of this. But this occasionally leaves me with another problem. Suppose that inside my function I wish to perform some check on the arguments. Let's say that my function takes two arguments and that I want to check that the first is compatible with second. I might coerce both arguments into being Numpy arrays and then compare their shapes.

def func(x, y): x = np.asarray(x) y = np.asarray(y) if not x.shape == y.shape: raise ValueError("message.") return x

The function works as follows.

>>> func(1., 1.) array(1.) >>> func([1.], [1.]) array([1.]) >>> func(1., [1.]) ValueError: message. >>> func([1.], [1., 1]) ValueError: message.

But if I want my function to be consistent with Numpy then casting scalars to arrays (i.e. promoting the dimension of an object) is exactly what I want to avoid doing. Having promoted the dimensions `x`

and `y`

I am stuck with Numpy arrays. I might try to extract the elements of `x`

and `y`

using `x.item()`

and `y.item()`

but this gives me Python `float`

objects, not Numpy `numpy.float64`

objects. The trick this time is to treat scalars and array-like objects separately and to coerce the array-like objects into being Numpy arrays instead of coercing the floats as I did before.

def func(x, y): if np.isscalar(x) and np.isscalar(y): return x else: x = np.asarray(x) y = np.asarray(y) if x.shape == y.shape: return x else: raise ValueError("message.")

This behaves how I want it to.

>>> func(1., 1.) 1. >>> func([1.], [1.]) array([1.]) >>> func(1., [1.]) ValueError: message. >>> func([1.], [1., 1]) ValueError: message.

Much better.

*The users of Stack Overflow helped me get my head straight on this by answering this question of mine.*