intellimath / recordclass

Mutable variant of namedtuple -- recordclass, which support assignments, compact dataclasses and other memory saving variants.
Other
14 stars 3 forks source link

Allow an option to store/reset to initial values. #4

Closed hulsed closed 2 months ago

hulsed commented 8 months ago

What I would like to do is have a class that (in addition to default arguments) that has initial arguments which can be accessed and reset to if needed.

For example, if I do:

class ExampleState(dataobject, keep_initial=True):
     x : float=1.0
     y : float=2.0

ex = ExampleState(3.0, 2.0)
ex.x = 4.0

I'd like to be able to get back to or access the initial value somehow, e.g.:

>>> ex.__initial__['x']
3.0
or 
>>> ex.reset()
>>> ex.x
3.0

This would be really helpful for modelling dynamical systems, where sometimes we might want to reset to an initial state to run an additional simulation.

intellimath commented 8 months ago

You can write as follows:

  >>> ex.__defaults__['x']
  3.0
  >>> ex.x = ex.__defaults__['x']
  >>> ex.x
  3.0
intellimath commented 8 months ago

May be the following prototype for reset could be solve the problem:

def reset(o, name):
    from recordclass._dataobject import Factory

    copy_default = o.__options__['copy_default']
    val = o.__defaults__[name]
    if val is not None:
        if type(val) is Factory:
            val = Factory(val)
        elif copy_default:
            if isinstance(val, list):
                val = val[:]
            elif isinstance(val, (dict, set)):
                val = val.copy()
            elif hasattr(o, '__copy__'):
                val = val.__copy__():

    setattr(o, name, val)

This recipe should work starting 0.21.1

hulsed commented 8 months ago

The issue with using __defaults__ is that the dict is a class variable. So If I have two different initial values in two different objects, both will reset to the same value instead of their independent values.

Ideally there should be an option to set it as a (ideally immutable post-object instantiation) object variable.

intellimath commented 8 months ago

So there is a case when class has user defined __init__ and you want to reset attributes to their initial values which they have after first instance initialization?

hulsed commented 8 months ago

Something like that. Several of the classes I've build off of recordclass have a defined __init__ but the goal would be to hold the initial field values rather than those custom arguments.

The idea would be that if I do something like:

ex1 = ExampleState(3.0, 2.0)
ex2 = ExampleState(4.0, 1.0)

I could then get some independent, hidden property that I could reset back to, e.g.:

>>> ex1.__initial__
(3.0, 2.0)
>>> ex2.__initial__
(4.0, 1.0)

I'd imagine this sort of thing would add considerable memory overhead, so it should be an optional feature obviously.

intellimath commented 8 months ago

Is this recipe suitable for the problem?

from recordclass import dataobject, asdict, update

class State:

    def get_state(self):
        return asdict(self)

    def set_state(self, state):
        update(self, **state)

class Point(dataobject, State):
    x:int
    y:int

a = Point(1,2)
state = a.get_state()
print(a)
a.x = 100
a.y = 200
print(a)
a.set_state(state)
print(a)
hulsed commented 8 months ago

Yes, but I would like to be able to have state be a property of the class so it is all contained in a single object.

intellimath commented 8 months ago

Here is another variant:

class State:

    def save_state(self):
        self.__state__ =  {nm:getattr(self, nm) for nm in self.__fields__ if nm != '__state__'}

    def restore_state(self):
        for nm,val in self.__state__.items():
            setattr(self, nm, val)

class Point(dataobject, State):
    x:int
    y:int
    __state__:dict = None

a = Point(1,2)
a.save_state()
print(a)
a.x = 100
a.y = 200
print(a)
a.restore_state()
print(a)
intellimath commented 8 months ago

Another variant with state as tuple:

class State:
    def save_state(self):
        self._state =  tuple(getattr(self, nm) for nm in self.__fields__[:-1])

    def restore_state(self, state):
        for nm, val in zip(self.__fields__[:-1], self._state):
            setattr(self, nm, val)

class Point(dataobject, State):
    x:int
    y:int
    _state:tuple

a = Point(1,2)
print(a)
state = a.save_state()
print(a)
a.x = 100
a.y = 200
print(a)
a.restore_state(state)
print(a)
intellimath commented 7 months ago

One can also save instance states in the class attribute. For example:

from typing import ClassVar

class State:

    __instance_states__: ClassVar[dict] = {}

    @classmethod
    def clear_states(cls):
        cls.__instance_states__ = {}

    def del_state(self):
        if id(self) in self.__instance_states__:
            del self.__instance_states__[id(self)]

    def save_state(self):
        self.__instance_states__[id(self)] =  tuple(getattr(self, nm) for nm in self.__fields__)

    def restore_state(self):
        _state = self.__instance_states__.get(id(self), ())
        for nm, val in zip(self.__fields__, _state):
            setattr(self, nm, val)

class Point(dataobject, State):
    x:int
    y:int

print(Point.__mro__)
a = Point(1,2)
print(a)
a.save_state()
print(a)
a.x = 100
a.y = 200
print(a)
a.restore_state()
print(a)
hulsed commented 7 months ago

This is all helpful, I will need to investigate which will work best for my use-case

intellimath commented 7 months ago

One addition to the previous post. It's possible to use __del__ method for automatic cleanup saved state. For example, one can add __del__:

from typing import ClassVar

class State:

    __instance_states__: ClassVar[dict] = {}

    @classmethod
    def clear_states(cls):
        cls.__instance_states__ = {}

    def del_state(self):
        if id(self) in self.__instance_states__:
            del self.__instance_states__[id(self)]

    def save_state(self):
        self.__instance_states__[id(self)] =  tuple(getattr(self, nm) for nm in self.__fields__)

    def restore_state(self):
        _state = self.__instance_states__.get(id(self), ())
        for nm, val in zip(self.__fields__, _state):
            setattr(self, nm, val)

    def __del__(self):
        if id(self) in self.__instance_states__:
            del self.__instance_states__[id(self)]

class Point(dataobject, State):
    x:int
    y:int

a = Point(1,2)
print(a)
a.save_state()
print(State.__instance_states__)
print(a)
a.x = 100
a.y = 200
print(a)
a.restore_state()
print(a)
del a
print(State.__instance_states__)
hulsed commented 2 months ago

I've found my own solution to this that doesn't involve the data structure itself. In my use-case, it makes more sense to hold these initial non-default values externally as opposed to making them a part of the recordclass itself.