pythonclassoopdecorator

What is the good practice to add many similar methods to a class in Python?


Let's say I have a class like this.

from scipy import interpolate
import numpy as np

class Bar:
    def __init__(self,data):
        self.data = data
    
    def mean(self):
        return self.data.mean(axis=0)
    
    def sd(self):
        return self.data.std(axis=0)

bar = Bar(np.random.rand(10,5))
print(bar.mean())
print(bar.sd())

The class Bar may have many such methods such as mean(), sd() etc. I want to add sampled versions of those methods, so that I can simply get results equivalent to this:

new_ids = np.linspace(bar.ids[0],bar.ids[-1],100)
sampled_mean = interpolate.interp1d(bar.ids,bar.mean(),axis=0)(new_ids)

Current workaround: manually add new methods with help of decorators.

from scipy import interpolate
import numpy as np

def sample(func):
    def wrapper(self,n_sample=100,*args,**kwargs):
        new_ids = np.linspace(self.ids[0],self.ids[-1],n_sample)
        vals = func(self,*args,**kwargs)
        return interpolate.interp1d(self.ids,vals,axis=0)(new_ids)
    return wrapper

class Bar:
    def __init__(self,ids,data):
        self.ids = ids
        self.data = data
    
    def mean(self):
        return self.data.mean(axis=0)
    
    def sd(self):
        return self.data.std(axis=0)
    
    @sample
    def spl_mean(self):
        return self.mean()
    
    @sample
    def spl_sd(self):
        return self.sd()

bar = Bar(ids=np.arange(5),data=np.random.rand(10,5))

new_ids = np.linspace(bar.ids[0],bar.ids[-1],100)
sampled_mean = interpolate.interp1d(bar.ids,bar.mean(),axis=0)(new_ids)
assert np.all(sampled_mean == bar.spl_mean())

However, I have many such methods. Of course I can write them with the help of LLM but what I want to know is:


Solution

  • One DRYer approach that avoids repeating method names for the sampled versions is to make the decorator a custom descriptor whose __get__ method returns an object that calls the decorated function when called, but also provides a sample method that calls the decorated function with the sampling logics around it:

    class Samplable:
        def __init__(self, method):
            self.method = method
    
        def __call__(self, *args, **kwargs):
            return self.method(*args, **kwargs)
    
        def sample(self, *args, **kwargs):
            return f'sampled {self(*args, **kwargs)}'
    
    class sample:
        def __init__(self, func):
            self.func = func
    
        def __get__(self, obj, objtype=None):
            return Samplable(self.func.__get__(obj))
    
    class Bar:
        def __init__(self, data):
            self.data = data
    
        @sample
        def mean(self):
            return f'mean {self.data}'
    
        @sample
        def sd(self):
            return f'sd {self.data}'
    

    so that:

    bar = Bar('data')
    print(bar.mean())
    print(bar.mean.sample())
    print(bar.sd())
    print(bar.sd.sample())
    

    outputs:

    mean data
    sampled mean data
    sd data
    sampled sd data
    

    Demo: https://ideone.com/RBa3UJ