17

("variables" here refers to "names", I think, not completely sure about the definition pythonistas use)

I have an object and some methods. These methods all need and all change the object's variables. How can I, in the most pythonic and in the best, respecting the techniques of OOP, way achieve to have the object variables used by the methods but also keep their original values for the other methods?

Should I copy the object everytime a method is called? Should I save the original values and have a reset() method to reset them everytime a method needs them? Or is there an even better way?

EDIT: I was asked for pseudocode. Since I am more interested in understanding the concept rather than just specifically solving the problem I am encountering I am going to try give an example:

class Player():
    games = 0
    points = 0
    fouls = 0
    rebounds = 0
    assists = 0
    turnovers = 0
    steals = 0

    def playCupGame(self):
        # simulates a game and then assigns values to the variables, accordingly
        self.points = K #just an example

    def playLeagueGame(self):
        # simulates a game and then assigns values to the variables, accordingly
        self.points = Z #just an example
        self.rebounds = W #example again

    def playTrainingGame(self):
        # simulates a game and then assigns values to the variables, accordingly
        self.points = X #just an example
        self.rebounds = Y #example again

The above is my class for a Player object (for the example assume he is a basketball one). This object has three different methods that all assign values to the players' statistics.

So, let's say the team has two league games and then a cup game. I'd have to make these calls:

p.playLeagueGame()
p.playLeagueGame()
p.playCupGame()

It's obvious that when the second and the third calls are made, the previously changed statistics of the player need to be reset. For that, I can either write a reset method that sets all the variables back to 0, or copy the object for every call I make. Or do something completely different.

That's where my question lays, what's the best approach, python and oop wise?

UPDATE: I am suspicious that I have superovercomplicated this and I can easily solve my problem by using local variables in the functions. However, what happens if I have a function inside another function, can I use locals of the outer one inside the inner one?

  • 4
    Could you explain your intent, the desired behaviour? Is it method chaining, like in jQuery? – Pēteris Caune Feb 01 '11 at 18:46
  • 1
    Perhaps some pseudocode would help explain why changing the object variables but changing them back at the end of the function is needed, as opposed to just using local variables. – tkerwin Feb 01 '11 at 19:28
  • I've read this a few times and I'm still not sure how to tell when a method will want to see updated member values and when it won't. This sounds like it needs an Agent, Facade, or MVC pattern. – Mike DeSimone Feb 01 '11 at 19:50

10 Answers10

8

Not sure if it's "Pythonic" enough, but you can define a "resettable" decorator for the __init__ method that creates a copy the object's __dict__ and adds a reset() method that switches the current __dict__ to the original one.

Edit - Here's an example implementation:

def resettable(f):
    import copy

    def __init_and_copy__(self, *args, **kwargs):
        f(self, *args)
        self.__original_dict__ = copy.deepcopy(self.__dict__)

        def reset(o = self):
            o.__dict__ = o.__original_dict__

        self.reset = reset

    return __init_and_copy__

class Point(object):
    @resettable
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return "%d %d" % (self.x, self.y)

class LabeledPoint(Point):
    @resettable
    def __init__(self, x, y, label):
        self.x = x
        self.y = y
        self.label = label

    def __str__(self):
        return "%d %d (%s)" % (self.x, self.y, self.label)

p = Point(1, 2)

print p # 1 2

p.x = 15
p.y = 25

print p # 15 25

p.reset()

print p # 1 2

p2 = LabeledPoint(1, 2, "Test")

print p2 # 1 2 (Test)

p2.x = 3
p2.label = "Test2"

print p2 # 3 2 (Test2)

p2.reset()

print p2 # 1 2 (Test)

Edit2: Added a test with inheritance

PaoloVictor
  • 1,296
  • 8
  • 18
  • 2
    In Python, they're called decorators, not annotations – Rafe Kettler Feb 01 '11 at 19:57
  • +1 Yes! I always mix them up - Funny thing is that while implementing I keep thinking "decorators", but typed "annotations" anyway. Yikes. – PaoloVictor Feb 01 '11 at 19:59
  • -1, Using `__dict__` as the source of all of an object's attributes is not reliable. It doesn't account for inheritance or metaclasses. – Apalala Feb 02 '11 at 01:50
  • @Apalala "Doesn't account for inheritance"? How exactly? Could you show me an example that show how this implementation is broken? I updated the code with a test that uses inheritance, and it seemly behaved ok. – PaoloVictor Feb 02 '11 at 12:27
  • `def __getattribute__(self,name): return 0` among other ways. See http://goo.gl/3hWLt. _See section Implementing Descriptors for another way in which attributes of a class retrieved via its instances may differ from the objects actually stored in the class’s __dict__. If no class attribute is found, and the object’s class has a __getattr__() method, that is called to satisfy the lookup._ – Apalala Feb 02 '11 at 19:07
  • 1
    -(-1) for trying harder. Note that it doesn't work for subclasses of some built-in types either, like in `class MyList(list): ...`. As the documentation for the Data Model says, `__dict__` is an implementation detail, and it's not always used. I'm not sure if the approach will work flawlessly even if you change the implementation to use the likes of `setattr`. The problem is that there's no generic way to know what to _reset_ an object means, so it has to be defined on a class by class basis. – Apalala Feb 02 '11 at 19:25
  • If other people have trouble with reset() throwing an exception if called more than once, see the answer I provided. – Brett Stottlemyer Jan 17 '17 at 05:13
  • If `B` inherits from `A` and I decorate `A.__init__()` with `@resettable`, should I expect `B` to be resetted correctly? – Teodoro Apr 09 '20 at 01:23
