5. Introduction to Python classes and objects¶
We have now learned how to run some simple Python code from inside (and outside) your game server. We have also taken a look at what our game dir looks and what is where. Now we’ll start to use it.
5.1. Importing things¶
In a previous lesson we already learned how to import resources into our code. Now we’ll dive a little deeper.
No one writes something as big as an online game in one single huge file. Instead one breaks up the code into separate files (modules). Each module is dedicated to different purposes. Not only does it make things cleaner, organized and easier to understand.
Splitting code also makes it easier to re-use - you just import the resources you need and know you only get just what you requested. This makes it easier to spot errors and to know what code is good and which has issues.
Evennia itself uses your code in the same way - you just tell it where a particular type of code is, and it will import and use it (often instead of its defaults).
Here’s a familiar example:
> py import world.test ; world.test.hello_world(me)
Hello World!
In this example, on your hard drive, the files looks like this:
mygame/
world/
test.py <- inside this file is a function hello_world
If you followed earlier tutorial lessons, the mygame/world/test.py
file should look like this (if
not, make it so):
def hello_world(who):
who.msg("Hello World!")
To reiterate, the python_path describes the relation between Python resources, both between and inside Python modules (that is, files ending with .py). Paths use .
and always skips the .py
file endings. Also, Evennia already knows to start looking for python resources inside mygame/
so this should never be included.
import world.test
The import
Python instruction loads world.test
so you have it available. You can now go “into”
this module to get to the function you want:
world.test.hello_world(me)
Using import
like this means that you have to specify the full world.test
every time you want
to get to your function. Here’s an alternative:
from world.test import hello_world
The from ... import ...
is very, very common as long as you want to get something with a longer
python path. It imports hello_world
directly, so you can use it right away!
> py from world.test import hello_world ; hello_world(me)
Hello World!
Let’s say your test.py
module had a bunch of interesting functions. You could then import them
all one by one:
from world.test import hello_world, my_func, awesome_func
If there were a lot of functions, you could instead just import test
and get the function
from there when you need (without having to give the full world.test
every time):
> from world import test ; test.hello_world(me)
Hello World!
You can also rename stuff you import. Say for example that the module you import to already has a function hello_world
but we also want to use the one from world/test.py
:
from world.test import hello_world as test_hello_world
The form from ... import ... as ...
renames the import.
> from world.test import hello_world as hw ; hw(me)
Hello World!
Avoid renaming unless it’s to avoid a name-collistion like above - you want to make things as easy to read as possible, and renaming adds another layer of potential confusion.
In the basic intro to Python we learned how to open the in-game multi-line interpreter.
> py
Evennia Interactive Python mode
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
[GCC 8.2.0] on Linux
[py mode - quit() to exit]
You now only need to import once to use the imported function over and over.
> from world.test import hello_world
> hello_world()
Hello World!
> hello_world()
Hello World!
> hello_world()
Hello World!
> quit()
Closing the Python console.
The same goes when writing code in a module - in most Python modules you will see a bunch of imports at the top, resources that are then used by all code in that module.
5.2. On classes and objects¶
Now that we know about imports, let look at a real Evennia module and try to understand it.
Open mygame/typeclasses/scripts.py
in your text editor of choice.
# mygame/typeclasses/script.py
"""
module docstring
"""
from evennia import DefaultScript
class Script(DefaultScript):
"""
class docstring
"""
pass
The real file is much longer but we can ignore the multi-line strings (""" ... """
). These serve as documentation-strings, or docstrings for the module (at the top) and the class
below.
Below the module doc string we have the import. In this case we are importing a resource
from the core evennia
library itself. We will dive into this later, for now we just treat this
as a black box.
The class
named Script
_ inherits_ from DefaultScript
. As you can see Script
is pretty much empty. All the useful code is actually in DefaultScript
(Script
inherits that code unless it overrides it with same-named code of its own).
We need to do a little detour to understand what a ‘class’, an ‘object’ or ‘instance’ is. These are fundamental things to understand before you can use Evennia efficiently.
5.2.1. Classes and instances¶
A ‘class’ can be seen as a ‘template’ for a ‘type’ of object. The class describes the basic functionality of everyone of that class. For example, we could have a class Monster
which has resources for moving itself from room to room.
Open a new file mygame/typeclasses/monsters.py
. Add the following simple class:
class Monster:
key = "Monster"
def move_around(self):
print(f"{self.key} is moving!")
Above we have defined a Monster
class with one variable key
(that is, the name) and one
method on it. A method is like a function except it sits “on” the class. It also always has
at least one argument (almost always written as self
although you could in principle use
another name), which is a reference back to itself. So when we print self.key
we are referring back to the key
on the class.
A class is just a template. Before it can be used, we must create an instance of the class. If
Monster
is a class, then an instance is Fluffy
, a specific dragon individual. You instantiate
by calling the class, much like you would a function:
fluffy = Monster()
Let’s try it in-game (we use py
multi-line mode, it’s easier)
> py
> from typeclasses.monsters import Monster
> fluffy = Monster()
> fluffy.move_around()
Monster is moving!
We created an instance of Monster
, which we stored in the variable fluffy
. We then
called the move_around
method on fluffy to get the printout.
Note how we didn’t call the method as
fluffy.move_around(self)
. While theself
has to be there when defining the method, we never add it explicitly when we call the method (Python will add the correctself
for us automatically behind the scenes).
Let’s create the sibling of Fluffy, Cuddly:
> cuddly = Monster()
> cuddly.move_around()
Monster is moving!
We now have two monsters and they’ll hang around until with call quit()
to exit this Python
instance. We can have them move as many times as we want. But no matter how many monsters we create, they will all show the same printout since key
is always fixed as “Monster”.
Let’s make the class a little more flexible:
class Monster:
def __init__(self, key):
self.key = key
def move_around(self):
print(f"{self.key} is moving!")
The __init__
is a special method that Python recognizes. If given, this handles extra arguments when you instantiate a new Monster. We have it add an argument key
that we store on self
.
Now, for Evennia to see this code change, we need to reload the server. You can either do it this way:
> quit()
Python Console is closing.
> reload
Or you can use a separate terminal and restart from outside the game:
$ evennia reload (or restart)
Either way you’ll need to go into py
again:
> py
> from typeclasses.monsters import Monster
fluffy = Monster("Fluffy")
fluffy.move_around()
Fluffy is moving!
Now we passed "Fluffy"
as an argument to the class. This went into __init__
and set self.key
, which we later used to print with the right name!
5.2.2. What’s so good about objects?¶
So far all we’ve seen a class do is to behave like our first hello_world
function but being more complex. We could just have made a function:
def monster_move_around(key):
print(f"{key} is moving!")
The difference between the function and an instance of a class (the object), is that the object retains state. Once you called the function it forgets everything about what you called it with last time. The object, on the other hand, remembers changes:
> fluffy.key = "Fluffy, the red dragon"
> fluffy.move_around()
Fluffy, the red dragon is moving!
The fluffy
object’s key
was changed for as long as it’s around. This makes objects extremely useful for representing and remembering collections of data - some of which can be other objects in turn. Some examples:
A player character with all its stats
A monster with HP
A chest with a number of gold coins in it
A room with other objects inside it
The current policy positions of a political party
A rule with methods for resolving challenges or roll dice
A multi-dimenstional data-point for a complex economic simulation
And so much more!
5.2.3. Classes can have children¶
Classes can inherit from each other. A “child” class will inherit everything from its “parent” class. But if the child adds something with the same name as its parent, it will override whatever it got from its parent.
Let’s expand mygame/typeclasses/monsters.py
with another class:
class Monster:
"""
This is a base class for Monster.
"""
def __init__(self, key):
self.key = key
def move_around(self):
print(f"{self.key} is moving!")
class Dragon(Monster):
"""
This is a dragon monster.
"""
def move_around(self):
print(f"{self.key} flies through the air high above!")
def firebreath(self):
"""
Let our dragon breathe fire.
"""
print(f"{self.key} breathes fire!")
We added some docstrings for clarity. It’s always a good idea to add doc strings; you can do so also for methods, as exemplified for the new firebreath
method.
We created the new class Dragon
but we also specified that Monster
is the parent of Dragon
but adding the parent in parenthesis. class Classname(Parent)
is the way to do this.
Let’s try out our new class. First reload
the server and then:
> py
> from typeclasses.monsters import Dragon
> smaug = Dragon("Smaug")
> smaug.move_around()
Smaug flies through the air high above!
> smaug.firebreath()
Smaug breathes fire!
Because we didn’t (re)implement __init__
in Dragon
, we got the one from Monster
. We did implement our own move_around
in Dragon
, so it overrides the one in Monster
. And firebreath
is only available for Dragon
s. Having that on Monster
would not have made much sense, since not every monster can breathe fire.
One can also force a class to use resources from the parent even if you are overriding some of it. This is done with the super()
method. Modify your Dragon
class as follows:
# ...
class Dragon(Monster):
def move_around(self):
super().move_around()
print("The world trembles.")
# ...
Keep
Monster
and thefirebreath
method. The# ...
above indicates the rest of the code is unchanged.
The super().move_around()
line means that we are calling move_around()
on the parent of the class. So in this case, we will call Monster.move_around
first, before doing our own thing.
To see, reload
the server and then:
> py
> from typeclasses.monsters import Dragon
> smaug = Dragon("Smaug")
> smaug.move_around()
Smaug is moving!
The world trembles.
We can see that Monster.move_around()
is called first and prints “Smaug is moving!”, followed by the extra bit about the trembling world from the Dragon
class.
Inheritance is a powerful concept. It allows you to organize and re-use code while only adding the special things you want to change. Evennia uses this a lot.
5.2.4. A look at multiple inheritance¶
Open mygame/typeclasses/objects.py
in your text editor of choice.
"""
module docstring
"""
from evennia import DefaultObject
class ObjectParent:
"""
class docstring
"""
pass
class Object(ObjectParent, DefaultObject):
"""
class docstring
"""
pass
In this module we have an empty class
named ObjectParent
. It doesn’t do anything, its only code (except the docstring) is pass
which means, well, to pass and don’t do anything. Since it also doesn’t inherit from anything, it’s just an empty container.
The class
named Object
_ inherits_ from ObjectParent
and DefaultObject
. Normally a class only has one parent, but here there are two. We already learned that a child inherits everything from a parent unless it overrides it. When there are more than one parents (“multiple inheritance”), inheritance happens from left to right.
So if obj
is an instance of Object
and we try to access obj.foo
, Python will first check if the Object
class has a property/method foo
. Next it will check if ObjectParent
has it. Finally, it will check in DefaultObject
. If neither have it, you get an error.
Why has Evennia set up an empty class parent like this? To answer, let’s check out another module, mygame/typeclasses/rooms.py
:
"""
...
"""
from evennia.objects.objects import DefaultRoom
from .objects import ObjectParent
class Room(ObjectParent, DefaultRoom):
"""
...
"""
pass
Here we see that a Room
inherits from the same ObjectParent
(imported from objects.py
) along with a DefaultRoom
parent from the evennia
library. You’ll find the same is true for Character
and Exit
as well. These are all examples of ‘in-game objects’, so they could well have a lot in common. The precense of ObjectParent
gives you an (optional) way to add code that should be the same for all those in-game entities. Just put that code in ObjectParent
and all the objects, characters, rooms and exits will automatically have it as well!
We will get back to the objects.py
module in the next lesson.
5.3. Summary¶
We have created our first dragons from classes. We have learned a little about how you instantiate a class into an object. We have seen some examples of inheritance and we tested to override a method in the parent with one in the child class. We also used super()
to good effect.
We have used pretty much raw Python so far. In the coming lessons we’ll start to look at the extra bits that Evennia provides. But first we need to learn just where to find everything.