0

In my python script I have defined a class similar to the following (admittedly bogus) class:

import copy


class Example:

    def __init__(self, a, b):

        self.a = a
        self.b = b

        self.__default__ = copy.deepcopy(self.__dict__)

        self.t = 0
        self.d = False

    def do(self):

        self.a += self.b - self.t
        self.t += 1
        if self.t == self.b:
            self.d = True
        return self.a

    def reset(self):

        self.__init__(**self.__default__)

Now, I would like to pass an instance of this class to my main function and repeatedly reset the instance to its default state. Despite having a look here, here, here and here, I couldn't get it going. The working example below gives the desired result, yet resets the instance in the main function explicitly. The dysfunctional example is one of my many tries to make it work using a reset method.

# working example:
def main(x):

    agg = []
    for i in range(x):
        klass = Example(1, 3)
        while not klass.d:
            a = klass.do()
            agg.append(a)
    return agg

# dysfunctional example:
def main2(klass, x):

    agg = []
    for i in range(x):
        klass.reset()
        while not klass.d:
            a = klass.do()
            agg.append(a)
    return agg

Then main(5) gives

res = main(5)
print(res)
>>> [4, 6, 7, 4, 6, 7, 4, 6, 7, 4, 6, 7, 4, 6, 7]

whereas

ex = Example(1, 3)  # default state
res = main2(ex, 5)
print(res)

throws the error: TypeError: __init__() got an unexpected keyword argument '__default__'

Since I would like to avoid having to re-instantiate the class in the main script anew for different reasons, I would be grateful if someone could help me out with the reset method.

apitsch
  • 1,532
  • 14
  • 31

3 Answers3

3

How about something like that:

class Example:

    def __init__(self, *args, **kwargs):
        """This stores the default state then init the instance using this default state"""
        self.__default_args__ = args
        self.__default_kwargs__ = kwargs
        self.init(*args, **kwargs)

    def do(self):
        """Do whatever you want """
        self.a += self.b - self.t
        self.t += 1
        if self.t == self.b:
            self.d = True
        return self.a

    def init(self, a, b):
        """Inits the instance to a given state"""
        self.a = a
        self.b = b
        self.t = 0
        self.d = False
        return self

    def reset(self):
        """Resets the instance to the default (stored) state"""
        return self.init(*self.__default_args__, **self.__default_kwargs__)
Guillaume
  • 5,497
  • 3
  • 24
  • 42
  • I don't see the point in allowing `*args` and `**kwargs` in `__init__` if you pass it all to a callable with the signature `init(self, a, b)` afterwards. Any keyword argument other than `a` or `b` will raise a `TypeError`. – j-i-l Oct 04 '18 at 14:33
  • Just for simplification. This way you only have to update the parameters in `init()` if you want more parameters to be handled. The `TypeError` would be there as well if you define the parameters in `__init__(self, a, b)` then call the methods with unexpected args. – Guillaume Oct 04 '18 at 14:40
  • Thanks for your answer. +1 as it works perfectly on the example I've given you. Yet, when I try to transfer the approach to my main project, `class.reset` returns a `NoneObject`. What could be reasons for that behavior? – apitsch Oct 04 '18 at 14:52
  • `.reset()` returns `None` if there is no explicit return clause. You seem to want `.reset()` to return the reset instance. Updated my answer – Guillaume Oct 04 '18 at 14:55
1

Here is an implementation using context manager:

class Example:
    def __init__(self, a, b):
        self.a = a
        self.b = b
        self.t = 0
        self.d = False

    def do(self):
        self.a += self.b - self.t
        self.t += 1
        if self.t == self.b:
            self.d = True
        return self.a

    def __enter__(self):
        self._a = self.a
        self._b = self.b

    def __exit__(self, *args):
        self.t = 0
        self.d = False
        self.a = self._a
        self.b = self._b

def main2(klass, x):
    agg = []
    for i in range(x):
        with klass:
            while not klass.d:
                a = klass.do()
                agg.append(a)
    return agg


ex = Example(1, 3)
res = main2(ex, 5)
print(res)
bboumend
  • 490
  • 3
  • 13
0

A reusable way to do this would be to implement a Resettable class to be inherited.

Resettable

class Resettable:
    def __init__(self, *args, **kwargs):
        self.__args__ = args
        self.__kwargs__ = kwargs

    def reset(self):
        self.__init__(*self.__args__, **self.__kwargs__)

Usage

Using a property to define an attribute that entirely depends on other attributes will also smoothen the process of resetting. This idea of having a single source of truth is generally a helpful paradigm for states that need to go back and forth in time.

class Example(Resettable):
    def __init__(self, a=0):
        self.a = a
        super().__init__(a)

    def do(self):
        self.a += 1
        return self.a

    @property
    def d(self):
        return self.a > 3 # or any other condition

def main(instance, x):
    agg = []
    for _ in range(x):
        instance.reset()
        while not instance.d:
            a = instance.do()
            agg.append(a)
    return agg

print(main(Example(), 3)) # [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]

The underlying assumption of the Resettable class is that the arguments passed to the constructor contain all the information needed to reset, using properties make that assumption easier to satisfy.

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73