7

I'm not sure about "pythonic", but why not just create a reset method in your object that does whatever resetting is required? Call this method as part of your __init__ so you're not duplicating the data (ie: always (re)initialize it in one place -- the reset method)

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • +1, Although swapping out `__dict__` is clever and all, this is the most explicit (and therefore most pythonic IMHO) method. An immutable object is also a good option, but only if your methods don't need to change the values while calculating. – senderle Feb 02 '11 at 02:37
  • +1 Forget about _pythonic_. The meaning of `reset()` must be defined by the class that needs it. – Apalala Feb 02 '11 at 19:28
3

I would create a default dict as a data member with all of the default values, then do __dict__.update(self.default) during __init__ and then again at some later point to pull all the values back.

More generally, you can use a __setattr__ hook to keep track of every variable that has been changed and later use that data to reset them.

richo
  • 8,717
  • 3
  • 29
  • 47
2

Sounds like you want to know if your class should be an immutable object. The idea is that, once created, an immutable object can't/should't/would't be changed.

On Python, built-in types like int or tuple instances are immutable, enforced by the language:

>>> a=(1, 2, 3, 1, 2, 3)
>>> a[0] = 9
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

As another example, every time you add two integers a new instance is created:

>>> a=5000
>>> b=7000
>>> d=a+b
>>> d
12000
>>> id(d)
42882584
>>> d=a+b
>>> id(d)
42215680

The id() function returns the address of the int object 12000. And every time we add a+b a new 12000 object instance is created.

User defined immutable classes must be enforced manually, or simply done as a convention with a source code comment:

class X(object):
    """Immutable class. Don't change instance variables values!"""
    def __init__(self, *args):
        self._some_internal_value = ...

    def some_operation(self, arg0):
        new_instance = X(arg0 + ...)
        new_instance._some_internal_operation(self._some_internal_value, 42)
        return new_instance

    def _some_internal_operation(self, a, b):
        """..."""

Either way, it's OK to create a new instance for every operation.

vz0
  • 32,345
  • 7
  • 44
  • 77
2

See the Memento Design Pattern if you want to restore previous state, or the Proxy Design Pattern if you want the object to seem pristine, as if just created. In any case, you need to put something between what's referenced, and it's state.

Please comment if you need some code, though I'm sure you'll find plenty on the web if you use the design pattern names as keywords.

# The Memento design pattern
class Scores(object):
    ...

class Player(object):
    def __init__(self,...):
        ...
        self.scores = None
        self.history = []
        self.reset()

    def reset(self):
        if (self.scores):
            self.history.append(self.scores)
        self.scores = Scores()
Apalala
  • 9,017
  • 3
  • 30
  • 48
  • any decorator to solve it? because calling reset method on each class is verbose especially with class has many attributes – TomSawyer May 17 '19 at 10:12
  • @TomSawyer. In other answers there are decorators that make copies of the object's `__dict__`. I think they are too broad in scope, but there's an idea there. Copying only attributes that don't start with "_", etc., may work. It all depends on the semantics of the problem at hand. – Apalala May 17 '19 at 14:56
1

I liked (and tried) the top answer from PaoloVictor. However, I found that it "reset" itself, i.e., if you called reset() a 2nd time it would throw an exception.

I found that it worked repeatably with the following implementation

def resettable(f):
    import copy

    def __init_and_copy__(self, *args, **kwargs):
        f(self, *args, **kwargs)
        def reset(o = self):
            o.__dict__ = o.__original_dict__
            o.__original_dict__ = copy.deepcopy(self.__dict__)
        self.reset = reset
        self.__original_dict__ = copy.deepcopy(self.__dict__)
    return __init_and_copy__
Brett Stottlemyer
  • 2,734
  • 4
  • 26
  • 38
1

