Skip to content

Instantly share code, notes, and snippets.

@theodoregoetz
Last active August 29, 2015 14:23
Show Gist options
  • Save theodoregoetz/8f514950ef914d918fd1 to your computer and use it in GitHub Desktop.
Save theodoregoetz/8f514950ef914d918fd1 to your computer and use it in GitHub Desktop.
Class disallowing reentry into methods when calculating properties
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)
@theodoregoetz
Copy link
Author

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.

@theodoregoetz
Copy link
Author

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