2

I apologise if the title is cryptic, I could not think of a way to describe my problem in a sentence. I am building some code in python2.7 that I describe below.

Minimal working example

My code has a Parameter class that implements attributes such as name and value, which looks something like this.

class Parameter(object):
    def __init__(self, name, value=None, error=None, dist=None, prior=None):
        self.name   = name
        self._value = value # given value for parameter, this is going to be changed very often in an MCMC sampler
        self.error  = error # initial estimate of error for the parameter, will only be set once
        self._dist  = dist # a distribution for the parameter, will only be set once
        self.prior  = prior

    @property
    def value(self):
        return self._value

    @property
    def dist(self):
        return self._dist

The class also has several properties that returns the mean, median, etc. of Parameter.dist if a distribution is given.

I have another class, e.g. ParameterSample, that creates a population of different Parameter objects. Some of these Parameter objects have their attributes (e.g. value, error) set using the Parameter.set_parameter() function, but some other Parameter objects are not explicitly set, but their value and dist attributes depend on some of the other Parameter objects that are set:

class ParameterSample(object):
    def __init__(self):
        varied_parameters  = ('a', 'b') # parameter names whose `value` attribute is varied
        derived_parameters = ('c',) # parameter names whose `value` attribute is varied, but depends on `a.value` and `b.value`
        parameter_names    = varied_parameters + derived_parameters
        
        # create `Parameter` objects for each parameter name
        for name in parameter_names:
            setattr(self, name, Parameter(name))

    def set_parameter(self, name, **kwargs):
        for key, val in kwargs.items():
            if key == 'value':
                key = '_'.join(['', key]) # add underscore to set `Parameter._value`
            setattr(getattr(self, name), key, val) # basically does e.g. `self.a.value = 1`

I can now create a ParameterSample and use them like this:

parobj = ParameterSample()
parobj.set_parameter('a', value=1, error=0.1)
parobj.set_parameter('b', value=2, error=0.5)

parobj.a.value
>>> 1
parobj.b.error
>>> 0.5

parobj.set_parameter('b', value=3)
parobj.b.value
>>> 3
parobj.b.error
>>> 0.5

What I want

What I ultimately want, is to use Parameter.c the same way. For example:

parobj.c.value
>>> 4 # returns parobj.a.value + parobj.b.value
parobj.c.dist
>>> None # returns a.dist + b.dist, but since they are not currently set it is None

c therefore needs to be a Parameter object with all the same attributes as a and b, but where its value and dist are updated according to the current attributes of a and b.

However, I should also mention that I want to be able to set the allowed prior ranges for parameter c, e.g. parobj.set_parameter('c', prior=(0,10)) before making any calls to its value -- so c needs to be an already defined Parameter object upon the creation of the ParameterSample object.

How would I implement this into my ParameterSample class?

What I've tried

I have tried looking into making my own decorators, but I am not sure if that is the way to go since I don't fully understand how I would use those.

I've also considered adding a @property to c that creates a new Parameter object every time it is called, but I feel like that is not the way to go since it may slow down the code.

I should also note that the ParameterSample class above is going to be inherited in a different class, so whatever the solution is it should be able to be used in this setting:

class Companion(ParameterSample)
    def __init__(self, name):
        self.name = name
        super(Companion, self).__init__()

comp = Companion(name='Earth')
comp.set_parameter('a', value=1)
comp.set_parameter('b', value=3)
comp.c.value
>>> 4
Community
  • 1
  • 1
vedadh
  • 21
  • 2

1 Answers1

0

I could not get this to work in Python 2 - the setattr calls never seemed to propagate the attributes to the child classes (Companion would have no c attribute).

I was more successful with Python 3 though. Since you have two parameter types (varied vs. derived), it makes sense IMO to have two classes to implement the behavior, instead of treating them all as one.

I added a DerivedParameter class, inheriting from Parameter that takes a dependents argument (along with its parent class' args/kwargs), but redefining value and dist to give dependent behavior:

class DerivedParameter(Parameter):
    def __init__(self, name, dependents, **kwargs):
        self._dependents = dependents
        super().__init__(name, **kwargs)

    @property
    def value(self):
        try:
            return sum(x._value for x in self._dependents if x is not None)
        except TypeError:
            return None

    @property
    def dist(self):
        try:
            return sum(x._dist for x in self._dependents if x is not None)
        except TypeError:
            return None

Then I adjusted how your parameter objects are added:

class ParameterSample:
    def __init__(self):
        # Store as instance attributes to reference later
        self.varied_params  = ('a', 'b') # parameter names whose `value` attribute is varied
        self.derived_params = ('c',) # parameter names whose `value` attribute is varied, but depends on `a.value` and `b.value`
        # No more combined names

        # create `Parameter` objects for each varied parameter name
        for name in self.varied_params:
            setattr(self, name, Parameter(name))

        # Create `DerivedParameter` objects for each derived parameter
        # Derived parameters depend on all `Parameter` objects. It wasn't
        # clear if this was the desired behavior though.
        params = [v for _, v in self.__dict__.items() if isinstance(v, Parameter)]
        for name in self.derived_params:
            setattr(self, name, DerivedParameter(name, params))

    def set_parameter(self, name, **kwargs):
        for key, val in kwargs.items():
            if key == 'value':
                key = '_'.join(['', key]) # add underscore to set `Parameter._value`
            setattr(getattr(self, name), key, val) # basically does e.g. `self.a.value = 1`

From this, I could then replicate your given example desired behavior:

>>> comp = Companion(name='Earth')
>>> comp.set_parameter('a', value=1)
>>> comp.set_parameter('b', value=3)
>>> print(comp.c.value)
>>> print(comp.c.dist)
4
None
>>> comp.set_parameter('c', prior=(0,10))
>>> print(comp.c.prior)
(0, 10)

As I pointed out in the comments, the design above ends up causing all derived parameters to use all varied parameters as their dependents - effectively making c and a potential d identical. You should be able to fix this fairly easily with some parameters/conditions.

Overall, I would have to agree with @Error - Syntactical Remorse though. This is a pretty complicated way to go about designing classes and would make maintenance confusing at best. I would strongly encourage you to reconsider your design and try to find an adaptable general solution that doesn't involve dynamic creation of attributes like this.

b_c
  • 1,202
  • 13
  • 24