Pythonic way to construct a Factory/Builder class pattern for smoothing a series
Asked Answered
C

3

6

I have a number of classes that implement smoothing techniques on a series of prices.

I am trying to figure the best way of implementing any of these smoothing classes within the __call__ function of another class, where some operation is then performed on the smoothed series:

i.e.

class NoSmoother:
    def __init__(self, prices):
        self.prices = prices
    
    def __call__(self):
        return self.prices

class MASmoother:
    def __init__(self, prices):
        self.prices = prices
    
    def __call__(self, window):
        return self.prices.rolling(window).mean().to_frame("price")


class ThatDoesSomethingWithSmoothedPrices():
    def __init__(self, prices):
        self.prices = prices

    def __call__(self, smoother=ma_smoother, window=3)
        smoothed_prices = SomeBuilderClassThatCallsTheCorrectSmootherClass()

As you can see, I would need the factory/builder class to implement NoSmoother if say smoother = None, otherwise, it would call the appropriate smoothing class. Of course, the returned object can vary from simple to complex, for example, if I smooth the prices using a Kalman Filter, the class can expect many more parameters.

Currently, my code instantiates class ThatDoesSomethingWithSmoothedPrices() with a price series, then calls by passing a **config.

Desired Output:

Ideally, I would like to be able to call any smoothing class from within the call function of

class ThatDoesSomethingWithSmoothedPrices().

Example implementation:

configs = {'smoother': MASmoother, 'window': 3}

processor = ThatDoesSomethingWithSmoothedPrices(prices)

output = processor(**config)

My attempt:

class Smoother:
    def __init__(self, prices):
        self.prices = prices
    
    def __call__(self, smoother, *args, **kwargs):
        return partial(smoother, **kwargs)
      
    def ma_smoother(self, window: int = 3):
        return self.prices.rolling(window).mean().to_frame("price")
        
    def no_smoother(self):
        return self.prices

class ThatDoesSomethingWithSmoothedPrices:
    def __init__(self, prices):
        self.prices = prices
    
    def __call__(self, smooth_method = 'no_smoother'):
        smoother = Smoother(prices)
        prices_smoothed = smoother(**configs)
        # do other things

if __name__ == '__main__':
   configs = {'smoother': 'ma_smoother', window=3}

   label = ThatDoesSomethingWithSmoothedPrices(**configs)

Any help greatly appreciated.

Contreras answered 22/3, 2022 at 11:51 Comment(9)
I'd make these just functions, not classes...Cesena
What is going to determine which class to instantiate? Will that be known well ahead of time, or perhaps not until the call needs to take place?Siderolite
@Siderolite - when I call ThatDoesSomethingWithSmoothedPrices(), I pass a config that determines the class to instantiate and it's parametersContreras
Please update your post with all this calling and parameter passing you do. With python its easy to just write the code that you want to have and sort out the details later.Siderolite
Ideally, the user will pass a config file to the execution of ThatDoesSomethingWithSmoothedPrices() naming the smoothing method to be applied or none at all.Contreras
Yes, see my answer below.Siderolite
Thank you for your answer. It is a requirement that the class ThatDoesSomethingWithSmoothedPrices, remains a class. What I had envisioned is some form of base class, with a lookup i.e NamedTuple that when called within ThatDoesSomethingWithSmoothedPrices knows what smoother to apply? Hopefully I am making sense.Contreras
There is nothing you have shown us that requires ThatDoesSomethingWithSmoothedPrices to be a class. It can simply be a factory function which returns the correct smoother.Siderolite
the class, ThatDoesSomethingWithSmoothedPrices takes the smoothed prices and creates labels off the back of these - it calls many different methods in order to construct the label i.e. it's purpose is associated with something quite different from simply smoothing the price.Contreras
C
4

For simplicity, if you don't have a lot of state, I'd just use regular functions.

You can use functools.partial() to partially apply a function, i.e. in this case set the MA window:

from functools import partial


def no_smoother(values):
    return values


def ma_smoother(values, *, window):
    return values.rolling(window).mean().to_frame("price")


