3

In python, is there an easy and efficient way to make function f(x: float) accept both lists and numpy arrays as arguments (in which case I would want to apply f element-wise and return the result in the same format as it was sent in)? For now, I need only 1-dimensional arrays.

As an illustration (my real f is more complex), let's say that I have:

def f(x):
    return math.log(x) if x > 0 else 0.0

Then this one works, but is not that elegant - and possibly not that efficient either, because of the recursion (which I use as it allows me to have just one function):

def f(x):
    if np.isscalar(x):
        return math.log(x) if x > 0 else 0.0
    elif isinstance(x, np.ndarray):
        return np.array([f(i) for i in x], dtype=float)
    else:
        return [f(i) for i in x]

Is there a better way?

4
  • 3
    You can look into np.vectorize and np.frompyfunc. Commented Apr 15, 2020 at 8:26
  • @hilberts_drinking_problem: but this would create new function that returns an numpy array. Since I want the output in the same format as input (scalar/list/array), this would need at least two functions and still some ifs to decide what to do - how is it better than my version? Commented Apr 15, 2020 at 9:19
  • Preserving the input type sounds like a single/multiple dispatch issue, which is a big deal in language design. You may be able to simplify the code for performing this in Python, but I doubt if you can gain much performance as of today. Commented Apr 15, 2020 at 9:54
  • 1
    You could probably use a decorator for a clean pythonic way to add pre- and post- call processing to your function. Commented Feb 27, 2021 at 22:11

1 Answer 1

3

Using a decorator function would be a good option if this is something you'd need to for more than just a single function definition in your code-base. It does mean that you have to wrap your code, but I'm assuming that what you mean by "having just one function" is that you do want the function expression in f(x) to be the same for all cases.

Using your existing code, the decorator function would look like this:

def elementwise_possible(func):
    def wrapper(x):
        if np.isscalar(x):
            return func(x)
        elif isinstance(x, np.ndarray):
            return np.array([func(i) for i in x], dtype=float)
        else:
            return [func(i) for i in x]
    return wrapper 

And you would write your function like this:

@elementwise_possible
def f(x):
    return math.log(x) if x > 0 else 0.0

The resulting output becomes

In[2]: A = f(2)
In[3]: A
Out[3]: 0.6931471805599453

In[4]: B = f(np.array([2,3,4]))
In[5]: B
Out[5]: array([0.69314718, 1.09861229, 1.38629436])

In[6]:C = f([5,6,7])
In[7]:C
Out[7]: [1.6094379124341003, 1.791759469228055, 1.9459101490553132]

I think efficiency should be the same.

Sign up to request clarification or add additional context in comments.

1 Comment

I should have read your answer before adding my comment to the question. Good one!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.