How can I avoid circular dependencies when setting properties?

This is a design principle issue for math / physics equation classes where the user is allowed to set whatever parameter the rest is calculated by. In this example, I would like to be able to set the frequency as well, avoiding circular dependencies.

For example:

from traits.api import HasTraits, Float, Property
from scipy.constants import c, h
class Photon(HasTraits):
    wavelength = Float # would like to do Property, but that would be circular?
    frequency = Property(depends_on = 'wavelength')
    energy = Property(depends_on = ['wavelength, frequency'])
    def _get_frequency(self):
        return c/self.wavelength
    def _get_energy(self):
        return h*self.frequency


I am also aware of the update trigger synchronization issue because I do not know the sequence in which the updates will be triggered:

  • Wavelength change
  • This causes the update of both dependent objects: frequency and energy.
  • But energy needs a frequency that needs to be renewed in order for the energy to have a value corresponding to the new wavelength!

(The answer to be accepted should also address this potential timing issue.)

So what's the best design pattern to get around these interdependent problems? In the end, I want the user to be able to update either the wavelength or the frequency as well as the frequency / wavelength and update the energy accordingly.

Problems like this, of course, arise mostly in all classes that try to deal with equations.

Let the competition begin!;)


source to share

2 answers

Thanks to Adam Hughes and Warren Walkesser on the Enthoughth mailing list, I figured out what was missing in my understanding. Properties do not exist as an attribute. Now I think of them as a kind of "virtual" attribute, depending entirely on what the author of the class is doing when _getter or _set is called.

So when I want the user to be able to set the wavelength and frequency by the user, I only need to understand that the frequency itself does not exist as an attribute, and that instead, when setting the time to the frequency, I need to update the 'fundamental' wavelength of the attribute, so that the next time a frequency is required, it is calculated again with a new wavelength!

I also need to thank user sr2222 for making me think about missing caching. I realized that the dependencies I was configuring with the "depend_on" keyword are only needed when using the "cached_property" property. If the computation cost is not that high or is not performed as often, _getters and _setters take care of everything you need and don't need to use the "depends_on" keyword.

Here is the now optimized solution I was looking for that allows you to set either the wavelength or frequency without circular loops:

class Photon(HasTraits):
    wavelength = Float 
    frequency = Property
    energy = Property

    def _wavelength_default(self):
        return 1.0
    def _get_frequency(self):
        return c/self.wavelength
    def _set_frequency(self, freq):
        self.wavelength = c/freq
    def _get_energy(self):
        return h*self.frequency


You can use this class like this:

photon = Photon(wavelength = 1064)



photon = Photon(frequency = 300e6)


to set the initial values ​​and get the energy now, she just uses it directly:



Note that the _wavelength_default method takes into account the case where the user initializes the Photon instance without providing an initial value. For the first access to the wavelength only, this method will be used to determine it. If I had not done this, the first access to the frequency would have resulted in the calculation of 1/0.



I would recommend teaching your application what to learn from what. For example, the typical case is that you have a set of n variables, and any of them can be derived from the rest. (You can of course model more complex cases, but I wouldn't do that until you run into cases like this).

This can be modeled as follows:

# variable_derivations is a dictionary: variable_id -> function
# each function produces this variable value given all the other variables as kwargs
class SimpleDependency:
  _registry = {}
  def __init__(self, variable_derivations):
    unknown_variable_ids = variable_derivations.keys() - self._registry.keys():
      raise UnknownVariable(next(iter(unknown_variable_ids)))
    self.variable_derivations = variable_derivations

  def register_variable(self, variable, variable_id):
    if variable_id in self._registry:
      raise DuplicateVariable(variable_id)
    self._registry[variable_id] = variable

  def update(self, updated_variable_id, new_value):
    if updated_variable_id not in self.variable_ids:
      raise UnknownVariable(updated_variable_id)
    other_variable_ids = self.variable_ids.keys() - {updated_variable_id}
    for variable_id in other_variable_ids:
      function = self.variable_derivations[variable_id]
      arguments = {var_id : self._registry[var_id] for var_id in other_variable_ids}

class FloatVariable(numbers.Real):
  def __init__(self, variable_id, variable_value = 0):
    self.variable_id = variable_id
    self.value = variable_value
  def assign(self, value):
    self.value = value
  def __float__(self):
    return self.value


This is just a sketch, I have not tested or considered every possible problem.



All Articles