cat posts/en/python-lernen-06-pythonic-schreiben.md
Learning Python (Part 6): Writing Pythonic code
Comprehensions, slicing, enumerate, zip, unpacking, and truthiness: these idioms make our Python code more compact, clearer, and easier to read.
The dungeon from part 5 is now neatly split into functions. The code works, it's understandable, and it can be extended.
Today only a few new game commands are added. Instead, we look at how to express typical tasks the way it's usually done in Python.
The term often used for this is Pythonic. Pythonic code uses the language's features and idioms instead of laboriously recreating patterns you know from other programming languages.
Keep this in mind:
Pythonic doesn't mean writing as few characters as possible. What counts is that the intent of the code stays easy to see.
We cover comprehensions, truthiness, slicing, enumerate, zip, unpacking,
and a few other idioms. Then we apply some of them to our dungeon.
Comprehensions
Let's say we want to convert the IDs of several rooms to uppercase.
With a regular loop, we can build a new list step by step:
raum_ids = ["halle", "bibliothek", "krypta"]
namen = []
for raum_id in raum_ids:
namen.append(raum_id.upper())
print(namen)
The output is:
['HALLE', 'BIBLIOTHEK', 'KRYPTA']
namen is German for names. Python offers a more compact notation for this:
the list comprehension.
namen = [raum_id.upper() for raum_id in raum_ids]
The result is the same.
The basic form is:
[ausdruck for element in iterable]
The expression (ausdruck) is evaluated for each element. The results end up
in a new list.
In our example:
[raum_id.upper() for raum_id in raum_ids]
you can read the line roughly like this:
- Take each
raum_idin turn. - Call
.upper()on it. - Put the results into a new list.
So in a comprehension, the expression comes before the loop. That feels a bit unusual the first time you read it, but you get used to it quickly.
Filtering elements in a comprehension
With an if at the end, you can keep only certain elements:
gegenstaende = ["bread", "torch", "rope", "key"]
lange_namen = [
gegenstand
for gegenstand in gegenstaende
if len(gegenstand) > 4
]
print(lange_namen)
The output is:
['bread', 'torch']
lange_namen means long names. The general form is:
[ausdruck for element in iterable if bedingung]
The condition (bedingung) decides which elements make it into the new list.
For our dungeon world, we can, for example, find all rooms that still have items lying in them:
raeume_mit_loot = [
raum["name"]
for raum in raeume.values()
if raum["gegenstaende"]
]
Two Python idioms meet here: a comprehension and truthiness. A non-empty list of items evaluates as true, an empty one as false. We'll look at what's behind that in more detail in a moment.
List comprehension or regular loop?
A comprehension is a good fit when you create new values from an iterable and the whole thing can be described with a short expression.
This example is compact and still easy to read:
quadrate = [zahl ** 2 for zahl in range(1, 6)]
The result (quadrate means squares) would be:
[1, 4, 9, 16, 25]
But a comprehension isn't automatically better just because it's shorter.
This example is already a lot denser:
werte = [
zahl * 2
for gruppe in gruppen
for zahl in gruppe
if zahl > 0
if zahl % 2 == 0
]
As a regular loop, the flow is easier to follow:
werte = []
for gruppe in gruppen:
for zahl in gruppe:
if zahl > 0 and zahl % 2 == 0:
werte.append(zahl * 2)
Pythonic code doesn't mean code golf. If the longer version is easier to understand, it's usually the better one, too.
Dict comprehensions
Besides lists, you can also create dictionaries with a comprehension.
From this dictionary:
lebenspunkte = {
"Karl": 100,
"Goblin": 30,
"Dragon": 999,
}
we build a new dictionary with halved hit points (lebenspunkte is German for
hit points):
halbe_lebenspunkte = {
name: hp // 2
for name, hp in lebenspunkte.items()
}
print(halbe_lebenspunkte)
The output is:
{'Karl': 50, 'Goblin': 15, 'Dragon': 499}
The basic form looks like this:
{
schluessel: wert
for element in iterable
}
Dict comprehensions can filter too:
starke_gegner = {
name: hp
for name, hp in lebenspunkte.items()
if hp >= 100
}
The result, the strong opponents, is:
{
"Karl": 100,
"Dragon": 999,
}
Of course, both the key and the value can be calculated as well. So a dict comprehension is more than just a shorter way to copy a dictionary.
Set comprehensions
Sets can also be created with a comprehension:
richtungen = ["North", "South", "NORTH", "East"]
normalisiert = {
richtung.lower()
for richtung in richtungen
}
print(normalisiert)
Since a set contains each value at most once, "North" and "NORTH" merge into
the same entry after .lower().
One possible output is:
{'north', 'south', 'east'}
The order might look different. Sets don't have an order your program logic can rely on.
The notation is similar to a dict comprehension, but without a colon:
{ausdruck for element in iterable}
Truthiness: using values as conditions
Python can evaluate practically any object directly as a Boolean.
The built-in values that count as False include:
FalseNone- numeric zero values such as
0and0.0 - empty strings such as
"" - empty lists such as
[] - empty tuples such as
() - empty dictionaries such as
{} - empty sets such as
set()
Non-empty collections and numbers other than 0, on the other hand, normally
evaluate as True.
You can try this out with bool(...):
>>> bool([])
False
>>> bool(["torch"])
True
>>> bool("")
False
>>> bool("Dungeon")
True
>>> bool(0)
False
>>> bool(25)
True
>>> bool(None)
False
With your own classes, you can influence which Boolean their objects produce. For the data types we've used so far, the rules above are enough for now.
Checking collections without len(...)
Let's say you want to know whether the inventory contains at least one item.
This works:
if len(inventar) > 0:
print("You're carrying something.")
In Python, you usually write it directly:
if inventar:
print("You're carrying something.")
The code then says pretty much exactly what we want to know:
Is the inventory not empty?
For the opposite case, we use not:
if not inventar:
print("Your inventory is empty.")
Instead of:
if len(inventar) == 0:
print("Your inventory is empty.")
The shorter version doesn't merely save characters here. It expresses the intent more directly.
Using truthiness with care
Truthiness is handy, but it can lump together information that you might want to tell apart.
Let's say gold can contain either a number or None:
gold = None
This check doesn't distinguish between None and 0:
if not gold:
print("No gold available.")
Both values are falsy.
If you explicitly want to know whether no gold value has been set yet, you
check for None:
if gold is None:
print("The gold value hasn't been set yet.")
For the numeric value 0, on the other hand, you use:
if gold == 0:
print("You don't have any gold coins.")
So truthiness is the right choice when the different falsy values are allowed to mean the same thing to your code.
Conditional expressions
You can make a small decision directly inside an expression:
status = "alive" if hp > 0 else "defeated"
This notation is called a conditional expression.
It's equivalent to:
if hp > 0:
status = "alive"
else:
status = "defeated"
An example from our dungeon:
inventar_text = ", ".join(inventar) if inventar else "(empty)"
This form works well when both alternatives are short and only decide which value is used.
Several nested conditional expressions, on the other hand, quickly get unwieldy:
status = "dead" if hp <= 0 else "critical" if hp <= 25 else "ready"
You can read it, but a regular if-elif-else structure shows the cases
more clearly.
Slicing: excerpts from sequences
With slicing, you can select excerpts from sequences such as lists, tuples, or strings.
The basic form is:
sequenz[start:stop]
start is included, stop isn't.
zahlen = [0, 1, 2, 3, 4, 5]
print(zahlen[1:4])
The output is:
[1, 2, 3]
So we start at index 1 and stop before index 4. zahlen is German for
numbers.
Leaving out limits
If you leave out start, the slice begins at the beginning:
zahlen[:3]
Result:
[0, 1, 2]
If you leave out stop, it runs to the end:
zahlen[3:]
Result:
[3, 4, 5]
Both limits can be missing, too:
kopie = zahlen[:]
With a list, this creates a new list with the same elements.
Negative indexes
As with regular access to a sequence, you can use negative indexes when slicing. They count from the end:
zahlen = [0, 1, 2, 3, 4, 5]
print(zahlen[-2:])
Output:
[4, 5]
For an inventory, we could use this to select the two items added most recently, for example:
inventar = ["bread", "torch", "rope", "key"]
letzte_funde = inventar[-2:]
letzte_funde (latest finds) contains:
["rope", "key"]
If there are fewer than two elements, that's no problem either. A slice doesn't
run off the end of the sequence with an IndexError. It returns the elements
that are there.
The step when slicing
Slicing has a third value: the step.
sequenz[start:stop:schritt]
That lets us, for example, select every second element:
zahlen = [0, 1, 2, 3, 4, 5]
print(zahlen[::2])
Output:
[0, 2, 4]
A negative step runs in the other direction. This well-known shorthand reverses a sequence:
print(zahlen[::-1])
Output:
[5, 4, 3, 2, 1, 0]
This also works with strings:
wort = "Dungeon"
print(wort[:3])
print(wort[::-1])
Output:
Dun
noegnuD
Slicing creates a shallow copy of a list
With a list, a slice such as [:] creates a new list:
inventar = ["torch", "bread"]
kopie = inventar[:]
kopie.append("key")
print(inventar)
print(kopie)
Output:
['torch', 'bread']
['torch', 'bread', 'key']
kopie (copy) and inventar are two different lists. So adding the key changes
only kopie.
However, it's a shallow copy. If the elements themselves are mutable objects, they aren't copied as well:
raeume = [
{"name": "Hall"},
{"name": "Crypt"},
]
kopie = raeume[:]
kopie[0]["name"] = "Great hall"
print(raeume[0]["name"])
The output is:
Great hall
The two outer lists are different, but they contain references to the same dictionaries. So if we change one of these dictionaries, the change is visible through both lists.
enumerate: number and element together
Sometimes, when going through an iterable, we also need a running number.
We could work with indexes ourselves for that:
richtungen = ["north", "east", "west"]
for index in range(len(richtungen)):
print(f"{index + 1}) {richtungen[index]}")
That works, but it's more cumbersome than necessary. This is exactly what
enumerate(...) is for:
for nummer, richtung in enumerate(richtungen, start=1):
print(f"{nummer}) {richtung}")
The output is:
1) north
2) east
3) west
When you loop over it, enumerate(...) returns a pair of counter and current
element each time. Thanks to unpacking, both land directly in the matching
variables:
nummer, richtung
nummer and richtung mean number and direction. Without start=1, the
counter starts at 0 by default:
for index, richtung in enumerate(richtungen):
print(index, richtung)
Output:
0 north
1 east
2 west
Use enumerate(...) when you need the element and a running number. If the
number doesn't matter, the direct loop stays simpler:
for richtung in richtungen:
print(richtung)
zip: going through iterables in parallel
With zip(...), you can go through several iterables in parallel:
namen = ["Goblin", "Orc", "Dragon"]
lebenspunkte = [30, 50, 999]
for name, hp in zip(namen, lebenspunkte):
print(f"{name}: {hp} HP")
The output is:
Goblin: 30 HP
Orc: 50 HP
Dragon: 999 HP
On each pass, zip(...) combines the elements at the same position:
"Goblin" + 30
"Orc" + 50
"Dragon" + 999
More precisely, zip(...) creates tuples that we unpack again right away in the
for loop:
for name, hp in zip(namen, lebenspunkte):
zip(...) works lazily. It doesn't first build a complete list of all
pairs. Instead, it delivers them one by one as you loop.
By default, zip stops at the shortest iterable
If the inputs have different lengths, zip(...) stops by default as soon as the
shortest iterable is exhausted:
namen = ["Goblin", "Orc", "Dragon"]
lebenspunkte = [30, 50]
for name, hp in zip(namen, lebenspunkte):
print(name, hp)
Output:
Goblin 30
Orc 50
The dragon isn't processed. Python doesn't report an error.
If different lengths would mean a bug in your data, you can use strict=True:
for name, hp in zip(namen, lebenspunkte, strict=True):
print(name, hp)
If the lengths differ, zip(...) raises a ValueError.
Especially when two series of data must always have the same number of
elements, strict=True keeps missing values from being silently cut off.
Unpacking
With unpacking, you distribute values from an iterable across several names:
position = (3, 7)
x, y = position
After that:
x == 3
y == 7
Normally, the number of names has to match the number of values supplied:
x, y = (3, 7, 9)
Here, three values don't fit into two names. So Python raises a ValueError.
We've already used unpacking several times, for example with a dictionary's
.items() or together with enumerate() and zip().
Swapping values with unpacking
In Python, you can swap two values without a helper variable:
x = 3
y = 7
x, y = y, x
print(x, y)
Output:
7 3
The values on the right-hand side are evaluated before they're assigned to the names on the left-hand side. That way, neither of the two old values is lost.
Collecting several values with *
When unpacking, a name marked with * can take up several leftover values:
erster, *mitte, letzter = [1, 2, 3, 4, 5]
print(erster)
print(mitte)
print(letzter)
Output:
1
[2, 3, 4]
5
The name marked with * gets a list. The names mean first, middle, and last.
You can use this to break up an input, for example:
befehl, *argumente = "take old torch".split()
print(befehl)
print(argumente)
Output:
take
['old', 'torch']
Only one name can be marked with * in such an assignment.
For our dungeon, though, a different method is more practical, because we want to keep the rest of the input as a single string.
_ for values you don't need
The underscore _ is often used as the name for a value we deliberately don't
need.
The string method partition(...) splits a string into three parts:
befehl = "take torch"
verb, trennzeichen, argument = befehl.partition(" ")
After that:
verb == "take"
trennzeichen == " "
argument == "torch"
If we don't need the separator (trennzeichen), we usually write:
verb, _, argument = befehl.partition(" ")
In a regular script, _ has no special technical meaning. It's a perfectly
ordinary name. The notation only signals to other readers of the code:
We're deliberately not using this value.
In the interactive Python shell, _ can also stand for the result of the most
recently evaluated expression. Don't rely on that in regular scripts.
Splitting commands with partition(...)
partition(" ") fits commands that consist of a verb and an optional argument:
eingabe = "take old torch"
verb, _, argument = eingabe.partition(" ")
print(verb)
print(argument)
Output:
take
old torch
Unlike a plain split(), partition(...) splits only at the first occurrence
of the given separator. The rest of the text stays together. eingabe is German
for input.
If the separator isn't found at all:
eingabe = "inventory"
verb, _, argument = eingabe.partition(" ")
we get:
verb == "inventory"
argument == ""
partition(...) always returns three values. If the separator is missing, the
second and third values here are empty strings.
Since an empty string is falsy, we can then check directly:
if not argument:
print("No argument was given.")
any(...) and all(...)
Two more useful built-ins are any(...) and all(...).
any(...) checks whether at least one element of an iterable is truthy:
tuer_offen = False
fenster_offen = True
geheimgang_offen = False
if any([tuer_offen, fenster_offen, geheimgang_offen]):
print("There's an open exit.")
The three names mean door open, window open, and secret passage open. Since
fenster_offen is True, any(...) also gives True.
all(...), on the other hand, checks whether all elements are truthy:
hat_schluessel = True
hat_fackel = True
hat_karte = False
if all([hat_schluessel, hat_fackel, hat_karte]):
print("You're fully equipped.")
Here, the result is False because hat_karte (has map) is False.
For a few fixed conditions, and and or are usually more direct:
if hat_schluessel and hat_fackel:
print("You can move on.")
any(...) and all(...) become particularly handy when the values to check
come from an iterable anyway or are generated dynamically.
With empty iterables, there's one more difference that can seem surprising at first:
>>> any([])
False
>>> all([])
True
any(...) can't find a single true value in an empty list. For all(...), on
the other hand, there's also no value that violates the condition.
The dungeon, step 6
Now we apply some of the new idioms to the dungeon from part 5.
Among other things, we change the following:
- Exits and inventory are numbered with
enumerate(...). - We still check empty collections with truthiness—now we know what's behind it.
- We split commands into a verb and an argument with
partition(...). - With
take torch, you can pick up individual items. - A list comprehension finds rooms that still have loot in them.
- For short alternatives, we use a conditional expression.
Open dungeon.py and replace the previous content with the following code:
# Dungeon—Part 6: Writing Pythonic code
def erstelle_raeume():
"""Creates the dungeon's rooms and returns them."""
return {
"halle": {
"name": "Hall",
"beschreibung": (
"You're standing in a dark hall with a musty smell."
),
"ausgaenge": {
"north": "bibliothek",
"east": "krypta",
},
"gegenstaende": ["torch"],
},
"bibliothek": {
"name": "Library",
"beschreibung": (
"Dusty shelves full of tattered books surround you."
),
"ausgaenge": {
"south": "halle",
},
"gegenstaende": ["key"],
},
"krypta": {
"name": "Crypt",
"beschreibung": (
"Moisture glistens on the walls of the old crypt."
),
"ausgaenge": {
"west": "halle",
},
"gegenstaende": [],
},
}
def zeige_raum(raum):
"""Shows the current room, its exits, and visible items."""
print()
print(f"Location: {raum['name']}")
print(raum["beschreibung"])
print("Exits:")
for nummer, richtung in enumerate(raum["ausgaenge"], start=1):
print(f" {nummer}) {richtung}")
if raum["gegenstaende"]:
print("Lying here:", ", ".join(raum["gegenstaende"]))
def zeige_gegenstaende(raum):
"""Shows the items in the current room."""
gegenstaende = raum["gegenstaende"]
text = ", ".join(gegenstaende) if gegenstaende else "nothing useful"
print(f"Lying here: {text}")
def zeige_inventar(inventar):
"""Shows the inventory as a numbered list."""
if not inventar:
print("Your inventory is empty.")
return
print("Inventory:")
for nummer, gegenstand in enumerate(inventar, start=1):
print(f" {nummer}) {gegenstand}")
def nimm_gegenstand(raum, inventar, name):
"""Picks up a specific item and returns its name."""
gesuchter_name = name.strip().lower()
for gegenstand in raum["gegenstaende"]:
if gegenstand.lower() == gesuchter_name:
raum["gegenstaende"].remove(gegenstand)
inventar.append(gegenstand)
return gegenstand
return None
def erstelle_statuszeile(spieler):
"""Creates a compact status line for the player."""
return (
f"{spieler['name']} | "
f"HP: {spieler['hp']}/{spieler['max_hp']} | "
f"Gold: {spieler['gold']} | "
f"Inventory: {len(spieler['inventar'])}"
)
def zeige_status(spieler, besuchte_raeume):
"""Shows the player state and a few statistics."""
print(erstelle_statuszeile(spieler))
print(f"Rooms visited: {len(besuchte_raeume)}")
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 in them."""
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.")
spieler = {
"name": input("What's your name, adventurer? ").strip(),
"hp": 100,
"max_hp": 100,
"gold": int(input("How many gold coins are you bringing? ")),
"inventar": [],
}
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)
zeige_raum(raum)
raum_anzeigen = False
verb, argument = teile_befehl(input("\n> "))
if verb == "quit":
print("You leave the dungeon. Until next time.")
break
elif verb == "look":
zeige_gegenstaende(raum)
elif verb == "take":
if not argument:
print("What do you want to take?")
continue
gegenstand = nimm_gegenstand(
raum,
spieler["inventar"],
argument,
)
if gegenstand:
print(f"You take the {gegenstand}.")
else:
print(f"There's no item called {argument} here.")
elif verb == "inventory":
zeige_inventar(spieler["inventar"])
elif verb == "status":
zeige_status(spieler, 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 argument in raum["ausgaenge"]:
ort = raum["ausgaenge"][argument]
raum_anzeigen = True
else:
print(f"You can't go {argument}.")
elif verb in raum["ausgaenge"] and not argument:
ort = raum["ausgaenge"][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.
> 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.
> 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 became Pythonic?
Let's take a closer look at some of the changes.
Numbered exits with enumerate(...)
Instead of managing a counter ourselves:
nummer = 1
for richtung in raum["ausgaenge"]:
print(f"{nummer}) {richtung}")
nummer = nummer + 1
we use:
for nummer, richtung in enumerate(raum["ausgaenge"], start=1):
print(f" {nummer}) {richtung}")
We don't have to create the counter ourselves or increase it. The code says directly that we also need a running number while looping.
Truthiness for empty collections
The inventory is checked without len(...):
if not inventar:
print("Your inventory is empty.")
return
For a room's items, the collection itself is also enough as a condition:
if raum["gegenstaende"]:
print("Lying here:", ", ".join(raum["gegenstaende"]))
In both cases, all we care about is whether the collection is empty.
Early return
In zeige_inventar(...), we end the function immediately if there's nothing to
show:
if not inventar:
print("Your inventory is empty.")
return
After that, we can write the normal case without an additional else block:
print("Inventory:")
for nummer, gegenstand in enumerate(inventar, start=1):
print(f" {nummer}) {gegenstand}")
This keeps the function body flatter.
Splitting input with partition(...) and unpacking
The teile_befehl(...) function (split command) contains this line:
verb, _, argument = eingabe.strip().lower().partition(" ")
This turns
take old torch
into:
verb == "take"
argument == "old torch"
The separator itself ends up in _ because we don't need it.
Since we also call .lower() before partition(...), both the verb and the
argument are normalized. "TAKE TORCH" and "take torch" are treated the
same.
Finding rooms with loot using a comprehension
The function
def finde_raeume_mit_loot(raeume):
return [
raum["name"]
for raum in raeume.values()
if raum["gegenstaende"]
]
reads almost like a description of the desired result (its name means find rooms with loot):
Create a list with the names of all rooms whose list of items isn't empty.
As a regular loop, the same thing would look like this:
ergebnis = []
for raum in raeume.values():
if raum["gegenstaende"]:
ergebnis.append(raum["name"])
return ergebnis
Both versions are correct. Here, the comprehension stays easy to follow and covers exactly one transformation with a simple filter condition.
Conditional expression for short alternatives
In zeige_gegenstaende(...), we use:
text = ", ".join(gegenstaende) if gegenstaende else "nothing useful"
Written out, that would be:
if gegenstaende:
text = ", ".join(gegenstaende)
else:
text = "nothing useful"
Since we're only choosing between two short values, the conditional expression stays easy to read.
We do the same for the loot command:
text = ", ".join(raeume_mit_loot) if raeume_mit_loot else "(none)"
Pythonic doesn't always mean shorter
Some parts of the dungeon code deliberately stay as regular loops and conditions.
nimm_gegenstand(...) (take item), for example, searches the items step by
step:
for gegenstand in raum["gegenstaende"]:
if gegenstand.lower() == gesuchter_name:
raum["gegenstaende"].remove(gegenstand)
inventar.append(gegenstand)
return gegenstand
As soon as a matching item is found, it's removed from the room, put into the
inventory, and returned. The return that follows ends the loop immediately as
well.
You could write this code more compactly with other Python features. But that wouldn't automatically make it more understandable for beginners.
Pythonic code uses idioms where they express the intent more clearly. A regular loop isn't bad Python code just because there's a one-liner to be found somewhere.
Pitfalls
-
Overloading comprehensions: Several nested
forandifparts can make a comprehension hard to understand. A regular loop is then often the better choice. -
Using a comprehension only because it's short: Comprehensions are mainly suited to creating new collections. Complicated flows and side effects usually belong in a regular loop.
-
Using truthiness too broadly:
0,None,"", and empty collections are all falsy. Useis None, for example, if you have to tellNoneapart from0explicitly. -
Confusing slicing with a full copy:
liste[:]creates a shallow copy. Mutable objects inside the list aren't copied as well. -
Forgetting the exclusive stop limit: With
werte[1:4], the indexes1,2, and3are included, but index4isn't. -
Using
enumerate(...)even though the number isn't needed: If you only need the elements,for element in iterableis simpler. -
Overlooking inputs of different lengths with
zip(...): Withoutstrict=True,zip(...)stops at the shortest iterable. The remaining elements aren't processed. -
Thinking
_is special syntax: In a regular script,_is an ordinary name. Its meaning of “I don't need this value” is a convention. -
Forgetting the three return values of
partition(...): The method always returns the part before the separator, the separator itself, and the part after it. If the separator isn't found, the last two strings are empty. -
Treating
split()andpartition()the same:split()can break an input into any number of parts.partition(" ")splits exactly at the first space it finds and keeps the rest together. -
Nesting conditional expressions: As soon as several conditions are nested inside each other, a regular
if-elif-elseis usually easier to read.
Exercises
1. Filter long item names
Use a list comprehension to create a new list from the following list that contains only items with more than four characters:
gegenstaende = ["torch", "bread", "rope", "key"]
Solution
lange_namen = [
gegenstand
for gegenstand in gegenstaende
if len(gegenstand) > 4
]
print(lange_namen)
['torch', 'bread']
2. Print the inventory as a numbered list
Print the inventory numbered with enumerate(...). The numbering should start
at 1.
Solution
inventar = ["torch", "key", "bread"]
for nummer, gegenstand in enumerate(inventar, start=1):
print(f"{nummer}. {gegenstand}")
1. torch
2. key
3. bread
3. Show the last two items
Use slicing to print only the last two items in the inventory.
Solution
inventar = ["bread", "rope", "torch", "key"]
letzte_funde = inventar[-2:]
print(letzte_funde)
['torch', 'key']
4. Combine names and hit points
Combine the following lists with zip(...) and print each opponent together
with their hit points:
namen = ["Goblin", "Orc", "Dragon"]
lebenspunkte = [30, 50, 999]
The two lists should always be the same length.
Solution
for name, hp in zip(namen, lebenspunkte, strict=True):
print(f"{name}: {hp} HP")
Goblin: 30 HP
Orc: 50 HP
Dragon: 999 HP
Going deeper
Some aspects of Pythonic code can be checked and formatted automatically:
ruff: Linter and formatter in one
Next up
Our rooms and the player state still consist of nested dictionaries. That
works, but it allows typos in keys such as "max_hp", for example, and it
spreads data and the behavior that belongs to it across several places in the
program.
In part 7, we bundle both into classes. The player state, the rooms, and the items become objects of their own, with matching attributes and methods.
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.