-
-
Save theodoregoetz/8f514950ef914d918fd1 to your computer and use it in GitHub Desktop.
class LockError(Exception): | |
def __init__(self,call_stack): | |
self.call_stack = call_stack[:] | |
def __str__(self): | |
msg = 'Not enough information.\n' | |
msg += ' call stack:\n ' | |
msg += '\n '.join(self.call_stack) | |
return msg | |
class OverspecifiedError(Exception): | |
def __init__(self,property_name): | |
self.property_name = property_name | |
def __str__(self): | |
return 'Overspecified with \''+self.property_name+'\'.' | |
class locked_property(object): | |
""" | |
A read-only @property that is only evaluated once. | |
And can only be entered once (locked). | |
""" | |
def __init__(self, fget, doc=None): | |
self.__doc__ = doc or fget.__doc__ | |
self.__name__ = fget.__name__ | |
self.locked_fget = self.lock(fget) | |
def __get__(self, obj, cls): | |
if obj is None: | |
return self | |
if not hasattr(obj,'_calculated_properties'): | |
obj._calculated_properties = list() | |
if self.__name__ not in obj.__dict__: | |
obj.__dict__[self.__name__] = self.locked_fget(obj) | |
obj._calculated_properties.append(self.__name__) | |
return obj.__dict__[self.__name__] | |
def __set__(self, obj, val): | |
if self.__name__ in obj.__dict__: | |
self.__delete__(obj) | |
try: | |
getattr(obj,self.__name__) | |
raise OverspecifiedError(self.__name__) | |
except LockError: | |
obj.__dict__[self.__name__] = val | |
def __delete__(self, obj): | |
if hasattr(obj,'_calculated_properties'): | |
for p in obj._calculated_properties: | |
if p is not self.__name__: | |
del obj.__dict__[p] | |
obj._calculated_properties.clear() | |
del obj.__dict__[self.__name__] | |
def lock(self,fn): | |
def locked_fget(obj): | |
if not hasattr(obj,'call_stack'): | |
obj.call_stack = list() | |
if self.__name__ in obj.call_stack: | |
raise LockError(obj.call_stack) | |
else: | |
obj.call_stack.append(self.__name__) | |
try: | |
val = fn(obj) | |
finally: | |
obj.call_stack.remove(self.__name__) | |
return val | |
return locked_fget | |
import numpy as np | |
sqrt = np.sqrt | |
class RightTriangle(object): | |
''' a**2 + b**2 = c**2 ''' | |
def __init__(self): | |
super().__init__() | |
@locked_property | |
def a(self): | |
b = self.b | |
c = self.c | |
a = sqrt(c**2 - b**2) | |
return a | |
@locked_property | |
def b(self): | |
a = self.a | |
c = self.c | |
b = sqrt(c**2 - a**2) | |
return b | |
@locked_property | |
def c(self): | |
try: | |
a = self.a | |
b = self.b | |
c = sqrt(a**2 + b**2) | |
except LockError: | |
d = self.d | |
c = 2*d | |
return c | |
@locked_property | |
def d(self): | |
c = self.c | |
d = c / 2 | |
return d | |
print('>>> calc = RightTriangle()') | |
calc = RightTriangle() | |
# prints: not enough information. call stack: a, b | |
try: | |
print('>>> calc.a') | |
print(calc.a) | |
except LockError as e: | |
print(e) | |
print('>>> calc.a = 3') | |
calc.a = 3 | |
print('>>> calc.b = 4') | |
calc.b = 4 | |
print('>>> print(a,b,c,d)') | |
print(calc.a,calc.b,calc.c,calc.d) | |
try: | |
print('>>> calc.c = 3') | |
calc.c = 3 | |
except OverspecifiedError as e: | |
print(e) | |
print('>>> calc.b = 5') | |
calc.b = 5 | |
print('>>> print(a,b,c,d)') | |
print(calc.a,calc.b,calc.c,calc.d) | |
print('\n>>> calc = RightTriangle()') | |
calc = RightTriangle() | |
print('>>> calc.a = 3') | |
calc.a = 3 | |
print('>>> calc.b = 2.5') | |
calc.d = 2.5 | |
print('>>> print(a,b,c,d)') | |
print(calc.a,calc.b,calc.c,calc.d) |
Abstracted all get/set attribute methods to a base class. Simplifying the calculator class. Synopsis of class functionality: There are three parameters (a,b,c). If I set any two of them, the third can be calculated. After that, setting the third parameter will throw an OverspecifiedError exception - regardless whether it is mathematically consistent. Also, if only one parameter is set and another is requested, a LockError exception is thrown indicating an infinite loop in the calculation.
total rewrite to make a single @locked_property decorator. Same functionality as before but with a little less code and quicker execution with some help from this reddit post.
Added:
locked_property._calculated_properties = list()
to keep track of those properties that were calculated as the result of a call to __get__()
. These will all get deleted if any other property is deleted. This makes setting properties rather expensive because every time the user sets a property, it is preemptively deleted and the class tries to calculate it, an OverspecifiedError
exception is thrown if successful.
Moved the lock/unlock to their own functions using inspect to get the caller's name. Maybe a little more elegant(?)