def get_prices():
    ...


def get_smoothed_prices(smoother):
    prices = get_prices()
    return smoother(prices)


get_smoothed_prices(smoother=no_smoother)
get_smoothed_prices(smoother=partial(ma_smoother, window=3))

EDIT

Based on the edit in the question:

configs = {'smoother': MASmoother, 'window': 3}
processor = ThatDoesSomethingWithSmoothedPrices(prices)
output = processor(**config)

would be expressed as something like

def construct_smoother(smoother, **kwargs):
    return partial(smoother, **kwargs)

smoother = construct_smoother(**configs)
# ...
Cesena answered 22/3, 2022 at 12:7 Comment(6)
Thank you for your response. Can you please show me how you would implement this in the call method of the class ThatDoesSomethingWithSmoothedPrices()?Contreras
The equivalent of that class and method here is get_smoothed_prices itself; it gets the unsmoothed prices and applies a smoother. If you need something to construct a smoother with the given arguments... well, that's partial().Cesena
@Contreras Please see my edit - it might illuminate things.Cesena
Many thanks for this. The only problem I have is that I would like this will be used across a lot of different repos, and call from different modules. So for example, I would pass the config to the ___call__ method of thatDoesSomethingWithSmoothedPrices(prices), it would interpret the key and know which smoother to use, without having to import the smoother implementation locally (if that makes sense?) Would it be possible to show me how you would do so in a class please? I have added my attempt above.Contreras
You'd probably want a registry (dict) that would map smoothing names to their implementations. However, with that indirection, your users would need to know the (kw)args for each smoother; I assume window is only relevant for moving average here. In that sense, it would be a better API to just have your users pass in smoothing functions they've constructed. Another, higher-level API could work with a config dict.Cesena
Agree - this is what I envisioned, however, I was unsure of what to implement conventially in this regard? You are right insofar that the user already knows the names of the smoothers. Thank you once again.Contreras
S
1

Given your latest update you need to pass in the configs as well:

configs = {'smoother': MASmoother, 'window': 3}

processor = ThatDoesSomethingWithSmoothedPrices(configs, prices)

output = processor(**config)

So that means ThatDoesSomethingWithSmoothedPrices could be like this:

def ThatDoesSomethingWithSmoothedPrices(configs, prices):
    smoother = configs['smoother'](prices)
    return smoother
Siderolite answered 22/3, 2022 at 12:20 Comment(0)
M
0

To create a factory, you can create a class method inside a class.

# declaration
class Factory:
  # class variables (static)
  id = 0
  
  @classmethod
  def new(cls):
    return object()

# usage
obj = Factory.new()

If you have a class that needs arguments in the constructor, then you can pass variable number of arguments. Basically * removes brackets around the list or dict you are passing.

# declaration
def function(var1, var2, *args, **kwargs):
  # transfer variable arguments to other function
  return SomeObejct(*args, **kwargs)

# usage
obj = function(1, 2, a, b, c, key=value)

In your case, you would do something like this:

# declaration
# you can also pass classes as arguments
def __call__(self, smoother=MASmoother, window=3, *args, **kwargs)
    smoothed_prices = smoother(*args, **kwargs)
    return smoothed_prices

# usage
smth = ThatDoesSomethingWithSmoothedPrices()
smoother1 = smth()
smoother2 = smth(NoSmoother, 2)
smoother3 = smth(NoSmoother, 2, arg1, arg2, key=value)
Masquer answered 22/3, 2022 at 12:3 Comment(1)
I don't get what this answer/code is trying to say. The class Factory seems bogus – the new constructor is plain wrong, and it's not clear at all why not to use the regular construction (__new__/__init__). The def function is – what exactly? Just an example for *args, **kwargs? It doesn't help that the actual code relating to the question – the def __call__ and ThatDoesSomethingWithSmoothedPrices – omits major parts such as the class around def __call__ and adds bogus arguments that are discarded or erroneously forwarded.Infold

© 2022 - 2024 — McMap. All rights reserved.