Ren'Py Keeping class instances readable (Solved)

GNVE

Active Member
Jul 20, 2018
681
1,150
First off I know I am way out of my depth but I'm just dicking around and learning a lot about Ren'Py and Python in general. I tried to google an answer but I'm not sure how to word the question exactly so no luck there.

The problem I'm running into is that the classes I'm writing contain 'a lot' of data. So an instance of said class isn't very human readable anymore. Now I already got that splitting classes can help a lot (and started doing that naturally.) But is there a better way to keep oversight of what I'm doing?
My idea was to create a loader for the class instances that reads a text file where each line is one of the class variables but maybe there are better ways of doing things.

as an example:
Python:
#The class government currently contains the following variables
class government:
        def __init__(self, name, glevel, corruption, influence, prestige, base, mod, policies, upgrades):
           
#The (uncompleted) instance for the government looks like this:
default town = government('Virgin Bay','town',0,0,-1000.0,{'population':9999, 'previousAS':0, 'previousAW':0, 'previousDS':0, 'edugrant':0, 'churchgrant':5000, 'otherexpenses':25.01},{'population':1.0004, 'poptax':0.04, 'bsnstax':0.025, 'edutax':0.05, 'churchtax':0.00, 'churches':['protestant','catholic','nondiscript']},[],[])
As you can see above the instance is already becoming a little unwieldy and there are still two empty lists in there and the class may expand as well. Other classes might be bigger still. (especially if they contain text like a description or the like)

So would it be smart to writ a text file like below with a loader or is there a better way of doing things that I don't know about due to inexperience?
Code:
town
government
'Virgin Bay'
'town'
0
0
-1000.0
{'population':9999, 'previousAS':0, 'previousAW':0, 'previousDS':0, 'edugrant':0, 'churchgrant':5000, 'otherexpenses':25.01}
{'population':1.0004, 'poptax':0.04, 'bsnstax':0.025, 'edutax':0.05, 'churchtax':0.00, 'churches':['protestant','catholic','nondiscript']}
[]
[]
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,299
15,167
[Note: It's a bit late, don't hesitate if there's things that I'll not explain clearly]

My idea was to create a loader for the class instances that reads a text file where each line is one of the class variables but maybe there are better ways of doing things.
If I understand right, your main issue is the too many arguments when creating an object.
But thankfully, there's ways to solve this issue.


Your arguments are all positional (what variable get what value depend on the position of the value). It's useful when you've a short list of arguments, but as you witnessed, it become a problem when the list grow.
To counter this, prefer named/keyword arguments (a couple name/value) every time it make sense.

