How to avoid circular dependencies when setting Properties?

前端 未结 2 1943
清酒与你
清酒与你 2021-01-21 09:12

This is a design principle question for classes dealing with mathematical/physical equations where the user is allowed to set any parameter upon which the remaining are being ca

相关标签:
2条回答
  • 2021-01-21 09:30

    I would recommend to teach your application what can be derived from what. For example, a typical case is that you have a set of n variables, and any one of them can be derived from the rest. (You can model more complicated cases as well, of course, but I wouldn't do it until you actually run into such cases).

    This can be modeled like this:

    # variable_derivations is a dictionary: variable_id -> function
    # each function produces this variable's 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)
        self._registry[updated_variable_id].assign(new_value)
        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}
          self._registry[variable_id].assign(function(**arguments))
    
    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 didn't test or think through every possible issue.

    0 讨论(0)
  • 2021-01-21 09:48

    Thanks to Adam Hughes and Warren Weckesser from the Enthought mailing list I realized what I was missing in my understanding. Properties do not really exist as an attribute. I now look at them as something like a 'virtual' attribute that completely depends on what the writer of the class does at the time a _getter or _setter is called.

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

    I also need to thank the user sr2222 who made me think about the missing caching. I realized that the dependencies I set up by using the keyword 'depends_on' are only required when using the 'cached_property' Trait. If the cost of calculation is not that high or it's not executed that often, the _getters and _setters take care of everything that one needs and one does not need to use the 'depends_on' keyword.

    Here now the streamlined solution I was looking for, that allows me to set either 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
    

    One would use this class like this:

    photon = Photon(wavelength = 1064)
    

    or

    photon = Photon(frequency = 300e6)
    

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

    print(photon.energy)
    

    Please note that the _wavelength_default method takes care of the case when the user initializes the Photon instance without providing an initial value. Only for the first access of wavelength this method will be used to determine it. If I would not do this, the first access of frequency would result in a 1/0 calculation.

    0 讨论(0)
提交回复
热议问题