Is it possible to numpy.vectorize an instance meth

2019-02-25 08:23发布

问题:

I've found that the numpy.vectorize allows one to convert 'ordinary' functions which expect a single number as input to a function which can also convert a list of inputs into a list in which the function has been mapped to each input. For example, the following tests pass:

import numpy as np
import pytest


@np.vectorize
def f(x):
    if x == 0:
        return 1
    else:
        return 2


def test_1():
    assert list(f([0, 1, 2])) == [1, 2, 2]

def test_2():
    assert f(0) == 1

if __name__ == "__main__":
    pytest.main([__file__])

However, I've not been able to get this to work for an instance method which makes use of an instance attribute. For example:

class Dummy(object):
    def __init__(self, val=1):
        self.val = val

    @np.vectorize
    def f(self, x):
        if x == 0:
            return self.val
        else:
            return 2


def test_3():
    assert list(Dummy().f([0, 1, 2])) == [1, 2, 2]

This test fails:

=================================== FAILURES ===================================
____________________________________ test_3 ____________________________________

    def test_3():
>       assert list(Dummy().f([0, 1, 2])) == [1, 2, 2]

test_numpy_vectorize.py:31: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/numpy/lib/function_base.py:2739: in __call__
    return self._vectorize_call(func=func, args=vargs)
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/numpy/lib/function_base.py:2809: in _vectorize_call
    ufunc, otypes = self._get_ufunc_and_otypes(func=func, args=args)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <numpy.lib.function_base.vectorize object at 0x106546470>
func = <function Dummy.f at 0x10653a2f0>, args = [array([0, 1, 2])]

    def _get_ufunc_and_otypes(self, func, args):
        """Return (ufunc, otypes)."""
        # frompyfunc will fail if args is empty
        if not args:
            raise ValueError('args can not be empty')

        if self.otypes is not None:
            otypes = self.otypes
            nout = len(otypes)

            # Note logic here: We only *use* self._ufunc if func is self.pyfunc
            # even though we set self._ufunc regardless.
            if func is self.pyfunc and self._ufunc is not None:
                ufunc = self._ufunc
            else:
                ufunc = self._ufunc = frompyfunc(func, len(args), nout)
        else:
            # Get number of outputs and output types by calling the function on
            # the first entries of args.  We also cache the result to prevent
            # the subsequent call when the ufunc is evaluated.
            # Assumes that ufunc first evaluates the 0th elements in the input
            # arrays (the input values are not checked to ensure this)
            args = [asarray(arg) for arg in args]
            if builtins.any(arg.size == 0 for arg in args):
                raise ValueError('cannot call `vectorize` on size 0 inputs '
                                 'unless `otypes` is set')

            inputs = [arg.flat[0] for arg in args]
>           outputs = func(*inputs)
E           TypeError: f() missing 1 required positional argument: 'x'

Is it possible to apply numpy.vectorize to an instance method?

回答1:

Simple solution without modifying the class

You can use np.vectorize directly on the method on the instance:

class Dummy(object):

    def __init__(self, val=1):
        self.val = val

    def f(self, x):
        if x == 0:
            return self.val
        else:
            return 2


vec_f = np.vectorize(Dummy().f) 


def test_3():
    assert list(vec_f([0, 1, 2])) == [1, 2, 2]

test_3()

You can also create a vectorized function vec_f in your __init__:

Adding a vectorized version to the instance

class Dummy(object):

    def __init__(self, val=1):
        self.val = val
        self.vec_f = np.vectorize(self.f) 

    def f(self, x):
        if x == 0:
            return self.val
        else:
            return 2


def test_3():
    assert list(Dummy().vec_f([0, 1, 2])) == [1, 2, 2]

or with a different naming scheme:

class Dummy(object):

    def __init__(self, val=1):
        self.val = val
        self.f = np.vectorize(self.scalar_f) 

    def scalar_f(self, x):
        if x == 0:
            return self.val
        else:
            return 2


def test_3():
    assert list(Dummy().f([0, 1, 2])) == [1, 2, 2]

test_3()

    test_3()


回答2:

Remembering a technique I saw in the memoized decorator, I managed to get the decorator to also work for instance methods by subclassing numpy.vectorize as follows:

import numpy as np
import functools


class vectorize(np.vectorize):
    def __get__(self, obj, objtype):
        return functools.partial(self.__call__, obj)

Now if I decorate the Dummy class' f method with vectorize instead of np.vectorize, the test passes:

class Dummy(object):
    def __init__(self, val=1):
        self.val = val

    @vectorize
    def f(self, x):
        if x == 0:
            return self.val
        else:
            return 2


def test_3():
    assert list(Dummy().f([0, 1, 2])) == [1, 2, 2]

if __name__ == "__main__":
    pytest.main([__file__])

with output

test_numpy_vectorize.py .

=========================== 1 passed in 0.01 seconds ===========================
[Finished in 0.7s]


回答3:

Here's a generic decorator that works with instance methods as well as functions (refer to Numpy's documentation for otypes and signature):

from functools import wraps

import numpy as np

def vectorize(otypes=None, signature=None):
    """Numpy vectorization wrapper that works with instance methods."""
    def decorator(fn):
        vectorized = np.vectorize(fn, otypes=otypes, signature=signature)
        @wraps(fn)
        def wrapper(*args):
            return vectorized(*args)
        return wrapper
    return decorator

You may use it to vectorize your method as follows:

class Dummy(object):
    def __init__(self, val=1):
        self.val = val

    @vectorize(signature="(),()->()")
    def f(self, x):
        if x == 0:
            return self.val
        else:
            return 2


def test_3():
    assert list(Dummy().f([0, 1, 2])) == [1, 2, 2]

The key is to make use of the signature kwarg. Parenthesized values to the left of -> specify input parameters and values to the right specify output values. () represents a scalar (0-dimensional vector); (n) represents a 1-dimensional vector; (m,n) represents a 2-dimensional vector; (m,n,p) represents a 3-dimensional vector; etc. Here, signature="(),()->()" specifies to Numpy that the first parameter (self) is a scalar, the second (x) is also a scalar, and the method returns a scalar (either self.val or 2, depending on x).

$ pytest /tmp/instance_vectorize.py
======================= test session starts ========================
platform linux -- Python 3.6.5, pytest-3.5.1, py-1.5.3, pluggy-0.6.0
rootdir: /tmp, inifile:
collected 1 item

../../tmp/instance_vectorize.py .                                                                                                                                                     [100%]

==================== 1 passed in 0.08 seconds ======================


回答4:

From the docs:

The data type of the output of vectorized is determined by calling the function with the first element of the input. This can be avoided by specifying the otypes argument.

The first input in your function f(self, x) is self. Maybe you can make that function a wrapper around a staticmethod function?