Expected behavior of objects after save/load game - between released game versions

Abacus_Games

Newbie
Dec 11, 2022
20
26
Hi,

I am trying to understand the expected behavior of created objects when you save your game in one version, then expand some functionality in the class the object is based on, release a new game version, load the previous saved game and see what works and what not.

My issue is that when you release a game in versions your code is a work in progress and evolve over time - you rarely get all attributes, properties and methods perfect from the beginning (at least not I that is not a professional coder in any way and just develop for fun in my spare time).

I have included a simple test snippet below and from that I have so far deducted that between releasing versions...
  • I can't add additional attributes as these are not recognized after loading a previous save
  • I can't add additional methods as these are not recognized after loading a previous save
  • I can change the code of properties (and probably methods as well although I haven't tested this yet) after loading a previous save
I guess this is the expected behavior although this make it much harder to keep and maintain save game/version compatibility if you wish to evolve your code during development. (Which for me is annoying since I want to code smarter once I learn something new).

I guess you could use the label after_load to manually convert/upgrade/transfer objects or object data but that seems tedious - as for example I have hundreds of objects (actors, events, quests).

Is this understanding correct or am I missing something fundamental?

This thread seems to confirm this.

However, is there a way to add new methods to old objects as well - as is done with the attributes? How can I connect a new method to an old object - just by assigning a function?
Possibly, you could add a few "dummy" methods that is reserved for future use but you wouldn't be able to change their method names when calling them!? number of arguments!?


Python:
# Testing object attribute, property and method changes after save/load

default adam = Actor("Adam")

init python:

    class Actor(store.object):

        def __init__(self, name):

            self.name = name
            # self.age = 18

            self.c = Character(name)

        @property
        def name(self):
            # return "{color=#FF0000}" + self._name + "{/color}"
            return self._name

        @name.setter
        def name(self, value):
            self._name = value

        # def get_age(self):
        #    return self.age


# Testing

label start:

adam.c "This is the start."
adam.c "Save the game here in version 1"

# Saving - Then change code (commenting), restart and load saved game

# Loading for version 2

adam.c "Is my name in red or not : [adam.name]."
adam.c "Do I have a default age?"

python:

    try:
        if adam.age:
            renpy.say(adam.c, "I am [adam.age] years old")
    except:
        renpy.say(adam.c, "I don't have an age.")
        # temp_age = adam.get_age()
        # renpy.say(adam.c, "And I can't use added functions to get it (this will fail) : [temp_age] years old.")


adam.c "Now back to the menu."
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,266
15,081
I can't add additional attributes as these are not recognized after loading a previous save
Only half true.


The attributes will not exist, because the loading phase do not trigger the __init__ method. Yet, both Python and Ren'Py explicitly have a way to solve that issue.

At Python level it's done through the magic method:
Python:
class whatever():
    def __setstate__( self, state ):
        if not "newAttribute" in state:
            state["newAttribute"] = defaultValue
        self.__dict__.update( state )
At Ren'Py level, it's done through the special label:
Python:
label after_load:
    if not hasattr( object, "newAttribute" ):
        $ object.newAttribute = defaultValue
or
Python:
label after_load:
    if savedVersion <= currentVersion:
        python:
            for obj in [ LIST OF OBJECTS ]:
                obj.newAttribute = defaultValue
Which approach to use depend on how many objects there's to update, and how different the default value are.
If it's the same value for all object, or if the default value can be easily computed, then the Python approach is better ; the same code will update all objects from the given class.
If the default value have to differ depending on the object, then the Ren'Py approach is better, because working at object level, and not at class level like for the Python one.


I can't add additional methods as these are not recognized after loading a previous save
False.

Objects are not included in the save file, only their class, as well as their attributes and their respective values, are saved. Therefore, when you load a save file, the object is recreated, and will have all the method that exist at than moment.

Python:
init python:

    class MyClass():
        def pikaboo( self ):
            renpy.notify( "Hey, it's me, Joey !" )

default myObject = MyClass()

label start:

    "blablabla"
    $ myObject.pikaboo()
    "bliblibli"
    "Save here, quit Ren'py, edit the code, add the 'imNewHere' method, then continue from your save file."
    "Ready to see the magic ?"
    $ myObject.imNewHere()
    "Oh, it works !"
After you've saved and quited Ren'Py (but not the SDK, no need to quit it), add this to the "MyClass" class:
Python:
        def imNewHere( self ):
            renpy.notify( "Here I am, J.H." )

I can change the code of properties (and probably methods as well although I haven't tested this yet) after loading a previous save
True, for the reasons explained right above.


I guess you could use the label after_load to manually convert/upgrade/transfer objects or object data but that seems tedious - as for example I have hundreds of objects (actors, events, quests).
Well, as the saying goes, "days of struggling save you from hours of planing". And if it's not a saying, then it should become one.



Python:
# Testing object attribute, property and method changes after save/load
Broken tests lead to false assumptions.

Python:
default adam = Actor("Adam")

init python:

    class Actor(store.object):

        def __init__(self, name):

            # God, even this is broken...
            #self.name = name
            self._name = name

            # self.age = 18

            self.c = Character(name)

        @property
        def name(self):
            # return "{color=#FF0000}" + self._name + "{/color}"
            return self._name

        @name.setter
        def name(self, value):
            self._name = value

        # def get_age(self):
        #    return self.age


label start:
    adam.c "This is the start."
    adam.c "Save the game here in version 1"

    adam.c "Is my name in red or not : [adam.name]."
    adam.c "Do I have a default age?"

    if hasattr( adam, "age" ):
        adam.c "I'm [adam.age] years old"
    elif hasattr( adam, "get_age" ):
        adam.c "I could tell you my age, because I have a method for this. But right now it would lead to an exception telling my developer that I don't have an 'age' attribute."

    adam.c "Now back to the menu."
 

Abacus_Games

Newbie
Dec 11, 2022
20
26
Thank you. That helps. Although I guess there is no substitute for planning and knowing your stuff. Thankfully, learning never ends.

I'm curious of your comment "# God, even this is broken..." though as I want to learn and understand why you commented that?

I see you changed to set the protected attribute directly while I (at least my current understanding of it) used a property to set the protected attribute. I didn't use any validation though so it's rather useless (well, unnecessary) in my example but included it since I use the getter to change color.

Is it considered bad practice to use properties to set attributes from within the __init__ constructor or am I missing your point?
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,266
15,081
I'm curious of your comment "# God, even this is broken..." though as I want to learn and understand why you commented that?

I see you changed to set the protected attribute directly while I (at least my current understanding of it) used a property to set the protected attribute.
Well...

Firstly, one leading underscore do not mean that the attribute is protected, but that it is private. It's a leading double underscore that turn an attribute into a protected one.

Secondly, "private attribute" is just a naming convention, anyone can still address and change its value, from inside and outside of the object. Unlike "protected attributes", that goes further than on convention, but still just make the access to the attribute a bit harder, not impossible.

Thirdly, "__name" (protected attribute), "_name" (private attribute) and "name" (public attribute) are three different attributes, not three different ways to address a single one.

Fourthly, it (above point) is why I said that it's broken. In the __init__ method, you create a "name" attribute, while everywhere else in the class, you address a "_name" attribute that do not exist.

Fifthly, methods and properties are just attributes to which a code is assigned as value.

Sixthly, methods and properties are assigned when the object is created (__new__), therefore attributes created when the object is finally initialized (__init__) can possibly override methods and/or properties, as well as class level attributes.

Seventhly, it (above point) is also why I said that it's broken. By assigning a value to "name", you override the whole "name" property. What make the code works, but by hiding the bug, not by fixing it.
 
  • Like
Reactions: gojira667

Abacus_Games

Newbie
Dec 11, 2022
20
26
Thanks. It's late here so will go through the list tomorrow.


Well...

Firstly, one leading underscore do not mean that the attribute is protected, but that it is private. It's a leading double underscore that turn an attribute into a protected one.

Secondly, "private attribute" is just a naming convention, anyone can still address and change its value, from inside and outside of the object. Unlike "protected attributes", that goes further than on convention, but still just make the access to the attribute a bit harder, not impossible.
However, I believe this is the other way around. Private is the more limited one, only accessible from within the object. At least in the books and tutorials I've seen.

Protected is one "_" and private is two "__".
 

Abacus_Games

Newbie
Dec 11, 2022
20
26
I see what you are saying but I must admit that I don't see the implications of it or why this is bad/problem.

I'm not saying it's right (I'm way too hobbyist for that) but my way of implementing validation(*) with a property (using a protected attribute "_" in the property code) in the constructor seems to be widespread and commonly used, taught and recommended by python coders and tutorials.

(*) I don't have validation in my code as it was just an example but the "structure" of the code would be the same.

You could have the validation code in the constructor itself and use the protected (or private) attribute directly but if you want properties for external access anyway for your attributes that would mean you would repeat yourself - DRY - so letting the constructor use the property seems like a good idea - even if it is as you say an override.
 

a82912

New Member
Oct 19, 2020
4
1
The public/private distinction would be true in several other languages, but it's purely hypothetical in python. You can test this for yourself,
Code:
>>> class Foo:
...     def __init__(self):
...             self._bar = "baz"
...
>>> foo = Foo()
>>> foo._bar
'baz'
>>> foo._bar = "qux"
>>> foo._bar
'qux'
>>>
Python is generally extremely loosey-goosey compared to typed languages (eg java etc) and while it's a lot better about not actively doing dumb stuff than JS, if you go out of your way to do something dumb, it will almost never stop you.

In contrast dunders are special, they have an actual syntactic meaning unlike a single underscore. The exact details have some slightly esoteric implications, but if you want to look into it the key phrase is "name mangling"
 

a82912

New Member
Oct 19, 2020
4
1
Notably overriding attributes that were set using property decorators during instantiation may have effects you aren't expecting because the attribute is not its simple value. Eg

Code:
class Foo:

@property
def bar(self):
    """Whatever"""
    return self._bar
is NOT the same as just having

Code:
class Foo:

def __init__(self, bar):
    self._bar = bar
And is yet again distinct from

Code:
class Foo:

def __init__(self, bar):
    self.bar = bar
When you use a [USER=2111498]property[/USER] decorator, your object will have

foo.bar

AND

foo._bar

after initialization, but it will ALREADY have foo.bar after you finish instantiation and BEFORE you call __init__ (someone pedantic should DM me about whether python distinguishes between intialization and instantiation because they probably have nothing better to do at 2 am on a saturday either)

The first will be a method, and the second will be a field (or non-callable data if you will). If you then assign something to foo.bar DURING initialization, you will (edit: ATTEMPT to) override the thing that is already at foo.bar, which was a method. Hence problems

Edit: This actually depends on if you already defined a setter, but it's going to error out if you have or haven't. If there's no setter, foo.bar will simply error out because it does indeed act as a protected attribute (but foo._bar doesn't), but if you have a setter for foo.bar, this will actually go into an infinite recursion. Happily in python that doesn't generally blow up your computer.
 
Last edited:
  • Yay, new update!
Reactions: gojira667