It sounds like overall your design needs some reworking. What about a PlayerGameStatistics class that would keep track of all that, and either a Player or a Game would hold a collection of these objects?

Also the code you show is a good start, but could you show more code that interacts with the Player class? I'm just having a hard time seeing why a single Player object should have PlayXGame methods -- does a single Player not interact with other Players when playing a game, or why does a specific Player play the game?

user470379
  • 4,879
  • 16
  • 21
1

A simple reset method (called in __init__ and re-called when necessary) makes a lot of sense. But here's a solution that I think is interesting, if a bit over-engineered: create a context manager. I'm curious what people think about this...

from contextlib import contextmanager

@contextmanager
def resetting(resettable):
    try:
        resettable.setdef()
        yield resettable
    finally:
        resettable.reset()

class Resetter(object):
    def __init__(self, foo=5, bar=6):
        self.foo = foo
        self.bar = bar
    def setdef(self):
        self._foo = self.foo
        self._bar = self.bar
    def reset(self):
        self.foo = self._foo
        self.bar = self._bar
    def method(self):
        with resetting(self):
            self.foo += self.bar
            print self.foo

r = Resetter()
r.method()    # prints 11
r.method()    # still prints 11

To over-over-engineer, you could then create a @resetme decorator

def resetme(f):
    def rf(self, *args, **kwargs):
        with resetting(self):
            f(self, *args, **kwargs)
    return rf

So that instead of having to explicitly use with you could just use the decorator:

@resetme
def method(self):
    self.foo += self.bar
    print self.foo
senderle
  • 145,869
  • 36
  • 209
  • 233
  • 1
    I think it's too complex. `def reset(self):...` is completely obvious without requiring a bunch of extra code. Anything else just adds complexity for no gain IMO. – Bryan Oakley Feb 02 '11 at 18:37
  • Well I think there is some gain, in that rather than returning to some fixed state that cannot be changed easily, the values of foo and bar return to their state from just before the method was called. If you want this functionality in a large class with lots of methods, this technique saves you from having to copy state all the time. Still, I mostly agree with you :). I just thought it was a fun exercise. – senderle Feb 02 '11 at 19:15
0

thanks for the nice input, as I had kind of a similar problem. I'm solving it with a hook on the init method, since I'd like to be able to reset to whatever initial state an object had. Here's my code:

import copy
_tool_init_states = {}

def wrap_init(init_func):
    def init_hook(inst, *args, **kws):
        if inst not in _tool_init_states:
            # if there is a class hierarchy, only the outer scope does work
            _tool_init_states[inst] = None
            res = init_func(inst, *args, **kws)
            _tool_init_states[inst] = copy.deepcopy(inst.__dict__)
            return res
        else:
            return init_func(inst, *args, **kws)
    return init_hook

def reset(inst):
    inst.__dict__.clear()
    inst.__dict__.update(
        copy.deepcopy(_tool_init_states[inst])
    )

class _Resettable(type):
    """Wraps __init__ to store object _after_ init."""
    def __new__(mcs, *more):
        mcs = super(_Resetable, mcs).__new__(mcs, *more)
        mcs.__init__ = wrap_init(mcs.__init__)
        mcs.reset = reset
        return mcs

class MyResettableClass(object):
    __metaclass__ = Resettable
    def __init__(self):
        self.do_whatever = "you want,"
        self.it_will_be = "resetted by calling reset()"

To update the initial state, you could build some method like reset(...) that writes data into _tool_init_states. I hope this helps somebody. If this is possible without a metaclass, please let me know.

Heiner
  • 1
0

It sounds to me like you need to rework your model to at least include a separate "PlayerGameStats" class.

Something along the lines of:

PlayerGameStats = collections.namedtuple("points fouls rebounds assists turnovers steals")

class Player():
    def __init__(self):
        self.cup_games = []
        self.league_games = []
        self.training_games = []

def playCupGame(self):
    # simulates a game and then assigns values to the variables, accordingly
    stats = PlayerGameStats(points, fouls, rebounds, assists, turnovers, steals)
    self.cup_games.append(stats)

def playLeagueGame(self):
    # simulates a game and then assigns values to the variables, accordingly
    stats = PlayerGameStats(points, fouls, rebounds, assists, turnovers, steals)
    self.league_games.append(stats)

def playTrainingGame(self):
    # simulates a game and then assigns values to the variables, accordingly
    stats = PlayerGameStats(points, fouls, rebounds, assists, turnovers, steals)
    self.training_games.append(stats)

And to answer the question in your edit, yes nested functions can see variables stored in outer scopes. You can read more about that in the tutorial: http://docs.python.org/tutorial/classes.html#python-scopes-and-namespaces

ncoghlan
  • 40,168
  • 10
  • 71
  • 80