cat posts/en/python-lernen-07-klassen-und-objekte.md
Learning Python (Part 7): Classes and objects
Classes bundle state and behavior: players, rooms, and items become objects with their own attributes and methods.
In part 6, we reworked the dungeon to be Pythonic. The player state still lives in a dictionary, each room consists of another dictionary, and the logic that goes with them is spread across several functions.
That works well. But the bigger our program gets, the more we have to track ourselves which data and functions belong together.
The player state, for example, looks like this:
spieler = {
"name": "Karl",
"hp": 100,
"max_hp": 100,
"gold": 50,
"inventar": [],
}
The functions that work with this data are somewhere else:
zeige_inventar(spieler["inventar"])
erstelle_statuszeile(spieler)
nimm_gegenstand(raum, spieler["inventar"], name)
With classes, we can combine data and the behavior that goes with it into
objects of their own. The spieler dictionary becomes a Spieler object
(German for player) that manages its state and provides methods such as
nimm(...), heile(...), or zeige_inventar().
Classes and objects
A class defines a new type of object. It describes which data objects of this type can have and which behavior is available to them.
An object is a concrete instance of this class.
class Spieler:
def __init__(self, name):
self.name = name
self.hp = 100
We create an object by calling the class:
held = Spieler("Karl")
Spieler is the class. held (German for hero) refers to a concrete instance
of this class.
We can create as many independent players as we like:
held = Spieler("Karl")
magierin = Spieler("Lara")
print(held.name)
print(magierin.name)
The output is:
Karl
Lara
magierin is German for a female mage. Both objects belong to the
Spieler class, but each has its own state:
held.hp = 75
print(held.hp)
print(magierin.hp)
Output:
75
100
The change to held doesn't affect magierin. Here, hp is an instance
attribute, so it belongs to one particular object.
Class names are capitalized
We usually write variables and functions in snake_case:
max_hp = 100
def zeige_status():
...
For class names, on the other hand, the PEP 8 style guide recommends the CapWords style, which is also often called PascalCase. Each word starts with an uppercase letter, and the words are joined without underscores:
class Spieler:
...
class Gegenstand:
...
class MagischerGegenstand:
...
Gegenstand means item, and MagischerGegenstand means magic item. This way,
the name alone usually tells you in the code whether a class, a function, or a
variable is meant.
__init__: setting up the starting state
A class can define the special method __init__:
class Spieler:
def __init__(self, name, max_hp=100, gold=0):
self.name = name
self.hp = max_hp
self.max_hp = max_hp
self.gold = gold
self.inventar = []
When we then create an object:
held = Spieler("Karl", max_hp=120, gold=50)
Python automatically calls __init__ for the new instance. After that, held
has the attributes that were set up:
print(held.name)
print(held.hp)
print(held.max_hp)
print(held.gold)
print(held.inventar)
Output:
Karl
120
120
50
[]
Colloquially, __init__ is often called the constructor. Strictly speaking,
though: __new__ is responsible for creating the new
instance, and __init__ then initializes the object that already exists.
In regular application code, we almost never have to deal with __new__
ourselves. For our dungeon, it's enough to think of __init__ as the place
where a new object gets its starting state.
The name __init__ starts and ends with two underscores each. Methods like this
are often called dunder methods, short for double underscore.
What does self mean?
By Python convention, the first parameter of a regular instance method is
called self:
class Spieler:
def __init__(self, name):
self.name = name
self refers to the concrete instance the method is currently running for.
With
held = Spieler("Karl")
self refers to the newly created Spieler object during the __init__ call.
This assignment
self.name = name
uses two different names:
nameis the parameter of__init__.self.nameis an attribute of the object.
The local parameter name is needed only during this method call. self.name
then stays around as part of the object.
When we create the player, we don't pass an argument for self:
held = Spieler("Karl")
With a bound method call, Python takes care of inserting the instance as the first argument.
self isn't a reserved keyword. Technically, the parameter could have a
different name. But the convention is so well established in Python that you
should always use self for regular instance methods.
Attributes store the state
We store values that belong to an object in attributes:
held.name
held.hp
held.gold
held.inventar
Together, these instance attributes make up the state of our player object.
We access them with a dot:
print(held.name)
held.gold = held.gold + 10
With regular Python classes, new attributes can in principle be created later, too:
held.level = 1
It's usually clearer to set up the essential instance attributes in __init__.
That way, one central place shows the state a new object starts with.
Methods: behavior on the object
Put simply, a method is a function that's available through a class and its instances:
class Spieler:
def __init__(self, name, max_hp=100):
self.name = name
self.hp = max_hp
self.max_hp = max_hp
self.inventar = []
def nimm(self, gegenstand):
self.inventar.append(gegenstand)
def ist_besiegt(self):
return self.hp <= 0
We call the methods through an object:
held = Spieler("Karl")
held.nimm("torch")
print(held.inventar)
print(held.ist_besiegt())
Output:
['torch']
False
The nimm(...) method (take) doesn't need the inventory as an additional
argument. Through self.inventar, it already has access to the state of the
concrete player object.
The call
held.nimm("torch")
is, in this case, the same as:
Spieler.nimm(held, "torch")
When accessed through held, the function Spieler.nimm becomes a method bound
to this instance. When it's called, Python automatically inserts held as the
first argument.
Methods can change the state
A method can read and change the object's attributes:
class Spieler:
def __init__(self, name, max_hp=100):
self.name = name
self.hp = max_hp
self.max_hp = max_hp
def erleide_schaden(self, menge):
self.hp = max(self.hp - menge, 0)
def heile(self, menge):
self.hp = min(self.hp + menge, self.max_hp)
Usage:
held = Spieler("Karl")
held.erleide_schaden(30)
print(held.hp)
held.heile(20)
print(held.hp)
Output:
70
90
erleide_schaden means take damage. The rules for hit points now live directly
in the behavior of the player object:
- Damage doesn't let
hpdrop below0. - Healing doesn't let
hprise abovemax_hp.
The calling code doesn't have to remember to respect these limits every time.
Methods can return values
Just like regular functions, methods can return a value with return:
class Spieler:
def __init__(self, name, hp=100):
self.name = name
self.hp = hp
def ist_besiegt(self):
return self.hp <= 0
You can use the method directly in a condition:
if held.ist_besiegt():
print("Your adventure ends here.")
So a method doesn't have to change the state. It can also just read it and return a result.
__str__: representing an object as a string
If we print one of our own objects without any further adjustments, we first get a technical representation:
held = Spieler("Karl")
print(held)
It looks something like this:
<__main__.Spieler object at 0x7f41d23a7e10>
That tells us hardly anything interesting about the player.
With the dunder method __str__, we can define which human-readable string
representation our object should have:
class Spieler:
def __init__(self, name, max_hp=100, gold=0):
self.name = name
self.hp = max_hp
self.max_hp = max_hp
self.gold = gold
self.inventar = []
def __str__(self):
return (
f"{self.name} | "
f"HP: {self.hp}/{self.max_hp} | "
f"Gold: {self.gold} | "
f"Inventory: {len(self.inventar)}"
)
Now this works:
held = Spieler("Karl", gold=50)
print(held)
Output:
Karl | HP: 100/100 | Gold: 50 | Inventory: 0
For objects, print(...) uses their string representation. For our Spieler,
that means __str__ comes into play.
We also get this representation in an f-string without a special format specifier:
print(f"Status: {held}")
Output:
Status: Karl | HP: 100/100 | Gold: 50 | Inventory: 0
__str__ has to return a string. The method shouldn't print anything
itself.
__repr__: a representation for developers
Besides __str__, there's __repr__.
While __str__ is mainly meant to provide a pleasant representation for humans,
__repr__ aims for an unambiguous representation that helps developers.
class Gegenstand:
def __init__(self, name, beschreibung=""):
self.name = name
self.beschreibung = beschreibung
def __str__(self):
return self.name
def __repr__(self):
return (
f"Gegenstand("
f"name={self.name!r}, "
f"beschreibung={self.beschreibung!r}"
f")"
)
Usage:
fackel = Gegenstand(
"torch",
"A sooty but still usable torch.",
)
print(str(fackel))
print(repr(fackel))
Output:
torch
Gegenstand(name='torch', beschreibung='A sooty but still usable torch.')
The !r in the f-string uses the repr(...) of the respective value. That
makes strings, for example, show up with their quotation marks. The variable
fackel is German for torch.
Where it makes sense, __repr__ is often designed so that the representation
resembles a valid Python expression that would create an equivalent object.
That's not a hard requirement, though.
You run into __repr__ in the REPL, when debugging, and when objects are
displayed inside many collections, among other places.
For our dungeon, __str__ matters more. We're still covering __repr__
because the difference quickly becomes practical with your own objects.
Objects can work with other objects
The player should no longer store plain strings in their inventory, but real
Gegenstand objects:
class Gegenstand:
def __init__(self, name, beschreibung=""):
self.name = name
self.beschreibung = beschreibung
def __str__(self):
return self.name
We create a torch:
fackel = Gegenstand(
"torch",
"A sooty but still usable torch.",
)
Then the player can pick up this object, using the Spieler class with
nimm(...) from the section “Methods: behavior on the object”:
held.nimm(fackel)
The inventory now contains a Gegenstand object and not the string "torch".
For human-readable output, we can convert the items with str(...):
print(", ".join(str(gegenstand) for gegenstand in held.inventar))
Output:
torch
In object-oriented programming, putting objects together from other objects is often called composition. People also often speak of a has-a relationship: A player has items in their inventory.
With our dungeon, though, there's a small subtlety. The torch is created independently of the player and can move between a room and the inventory. In stricter OOP terminology, this relationship can therefore also be called aggregation. For our code, what matters most is the practical idea: Our objects can store references to other objects and work with them.
We use that in several places:
- A
Spielerhas a list ofGegenstandobjects. - A
Raum(room) also has a list ofGegenstandobjects. - The
raeumedictionary refers to severalRaumobjects.
Bit by bit, this creates a network of connected objects.
Classes don't replace every collection
Classes and collections do different jobs.
A dictionary is still an excellent way to quickly find the right room by a known ID:
raeume = {
"halle": Raum(...),
"bibliothek": Raum(...),
"krypta": Raum(...),
}
A dictionary also makes sense for a room's exits:
{
"north": "bibliothek",
"east": "krypta",
}
So the Raum class doesn't automatically replace all dictionaries and lists. It
bundles the state and the behavior that together make up a room.
Inside objects, we'll keep using lists, dictionaries, sets, and other collections all the time.
Avoiding mutable default values
The same rules for default values apply to __init__ as to any other function
or method.
This class contains a well-known trap:
class Raum:
def __init__(self, name, gegenstaende=[]):
self.name = name
self.gegenstaende = gegenstaende
The default value [] is created only once, when the function definition runs.
All calls without their own gegenstaende then use the same list:
halle = Raum("Hall")
krypta = Raum("Crypt")
halle.gegenstaende.append("torch")
print(krypta.gegenstaende)
Surprising output:
['torch']
Here, the hall and the crypt refer to the same list.
The usual solution uses None as the default:
class Raum:
def __init__(self, name, gegenstaende=None):
self.name = name
if gegenstaende is None:
gegenstaende = []
self.gegenstaende = gegenstaende
We can write it more compactly:
class Raum:
def __init__(self, name, gegenstaende=None):
self.name = name
self.gegenstaende = [] if gegenstaende is None else gegenstaende
For our complete dungeon, we go one small step further and create copies of the collections that are passed in:
self.ausgaenge = dict(ausgaenge) if ausgaenge is not None else {}
self.gegenstaende = list(gegenstaende) if gegenstaende is not None else []
That gives each room its own outer collections. If the calling code passes in a list of items, for example, the room doesn't store exactly that list, but a new list with the same elements.
This is still only a shallow copy. The Gegenstand objects inside it
aren't duplicated—and that's exactly what we want here.
The commonly seen shorthand
self.gegenstaende = gegenstaende or []
isn't fully equivalent. An empty list that was passed in on purpose is also falsy and would therefore be replaced by a different new list.
With
gegenstaende is None
we check for exactly the case we actually mean.
The dungeon, step 7
We now replace the dictionaries for the player and the individual rooms with objects of their own. The outer dungeon world stays a dictionary so that we can still find rooms conveniently by their ID.
Open dungeon.py and replace the previous content with the following code:
# Dungeon—Part 7: Classes and objects
class Gegenstand:
"""An item that can lie in a room or in an inventory."""
def __init__(self, name, beschreibung=""):
self.name = name
self.beschreibung = beschreibung
def __str__(self):
return self.name
def __repr__(self):
return (
f"Gegenstand("
f"name={self.name!r}, "
f"beschreibung={self.beschreibung!r}"
f")"
)
class Spieler:
"""Manages the player's state and behavior."""
def __init__(self, name, max_hp=100, gold=0):
self.name = name
self.hp = max_hp
self.max_hp = max_hp
self.gold = gold
self.inventar = []
def nimm(self, gegenstand):
"""Puts an item into the inventory."""
self.inventar.append(gegenstand)
def erleide_schaden(self, menge):
"""Reduces HP, but not below zero."""
self.hp = max(self.hp - menge, 0)
def heile(self, menge):
"""Increases HP, but not above max_hp."""
self.hp = min(self.hp + menge, self.max_hp)
def ist_besiegt(self):
"""Checks whether the player has no HP left."""
return self.hp <= 0
def zeige_inventar(self):
"""Shows the inventory as a numbered list."""
if not self.inventar:
print("Your inventory is empty.")
return
print("Inventory:")
for nummer, gegenstand in enumerate(self.inventar, start=1):
print(f" {nummer}) {gegenstand}")
def __str__(self):
return (
f"{self.name} | "
f"HP: {self.hp}/{self.max_hp} | "
f"Gold: {self.gold} | "
f"Inventory: {len(self.inventar)}"
)
class Raum:
"""A room with a description, exits, and items."""
def __init__(
self,
name,
beschreibung,
ausgaenge=None,
gegenstaende=None,
):
self.name = name
self.beschreibung = beschreibung
self.ausgaenge = dict(ausgaenge) if ausgaenge is not None else {}
self.gegenstaende = (
list(gegenstaende)
if gegenstaende is not None
else []
)
def beschreibe(self):
"""Shows the room, its exits, and visible items."""
print()
print(f"Location: {self.name}")
print(self.beschreibung)
print("Exits:")
for nummer, richtung in enumerate(self.ausgaenge, start=1):
print(f" {nummer}) {richtung}")
if self.gegenstaende:
print(
"Lying here:",
", ".join(
str(gegenstand)
for gegenstand in self.gegenstaende
),
)
def zeige_gegenstaende(self):
"""Shows the room's items."""
if not self.gegenstaende:
print("There's nothing useful here.")
return
print(
"Lying here:",
", ".join(
str(gegenstand)
for gegenstand in self.gegenstaende
),
)
def hat_ausgang(self, richtung):
"""Checks whether the room has an exit in that direction."""
return richtung in self.ausgaenge
def ziel(self, richtung):
"""Returns the ID of the destination room, or None."""
return self.ausgaenge.get(richtung)
def nimm_gegenstand(self, name):
"""Removes a matching item and returns it."""
gesuchter_name = name.strip().lower()
for gegenstand in self.gegenstaende:
if gegenstand.name.lower() == gesuchter_name:
self.gegenstaende.remove(gegenstand)
return gegenstand
return None
def erstelle_raeume():
"""Creates the dungeon world from Raum and Gegenstand objects."""
fackel = Gegenstand(
"torch",
"A sooty but still usable torch.",
)
schluessel = Gegenstand(
"key",
"A heavy iron key with rusty teeth.",
)
return {
"halle": Raum(
name="Hall",
beschreibung=(
"You're standing in a dark hall with a musty smell."
),
ausgaenge={
"north": "bibliothek",
"east": "krypta",
},
gegenstaende=[fackel],
),
"bibliothek": Raum(
name="Library",
beschreibung=(
"Dusty shelves full of tattered books surround you."
),
ausgaenge={
"south": "halle",
},
gegenstaende=[schluessel],
),
"krypta": Raum(
name="Crypt",
beschreibung=(
"Moisture glistens on the walls of the old crypt."
),
ausgaenge={
"west": "halle",
},
),
}
def teile_befehl(eingabe):
"""Splits the input into a verb and an optional argument."""
verb, _, argument = eingabe.strip().lower().partition(" ")
return verb, argument.strip()
def finde_raeume_mit_loot(raeume):
"""Returns the names of all rooms that have items."""
return [
raum.name
for raum in raeume.values()
if raum.gegenstaende
]
def main():
"""Starts the dungeon and runs the game loop."""
raeume = erstelle_raeume()
print("You stand before the rusty gate of a forgotten dungeon.")
print("A cold draft blows toward you.")
name = input("What's your name, adventurer? ").strip()
gold = int(input("How many gold coins are you bringing? "))
spieler = Spieler(
name=name,
max_hp=100,
gold=gold,
)
ort = "halle"
besuchte_raeume = set()
raum_anzeigen = True
print()
print(f"Welcome, {spieler.name}!")
print(
"Commands: look, take <item>, inventory, status, "
"loot, go <direction>, quit"
)
while True:
raum = raeume[ort]
if raum_anzeigen:
besuchte_raeume.add(ort)
raum.beschreibe()
raum_anzeigen = False
verb, argument = teile_befehl(input("\n> "))
if verb == "quit":
print("You leave the dungeon. Until next time.")
break
elif verb == "look":
raum.zeige_gegenstaende()
elif verb == "take":
if not argument:
print("What do you want to take?")
continue
gegenstand = raum.nimm_gegenstand(argument)
if gegenstand is None:
print(f"There's no item called {argument} here.")
continue
spieler.nimm(gegenstand)
print(f"You take the {gegenstand}.")
if gegenstand.beschreibung:
print(gegenstand.beschreibung)
elif verb == "inventory":
spieler.zeige_inventar()
elif verb == "status":
print(spieler)
print(f"Rooms visited: {len(besuchte_raeume)}")
elif verb == "loot":
raeume_mit_loot = finde_raeume_mit_loot(raeume)
text = (
", ".join(raeume_mit_loot)
if raeume_mit_loot
else "(none)"
)
print("Rooms with loot:", text)
elif verb == "go":
if not argument:
print("Which direction do you want to go?")
continue
if not raum.hat_ausgang(argument):
print(f"You can't go {argument}.")
continue
ort = raum.ziel(argument)
raum_anzeigen = True
elif not argument and raum.hat_ausgang(verb):
ort = raum.ziel(verb)
raum_anzeigen = True
else:
print("I don't understand that.")
print("Possible exits:", ", ".join(raum.ausgaenge))
print(
"Other commands: look, take <item>, inventory, "
"status, loot, go <direction>, quit"
)
if __name__ == "__main__":
main()
Run the program:
python dungeon.py
One possible run looks like this:
You stand before the rusty gate of a forgotten dungeon.
A cold draft blows toward you.
What's your name, adventurer? Karl
How many gold coins are you bringing? 50
Welcome, Karl!
Commands: look, take <item>, inventory, status, loot, go <direction>, quit
Location: Hall
You're standing in a dark hall with a musty smell.
Exits:
1) north
2) east
Lying here: torch
> take torch
You take the torch.
A sooty but still usable torch.
> inventory
Inventory:
1) torch
> go north
Location: Library
Dusty shelves full of tattered books surround you.
Exits:
1) south
Lying here: key
> take key
You take the key.
A heavy iron key with rusty teeth.
> status
Karl | HP: 100/100 | Gold: 50 | Inventory: 2
Rooms visited: 2
> loot
Rooms with loot: (none)
> quit
You leave the dungeon. Until next time.
What changed compared to part 6?
The basic flow of the game has stayed the same. Instead of dictionary keys, we now work with attributes and methods in many places.
Before:
gegenstand = nimm_gegenstand(raum, spieler["inventar"], argument)
Now:
gegenstand = raum.nimm_gegenstand(argument)
spieler.nimm(gegenstand)
Before:
raum["gegenstaende"]
Now:
raum.gegenstaende
Before:
if argument in raum["ausgaenge"]:
ort = raum["ausgaenge"][argument]
Now:
if raum.hat_ausgang(argument):
ort = raum.ziel(argument)
Methods such as hat_ausgang(...) (has exit) and ziel(...) (destination)
form an interface to the room. For these operations, the game loop no longer
needs to know that the exits are stored internally in a dictionary.
The player manages their own state
The player state is set up when the object is created:
spieler = Spieler(
name=name,
max_hp=100,
gold=gold,
)
After that, we access it through attributes:
spieler.name
spieler.hp
spieler.max_hp
spieler.gold
spieler.inventar
The behavior that goes with it lives in methods of the class:
spieler.nimm(gegenstand)
spieler.erleide_schaden(25)
spieler.heile(20)
spieler.ist_besiegt()
spieler.zeige_inventar()
This puts the state and the behavior that works directly with it in the same place in the program.
That doesn't mean every function has to become a method from now on.
teile_befehl(...), for example, belongs neither to a player nor to a room, so
it stays a regular function.
Items move between objects
At the start, the torch is in the hall's list of items.
The call
gegenstand = raum.nimm_gegenstand("torch")
removes the Gegenstand object from the room's list and returns exactly this
object.
Then
spieler.nimm(gegenstand)
puts the same torch into the player's inventory.
No new Gegenstand object is created in the process. The existing instance only
moves from one collection to another.
That's an important difference from our earlier dungeon, in which items were
only strings like "torch". The torch can now have state of its own: a
description, for example, and later perhaps weight, condition, or burn time.
Methods form an interface
In the game loop, we could still access many attributes directly:
raum.ausgaenge
raum.gegenstaende
spieler.inventar
For common operations or rules, we use methods instead:
raum.hat_ausgang(richtung)
raum.nimm_gegenstand(name)
spieler.nimm(gegenstand)
spieler.heile(menge)
These methods form a small API for each class.
As a result, the rest of the code depends less on how an object stores its data internally.
For example, Raum might organize its exits differently at some point. As long
as
raum.hat_ausgang(richtung)
raum.ziel(richtung)
still offer the same behavior, the game loop wouldn't necessarily have to change.
Our classes are by no means fully encapsulated yet, though. Attributes such as
raum.ausgaenge or spieler.hp stay directly accessible. For interfaces like
these, Python often relies more on conventions than on enforced access
restrictions.
Is this already object-oriented programming?
Yes. We model parts of our game as objects with their own state and fitting behavior.
Beyond that, object-oriented programming offers many more concepts and techniques, for example:
- encapsulation
- inheritance
- polymorphism
- class methods and static methods
- properties
- abstract base classes
But we don't have to use them automatically just because our program contains classes.
For the dungeon, a much simpler idea is enough for now:
A class can bundle data and the behavior that belong together logically.
Even that doesn't mean every dictionary should immediately become a class. A class is mainly worth it when something in the program is a recognizable unit of its own and has behavior of its own tied to that data.
Pitfalls
- Forgetting
self: Inside an instance method, you access the attributes of the respective object throughself.
python
self.hp = self.hp - schaden
hp on its own would be a different name and wouldn't automatically mean the
instance attribute here.
-
Passing
selfyourself in a regular method call: Withspieler.heile(20), Python binds the instance toselfautomatically. Sospieler.heile(spieler, 20)would pass one argument too many. -
Misspelling the dunder method: It's
__init__, with two underscores each before and afterinit. -
Returning a value of your own from
__init__:__init__initializes an instance and has to returnNone. Areturnwithout a value is possible, but any other return value leads to aTypeError. -
Using mutable default values: A list or a dictionary used directly as a default value isn't created anew for each instance.
python
def __init__(self, inventar=[]):
...
Normally, use None and create the collection inside the method.
-
Equating
wert or []with aNonecheck: An empty collection that was passed in on purpose is also falsy.is Nonechecks more precisely whether no value was passed at all. -
Mixing up class and object:
Spieleris the class.Spieler("Karl")creates an instance of this class. -
Mixing up a method and a method call:
spieler.zeige_inventarreturns the method object. Onlyspieler.zeige_inventar()calls the method. -
Having
__str__print directly:__str__has to return a string. The printing is then done byprint(...), for example. -
Thinking composition and aggregation are mandatory Python concepts: These are terms for describing relationships between objects, not special Python syntax.
-
Compulsively wrapping all data in classes: Lists, sets, and dictionaries are still useful. Classes complement collections; they don't replace them.
-
Putting too much behavior into one class: Just because a function could technically be a method of
Spielerdoesn't mean it automatically belongs there. The behavior should make sense for the object.
Exercises
1. Cause damage
Add a method erleide_schaden(menge) to the Spieler class. It should reduce
the HP, but not let it drop below 0.
Solution
def erleide_schaden(self, menge):
"""Reduces HP, but not below zero."""
self.hp = max(self.hp - menge, 0)
spieler.erleide_schaden(25)
print(spieler.hp)
2. Check an exit
Add a method hat_ausgang(richtung) to the Raum class that returns True if
the room has a matching exit.
Solution
def hat_ausgang(self, richtung):
"""Checks whether the room has an exit in that direction."""
return richtung in self.ausgaenge
if raum.hat_ausgang("north"):
print("You can go north.")
3. Search for items in the inventory
Add a method hat_gegenstand(name) (has item) to Spieler. Case shouldn't
matter.
Solution
def hat_gegenstand(self, name):
"""Checks whether an item is in the inventory."""
gesuchter_name = name.strip().lower()
return any(
gegenstand.name.lower() == gesuchter_name
for gegenstand in self.inventar
)
if spieler.hat_gegenstand("key"):
print("You can unlock the door.")
4. Describe an item in more detail
Add a method untersuche() (examine) to Gegenstand that returns either the
description or a default text.
Solution
def untersuche(self):
"""Returns the item's description."""
if self.beschreibung:
return self.beschreibung
return f"You notice nothing special about the {self.name}."
print(fackel.untersuche())
Going deeper
For classes that mainly store data, Python can generate a lot of boilerplate automatically:
Dataclasses: classes for structured data
Why methods such as __init__, __str__, and __repr__ work, and how your
own objects can behave like built-in data types:
The Python data model: dunder methods
Next up
Our dungeon now has real objects with their own state and behavior. When the player enters the gold value, though, the program still trusts that they really type a valid integer.
That's why part 8 is about error
handling: exceptions, try, except, else, and finally. With them, the
dungeon responds to invalid input and other problems in a controlled way,
instead of crashing with a traceback.
Keyboard shortcuts
- j k
- scroll down / up
- n p
- next / previous section
- g G
- start / end of the article
- t
- show outline
- ?
- show this help
- Esc
- close
0 comments
No comments yet. Be the first!
Sign in to leave a comment.