Last active
August 29, 2015 14:23
-
-
Save theodoregoetz/8f514950ef914d918fd1 to your computer and use it in GitHub Desktop.
Class disallowing reentry into methods when calculating properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.