Automatically add methods to a class in Python

Question:

I’m trying to create a custom class to handle timeseries related to various objects.

They all inherit from a base class called Timeseries, which looks like this

class Timeseries:
    def __init__(self, t_max: int):
        self.t_max = t_max
        self.current_t = 0

    def update(self):
        if self.current_t == self.t_max:
            raise TimeExceededError(self.current_t, self.t_max)
        self.current_t += 1

Next I am creating an object that inherits from this, as in

@dataclass
class MyTimeseries(Timeseries):
   a: np.ndarray

and possibly other attributes like a that would also be time series.

I would like to find a way to automatically generate methods for each attribute in MyTimeseries, so that for example I can call

ts = MyTimeseries(a=np.array([1,2,3,4]), t_max=4)
print(ts.current_a) #prints 1
ts.update()
print(ts.current_a) # prints 2
print(ts.prev_a) # prints 1

Normally I’d do this by creating a property, as

@property
def current_a(self):
   return self.a[self.current_t]

Is there a way to have all of these properties generated automatically? Something like decorating the class MyTimeseries or even Timeseries ?

So far, all I’ve managed to do is to write a script that writes all the relevant functions, which I then copy paste into my .py file, but this is very inefficient.


Following up on the answer below, I tried implementing the following.

For the Timeseries, I now have the following metaclass



class TimeseriesMeta(type):
    def __new__(mcs, name, bases, attrs, *args, **kwargs):
        for a in attrs['__annotations__']:
            if (a == 't_max') or (a == 'current_t'): continue
            attrs[f'current_{a}'] = property(lambda self: getattr(self, a)[self.current_t])
            attrs[f'prev_{a}'] = property(lambda self: getattr(self, a)[self.current_t - 1])
        return super().__new__(mcs, name, bases, attrs, *args, **kwargs)


@dataclass
class Timeseries(metaclass=TimeseriesMeta):
    t_max: int

    def __post_init__(self):
        self.current_t = 0

    def update(self):
        if self.current_t == self.t_max:
            raise ValueError(self.current_t, self.t_max)
        self.current_t += 1

I implemented a simple version of the Timeseries object with a classmethod to initialise it, as

@dataclass
class Dummy(Timeseries):
    a: np.ndarray

    @classmethod
    def from_initial_value(cls, a_0: float, t_max:int):
        a = np.zeros(t_max)
        a[0] = a_0
        return cls(a=a, t_max=t_max)

I run the following test, and it passes

class TestDummy:
    def test__dummy(self):
        a_0 = np.ones(2)
        t_max = 1
        series = Dummy.from_initial_value(a_0=a_0, t_max=t_max)
        assert series.current_a == pytest.approx(a_0)

However, I’m trying to run a more complicated case, which looks as follows

@dataclass
class CBTimeseries(Timeseries):
    # ea prefixes indicate Euro Area
    ea_inflation: np.ndarray
    ea_gdp: np.ndarray
    ea_growth: np.ndarray
    profits: np.ndarray
    r_policy_rate: np.ndarray
    shadow_interest_rate: np.ndarray
    equity: np.ndarray
    row_debt: np.ndarray
    t_max: int
    current_t: int = 0

    @classmethod
    def init_default(cls, t_max: int,
                     initial_ea_inflation: float,
                     initial_ea_gdp: float,
                     initial_rate: float,
                     initial_row_debt: float,
                     initial_equity: float = 1e7):
        ea_inflation = np.zeros(t_max)
        ea_inflation[0] = initial_ea_inflation
        ea_gdp = np.zeros(t_max)
        ea_gdp[0] = initial_ea_gdp
        ea_growth = np.zeros(t_max)
        profits = np.zeros(t_max)
        r_policy_rate = np.zeros(t_max)
        r_policy_rate[0] = initial_rate
        shadow_interest_rate = np.zeros(t_max)
        equity = np.zeros(t_max)
        equity[0] = initial_equity
        row_debt = np.zeros(t_max)
        row_debt[0] = initial_row_debt
        return cls(ea_inflation=ea_inflation,
                   ea_gdp=ea_gdp,
                   ea_growth=ea_growth,
                   profits=profits,
                   r_policy_rate=r_policy_rate,
                   shadow_interest_rate=shadow_interest_rate,
                   equity=equity,
                   row_debt=row_debt,
                   t_max=t_max)

I run this test

    def test__cb_ts_current(self):
        init_params ={'t_max': 2,
              'initial_ea_inflation': 0.03,
              'initial_ea_gdp': 15e9,
              'initial_rate': 2e-2,
              'initial_row_debt': 0}
        cb_ts_test = CBTimeseries.init_default(**init_params)
        assert cb_ts_test.current_ea_inflation == 0.03

which doesn’t pass, giving an error


>   attrs[f'current_{a}'] = property(lambda self: getattr(self, a)[self.current_t])
E   TypeError: 'int' object is not subscriptable
Asked By: Someone1348

||

Answers:

You can write a custom metaclass for that:

from dataclasses import dataclass

class TimeseriesMeta(type):
    def __new__(mcs, name, bases, attrs, *args, **kwargs):
        for a in attrs['__annotations__']:
            if a in {'t_max', 'current_t'}: continue
            attrs[f'current_{a}'] = property(lambda self, a=a: getattr(self, a)[self.current_t])
            attrs[f'prev_{a}'] = property(lambda self, a=a: getattr(self, a)[self.current_t - 1])
        return super().__new__(mcs, name, bases, attrs, *args, **kwargs)


@dataclass
class Timeseries(metaclass=TimeseriesMeta):
    t_max: int

    def __post_init__(self):
        self.current_t = 0

    def update(self):
        if self.current_t == self.t_max:
            raise ValueError(f'{self.current_t}, {self.t_max}')
        self.current_t += 1


@dataclass
class MyTimeseries(Timeseries):
    a: list[int]


ts = MyTimeseries(a=[1,2,3,4], t_max=4)
print(ts.current_a) #prints 1
ts.update()
print(ts.current_a) # prints 2
print(ts.prev_a) # prints 1

It works in in a very simple way: for every field in __annotations__ (every variable you have annotated in the class body), add properties current_* and prev_*. This is more like a bare bones of the solutions, you may want to tweak it to exclude other fields or do some more reliable checks.

I replaced np.array with list just to avoid installing numpy locally, you can restore it back, it doesn’t affect anything. I switched to dataclass + __post_init__ to make your original implementation work (__init__ is tricky for dataclasses, I’m not sure that your attempt was fine, now it’s more obvious).

Answered By: SUTerliakov
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.