Here's my try on it. If you're doing this for some end users, you might want to skip. What I did probably works well for setting up some fast math objects library, but only when the user knows what's going on.
Idea was that all variables describing a math object follow the same pattern, a=something*smntng.
So when calculating a variable irl, in the worst case I would be missing "something", then I'd go and calculate that value, and any values I'd be missing when calculating that one, and bring it back to finish calculating the original variable I was looking for. There's a certain recursion pattern noticeable.
When calculating a variable therefore, at each access of a variable I've got to check if it exists, and if it doesn't calculate it. Since it's at each access I have to use __getattribute__
.
I also need a functional relationship between the variables. So I'll pin a class attribute relations
which will serve just that purpose. It'll be a dict of variables and an appropriate function.
But I've also got to check in advance if I have all the necessary variables to calculate current one. so I'll amend my table, of centralized math relations between variables, to list all dependencies and before I go to calculate anything, I'll run over the listed dependencies and calc those if I need too.
So now it looks more like we'll have a ping pong match of semi-recursion where a function _calc
will call __getattribute__
which calls function _calc
again. Until such a time we run out of variables or we actually calculate something.
The Good:
- There are no
if
s
- Can initialize with different init variables. As long as the sent variables enable calculations of others.
- It's fairly generic and looks like it could work for any other mathematical object describable in a similar manner.
- Once calculated all your variables will be remembered.
The Bad:
- It's fairly "unpythonic" for whatever that word means to you (explicit is always better).
- Not user friendly. Any error message you recieve will be as long as the number of times
__getattribute__
and _calc
called each other. Also no nice way of formulating a pretty error print.
- You've a consistency issue at hand. This can probably be dealt with by overriding setters.
- Depending on initial parameters, there is a possibility that you'll have to wait a long time to calculate a certain variable, especially if the requested variable calculation has to fall through several other calculations.
- If you need a complex function, you have to make sure it's declared before
relations
which might make the code ugly (also see last point). I couldn't quite work out how to get them to be instance methods, and not class methods or some other more global functions because I basically overrided the .
operator.
- Circular functional dependencies are a concern as well. (
a
needs b
which needs e
which needs a
again and into an infinite loop).
relations
are set in a dict
type. That means here's only 1 functional dependency you can have per variable name, which isn't necessarily true in mathematical terms.
- It's already ugly:
value = self.relations[var]["func"]( *[self.__getattribute__(x) for x in requirements["req"]] )
Also that's the line in _calc
that calls __getattribute__
which either calls _calc
again, or if the variable exists returns the value. Also at each __init__
you have to set all your attributes to None, because otherwise a _getattr
will be called.
def cmplx_func_A(e, C):
return 10*C*e
class Elipse():
def __init__(self, a=None, b=None, **kwargs):
self.relations = {
"e": {"req":["a", "b"], "func": lambda a,b: a+b},
"C": {"req":["e", "a"], "func": lambda e,a: e*1/(a*b)},
"A": {"req":["C", "e"], "func": lambda e,C: cmplx_func_A(e, C)},
"a": {"req":["e", "b"], "func": lambda e,b: e/b},
"b": {"req":["e", "a"], "func": lambda e,a: e/a}
}
self.a = a
self.b = b
self.e = None
self.C = None
self.A = None
if kwargs:
for key in kwargs:
setattr(self, key, kwargs[key])
def __getattribute__(self, attr):
val = super(Elipse, self).__getattribute__(attr)
if val: return val
return self._calc(attr)
def _calc(self, var):
requirements = self.relations[var]
value = self.relations[var]["func"](
*[self.__getattribute__(x) for x in requirements["req"]]
)
setattr(self, var, value)
return value
Oputput:
>>> a = Elipse(1,1)
>>> a.A #cal to calculate this will fall through
#and calculate every variable A depends on (C and e)
20
>>> a.C #C is not calculated this time.
1
>>> a = Elipse(1,1, e=3)
>>> a.e #without a __setattribute__ checking the validity, there is no
3 #insurance that this makes sense.
>>> a.A #calculates this and a.C, but doesn't recalc a.e
30
>>> a.e
3
>>> a = Elipse(b=1, e=2) #init can be anything that makes sense
>>> a.a #as it's defined by relations dict.
2.0
>>> a = Elipse(a=2, e=2)
>>> a.b
1.0
There is one more issue here, related to the next to last point in "the bad". I.e. let's imagine that we can can define an elipse with C
and A
. Because we can relate each variable with others over only 1 functional dependency, if you defined your variables a
and b
over e
and a|b
like I have, you won't be able to calculate them. There will always be at least some miniature subset of variables you will have to send. This can be alleviated by making sure you define as much of your variables over as little other variables you can but can't be avoided.
If you're lazy, this is a good way to short-circuit something you need done fast, but I wouldn't do this somewhere, where I expect someone else to use it, ever!
**kwargs
if they are present? (That's what your 1st attempt suggests, if that is all, then it is possible to make it more pythonic) – AndalusiaEllipse
object is immutable? This simplifies implementation, cause i.e. changinga
of existingEllipse
object is ambiguous - it influences other values but it's hard to guess if for exampleb
stays the same orA
stays the same. I'm asking because if we agree to use properties then we need to take it into account. And for "I guess at least one readability improvement would be...wasting both time and precision" - there is no way to keep consistency in over-defined scenario without calculating and comparing everything. – Ransonn
input variables of whichk
are required to determinem=n+l
outputs. – Washwoman