Positional argument declaration: def __init__( argument1, argument2, argument3 )
Positional argument use: var = MyClass( value1, value2, value3
Named argument declaration: def __init__( argument1=defaultValue1, argument2=defaultValue2, argument3=defaultValue3 )
Named argument use: var = MyClass( argument2=value2, argument1=value1, argument3=value3 )

As you can see, each name argument have a default value. If an argument do not get an explicit value when the function/method is called, then it will get that default value.
Python:
init python:
    def testFnct( arg1=None, arg2="Default" ):
        print( "arg1 is " + str( arg1 ) )
        print( "arg2 is " + str( arg2 ) )
If you use the console to call that function, testFnct( 1, 2 ) will print:
arg1 is 1
arg2 is 2
Side note: Note that when the position of the value match the position of the argument, you can make them works like positional argument. But I do not recommend to do it, because it's more confusing that helping. I did it only for the demonstration.

but testFnct( arg2="Changed" ) will print:
arg1 is None
arg2 is Changed
and testFnct() will print:
arg1 is None
arg2 is Default
For each one of your arguments, there's surely a value that is (or will be) used often. It's its default value.
That way, each time an object have to assign that value, you can just skip it. This will shorten a bit the call.


The other advantage when using named argument is precisely it's readability. Especially when you keep in mind that you can perfectly split a line.

Let's say that your __init__ declaration only use named arguments, you can perfectly have this:
Python:
var = government( name = "Evil ones", 
                  glevel = "town",
                  corruption = 100,
                  influence = 50
                  prestige = 10
                  base = { ... }
                  mod = {...}
                  policies = [ "unique party", "curfew" ],
                  upgrades = [ ... ] )
What is perfectly manageable by a human.


Now, this being said, generally declarations use a mix between positional and named arguments. The first ones being used for any value expected to be unique for an object, while the second ones are used for anything that tweak the object.
So, in your case, it would looks like:
def __init__( self, name, glevel="town", corruption=50, influence=50, prestige=50, [...] )


Now, another thing that you can use, is dictionary unpacking.
When you've something like myDict = { "a": 1, "b": 2, "c": 3 } you can turn it into a suit of named arguments through the use of **.

In your class, there's three arguments that will probably have common values shared between many governments: "corruption", "influence" and "prestige".
So, you can have generic description for them:
Python:
averageDemocracy = { "corruption": 30, "influence": 50, "prestige": 50 }
averageDictatorship = { "corruption": 90, "influence": 30, "prestige": 30 }
Then you can simplify a bit more the creation of an object:
Python:
var = government( "Evil ones",  # Name is a positional argument.
                  #glevel = "town",  # "town" is the default value, you can skip it.
                  base = { ... },
                  mod = { ... },
                  policies = [ "unique party", "curfew" ],
                  upgrades = [ ... ],
                  #  Finally add /corruption/, /influence/ and /prestige/ values
                  # as they are defined by the /averageDictatorship/ dict.
                  **averageDictatorship )

Finally, in your example, I would also use a "false default value" approach for your two dictionaries arguments:
Python:
def __init__( [...] ):
    [...]

    # Not everything can be defaulted.
    if not "population" in base: raise ValueError( "You MUST define at least a base population value" )
    # I used 0 as default value, but you can obviously use whatever you want.
    if not "previousAS" in base: base["previousAS"] = 0
    if not "previousAW" in base: base["previousAW"] = 0
    if not "previousDS" in base: base["previousDS"] = 0
    if not "edugrant" in base: base["edugrant"] = 0
    # You can even make a default value depend on another value, passed or defaulted.
    if not "churchgrant" in base: base["churchgrant"] = int( base["polulation"] / 2 )
    if not "otherexpenses" in base: base["otherexpenses"] = 0
This would also permit to simplify your declaration.


In the end, your example would now looks like:
Python:
init python:
    class government:
        def __init__(self, name, glevel="town", corruption=10, influence=50, prestige=50, base={}, mod={}, policies=[], upgrades=[]):
             [...]

    #  There's possibility of conflict between /define/ and /default/, so create this in the /init/ block.
    # Being declared here and not changed, it shouldn't be part of savable values.
    badNeighborhoodDemocracy = { "corruption": 0, "influence": 0, "prestige": -1000.0 }

default town = government( 'Virgin Bay',
                           base = { "population": 9999, "otherexpenses": 25.01 },
                           mod = { [...] }, # I haven't made example for the default values
                           **badNeighborhoodDemocracy )
What is way simpler than what you have actually and don't need that you rely on an external file for the definition.


Side note:
Relying on an external file wouldn't effectively solve your issue, because it isn't effectively more readable. You just moved the issue, passing from:
Python:
default town = government( 'Virgin Bay',
                           'town',
                           0,
                           0,
                           -1000.0,
                           {'population':9999, 'previousAS':0, 'previousAW':0, 'previousDS':0, 'edugrant':0, 'churchgrant':5000, 'otherexpenses':25.01},
                           {'population':1.0004, 'poptax':0.04, 'bsnstax':0.025, 'edutax':0.05, 'churchtax':0.00, 'churches':['protestant','catholic','nondiscript']},
                           [],
                           [] )
to something that would looks the same in the file, but would have to be imported in a way or another.
Would you be able, in few months, to tell that the first "0" is the corruption, and the second the "influence" without having to look back at the "__init__()" declaration ?
 
  • Like
Reactions: GNVE and gojira667

Satori6

Game Developer
Aug 29, 2023
461
874
So an instance of said class isn't very human readable anymore.
Your example looks perfectly readable to me.

If you want to make it slightly easier to tell what each value means, you could just store them like this:

Code:
govs={
    'Virgin Bay': {
        'name':'Virgin Bay',
        'glevel': 'town',
        'corruption': 0,
        'influence': 0,
        'prestige': -1000.0,        
        'base': {
                 'population':9999, 
                 'previousAS':0, 
                 'previousAW':0, 
                 'previousDS':0, 
                 'edugrant':0, 
                 'churchgrant':5000, 
                 'otherexpenses':25.01
                },
        'mod': {
                'population':1.0004, 
                'poptax':0.04, 
                'bsnstax':0.025, 
                'edutax':0.05, 
                'churchtax':0.00, 
                'churches':['protestant','catholic','nondiscript']
                },
        'policies': [],
        'upgrades': []
    },
    'Slut Bay': {
        'name':'Slut Bay',
        'the_rest':...
    }
}
And then just create them from there:

Code:
class government:
    def __init__(self, id):
        self.name=govs[id]['name']
        self.etc=...
        
town1=government('Virgin Bay')
town2=government('Slut Bay')
This would make the government data easier to read and edit, although that only works if all of your towns will have preset starting values. If you're dynamically generating them, or using random attributes upon creation, then your object initializations will end up looking almost the same.

In the end, the best approach for me would be the one on the reply above: just add new lines on your object creation. It's perfectly readable like that.
 
  • Like
Reactions: GNVE

GNVE

Active Member
Jul 20, 2018
681
1,150
[Note: It's a bit late, don't hesitate if there's things that I'll not explain clearly]



If I understand right, your main issue is the too many arguments when creating an object.
But thankfully, there's ways to solve this issue.


Your arguments are all positional (what variable get what value depend on the position of the value). It's useful when you've a short list of arguments, but as you witnessed, it become a problem when the list grow.
To counter this, prefer named/keyword arguments (a couple name/value) every time it make sense.

Positional argument declaration: def __init__( argument1, argument2, argument3 )
Positional argument use: var = MyClass( value1, value2, value3
Named argument declaration: def __init__( argument1=defaultValue1, argument2=defaultValue2, argument3=defaultValue3 )
Named argument use: var = MyClass( argument2=value2, argument1=value1, argument3=value3 )

As you can see, each name argument have a default value. If an argument do not get an explicit value when the function/method is called, then it will get that default value.
Python:
init python:
    def testFnct( arg1=None, arg2="Default" ):
        print( "arg1 is " + str( arg1 ) )
        print( "arg2 is " + str( arg2 ) )
If you use the console to call that function, testFnct( 1, 2 ) will print:

Side note: Note that when the position of the value match the position of the argument, you can make them works like positional argument. But I do not recommend to do it, because it's more confusing that helping. I did it only for the demonstration.

but testFnct( arg2="Changed" ) will print:


and testFnct() will print:


For each one of your arguments, there's surely a value that is (or will be) used often. It's its default value.
That way, each time an object have to assign that value, you can just skip it. This will shorten a bit the call.


The other advantage when using named argument is precisely it's readability. Especially when you keep in mind that you can perfectly split a line.

Let's say that your __init__ declaration only use named arguments, you can perfectly have this:
Python:
var = government( name = "Evil ones",
                  glevel = "town",
                  corruption = 100,
                  influence = 50
                  prestige = 10
                  base = { ... }
                  mod = {...}
                  policies = [ "unique party", "curfew" ],
                  upgrades = [ ... ] )
What is perfectly manageable by a human.


Now, this being said, generally declarations use a mix between positional and named arguments. The first ones being used for any value expected to be unique for an object, while the second ones are used for anything that tweak the object.
So, in your case, it would looks like:
def __init__( self, name, glevel="town", corruption=50, influence=50, prestige=50, [...] )


Now, another thing that you can use, is dictionary unpacking.
When you've something like myDict = { "a": 1, "b": 2, "c": 3 } you can turn it into a suit of named arguments through the use of **.

In your class, there's three arguments that will probably have common values shared between many governments: "corruption", "influence" and "prestige".
So, you can have generic description for them:
Python:
averageDemocracy = { "corruption": 30, "influence": 50, "prestige": 50 }
averageDictatorship = { "corruption": 90, "influence": 30, "prestige": 30 }
Then you can simplify a bit more the creation of an object:
Python:
var = government( "Evil ones",  # Name is a positional argument.
                  #glevel = "town",  # "town" is the default value, you can skip it.
                  base = { ... },
                  mod = { ... },
                  policies = [ "unique party", "curfew" ],
                  upgrades = [ ... ],
                  #  Finally add /corruption/, /influence/ and /prestige/ values
                  # as they are defined by the /averageDictatorship/ dict.
                  **averageDictatorship )

Finally, in your example, I would also use a "false default value" approach for your two dictionaries arguments:
Python:
def __init__( [...] ):
    [...]

    # Not everything can be defaulted.
    if not "population" in base: raise ValueError( "You MUST define at least a base population value" )
    # I used 0 as default value, but you can obviously use whatever you want.
    if not "previousAS" in base: base["previousAS"] = 0
    if not "previousAW" in base: base["previousAW"] = 0
    if not "previousDS" in base: base["previousDS"] = 0
    if not "edugrant" in base: base["edugrant"] = 0
    # You can even make a default value depend on another value, passed or defaulted.
    if not "churchgrant" in base: base["churchgrant"] = int( base["polulation"] / 2 )
    if not "otherexpenses" in base: base["otherexpenses"] = 0
This would also permit to simplify your declaration.


In the end, your example would now looks like:
Python:
init python:
    class government:
        def __init__(self, name, glevel="town", corruption=10, influence=50, prestige=50, base={}, mod={}, policies=[], upgrades=[]):
             [...]

    #  There's possibility of conflict between /define/ and /default/, so create this in the /init/ block.
    # Being declared here and not changed, it shouldn't be part of savable values.
    badNeighborhoodDemocracy = { "corruption": 0, "influence": 0, "prestige": -1000.0 }

default town = government( 'Virgin Bay',
                           base = { "population": 9999, "otherexpenses": 25.01 },
                           mod = { [...] }, # I haven't made example for the default values
                           **badNeighborhoodDemocracy )
What is way simpler than what you have actually and don't need that you rely on an external file for the definition.


Side note:
Relying on an external file wouldn't effectively solve your issue, because it isn't effectively more readable. You just moved the issue, passing from:
Python:
default town = government( 'Virgin Bay',
                           'town',
                           0,
                           0,
                           -1000.0,
                           {'population':9999, 'previousAS':0, 'previousAW':0, 'previousDS':0, 'edugrant':0, 'churchgrant':5000, 'otherexpenses':25.01},
                           {'population':1.0004, 'poptax':0.04, 'bsnstax':0.025, 'edutax':0.05, 'churchtax':0.00, 'churches':['protestant','catholic','nondiscript']},
                           [],
                           [] )
to something that would looks the same in the file, but would have to be imported in a way or another.
Would you be able, in few months, to tell that the first "0" is the corruption, and the second the "influence" without having to look back at the "__init__()" declaration ?
This was exactly what I was looking for! Most online tutorials don't really deal with any of this and/or make example classes so small you still don't get what they are trying to teach. I think I understand 90%+ of what you wrote and the rest is just trying it for myself to make the connection. It is not your explanation but my lack of experience that stands in the way there.

Your example looks perfectly readable to me.
Right now yes, I agree it is still readable. I just know there will be problems later on. So it is better to ask before it becomes a real issue and might need a lot of reworking or dooming the project altogether. I was just missing a piece of the puzzle. As I said I'm in way over my head here. :)

If you're dynamically generating them, or using random attributes upon creation, then your object initializations will end up looking almost the same.
Luckily not. Everything will be predefined. This project, if it ever comes to fruition, is inspired by HHS+ but leaning way more into the management side of things with lots of upgrades.