From dd3ef28d8290f4a094ad76b071c95668e03a12a7 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Jan 2023 14:50:22 +0100 Subject: [PATCH] Updated the python-classes-and-objects with more examples around multiple-inheritance and why it's useful --- ...ner-Tutorial-Python-classes-and-objects.md | 120 +++++++++++------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-classes-and-objects.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-classes-and-objects.md index b2b07d0336..78e8680ab8 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-classes-and-objects.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-classes-and-objects.md @@ -85,8 +85,7 @@ 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. +> 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](./Beginner-Tutorial-Python-basic-introduction.md) we learned how to open the in-game multi-line interpreter. @@ -109,28 +108,26 @@ You now only need to import once to use the imported function over and over. > 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. +```{sidebar} Alternative to py +If you find entering multiple lines in the `py` command clunky (a traditional mud client is pretty limited for this kind of thing) you can also `cd` to your `mygame` folder and run `evennia shell`. You will end up in a python shell where Evennia is available. If you do `pip install ipython` you'll get an even more modern python shell to use. This works outside the game but `print` will show in the same way. +``` + +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. ## 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/objects.py` in your text editor of choice. +Open `mygame/typeclasses/scripts.py` in your text editor of choice. ```python +# mygame/typeclasses/script.py """ module docstring """ -from evennia import DefaultObject +from evennia import DefaultScript -class ObjectParent: - """ - class docstring - """ - pass - -class Object(DefaultObject): +class Script(DefaultScript): """ class docstring """ @@ -141,19 +138,15 @@ class Object(DefaultObject): A docstring is not the same as a comment (created by `#`). A docstring is not ignored by Python but is an integral part of the thing it is documenting (the module and the class in this case). For example, we read docstrings to help text for [API documentation](../../../Evennia-API.md); we could not do that with comments. ``` -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. +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 +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. -Next 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. We will not concern ourselves with it for this tutorial. +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). -The `class` named `Object`_ inherits_ from `ObjectParent` and `DefaultObject`. Since we see that `ObjectParent` is empty, what is interesting is `DefaultObject`. Again, the `Object` class doesn't -actually do anything on its own right now, but because of it being a child of `DefaultObject`, it's actually providing a lot of functionality! If this is confusing, read on. - -We will get back to this module in the [next lesson](./Beginner-Tutorial-Learning-Typeclasses.md). First 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. +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. ```{sidebar} OOP Classes, objects, instances and inheritance are fundamental to Python. This and some other concepts are often clumped together under the term Object-Oriented-Programming (OOP). @@ -179,8 +172,7 @@ class Monster: 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. +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. ```{sidebar} Terms @@ -194,7 +186,7 @@ by _calling_ the class, much like you would a function: fluffy = Monster() -Let's try it in-game (we use multi-line mode, it's easier) +Let's try it in-game (we use `py` multi-line mode, it's easier) > py > from typeclasses.monsters import Monster @@ -205,9 +197,7 @@ Let's try it in-game (we use multi-line mode, it's easier) 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 the `self` has to be -> there when defining the method, we _never_ add it explicitly when we call the method (Python -> will add the correct `self` for us automatically behind the scenes). +> Note how we _didn't_ call the method as `fluffy.move_around(self)`. While the `self` has to be there when defining the method, we _never_ add it explicitly when we call the method (Python will add the correct `self` for us automatically behind the scenes). Let's create the sibling of Fluffy, Cuddly: @@ -215,8 +205,8 @@ Let's create the sibling of Fluffy, Cuddly: > cuddly.move_around() Monster is moving! -We now have two dragons 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 dragons we create, they will all show the same printout since `key` is always fixed as "Monster". +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: @@ -232,11 +222,9 @@ class Monster: ``` -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`. +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: +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. @@ -269,16 +257,13 @@ So far all we've seen a class do is to behave like our first `hello_world` funct 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: +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: +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 @@ -327,12 +312,11 @@ class Dragon(Monster): 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. +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. ```{sidebar} Multi-inheritance -It's possible to add more comma-separated parents to a class. You should usually avoid this until you `really` know what you are doing. A single parent will be enough for almost every case you'll need. +It's possible to add more comma-separated parents to a class. We show an example of such 'multiple inheritance' last in this lesson. You should usually avoid yourself setting up multiple inheritance until you know what you are doing. A single parent will be enough for almost every case you'll need. ``` @@ -380,9 +364,59 @@ We can see that `Monster.move_around()` is called first and prints "Smaug is mov 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. +### A look at multiple inheritance + +Open `mygame/typeclasses/objects.py` in your text editor of choice. + +```python +""" +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`: + +```python +""" +... +""" + +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](./Beginner-Tutorial-Learning-Typeclasses.md). + ## 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. - +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. \ No newline at end of file