Skip to content

Commit cf3f654

Browse files
committed
add more OOP stuff and get rid of boring bank example
1 parent aa3cbf6 commit cf3f654

26 files changed

+1676
-210
lines changed

classes/abc.rst

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@ properties:
3434
def is_alive(self):
3535
return True
3636
37-
--------------
3837
39-
Exercise
40-
--------
38+
Exercise 1: Abstract Animals
39+
----------------------------
4140

4241
Implement the Dog class so that the code below runs.
4342

@@ -52,3 +51,31 @@ Implement the Dog class so that the code below runs.
5251
print(rex.is_alive())
5352
rex.make_noise()
5453
print(rex.species())
54+
55+
Exercise 2: Descriptors
56+
------------------------
57+
58+
The example in :download:`descriptors.py` uses the **descriptor protocol**, a mechanism to control the getting and setting of attributes using object composition.
59+
60+
Create both valid and invalid instances of the defined class.
61+
62+
63+
Exercise 3: Game Objects
64+
------------------------
65+
66+
Create a superclass `GameObject` for the classes in your game.
67+
Use the following example code:
68+
69+
.. code:: python3
70+
71+
from abc import ABC, abstractmethod
72+
73+
class Command(ABC):
74+
75+
def __init__(self, name):
76+
self.name = name
77+
78+
@abstractmethod
79+
def executed(self):
80+
pass
81+

classes/class_dataclass.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from dataclasses import dataclass, field
2+
3+
4+
@dataclass
5+
class Level:
6+
name: str
7+
level: str
8+
level_matrix: list[list[str]] = field(init=False, repr=False)
9+
10+
def __post_init__(self):
11+
self.level_matrix = [list(row) for row in self.level.strip().split()]
12+
13+
def __repr__(self) -> str:
14+
return "\n".join(["".join(row) for row in self.level_matrix])
15+
16+
17+
level_one = Level(
18+
name="empty level",
19+
level="""
20+
#############
21+
#...........#
22+
#...........#
23+
#...........#
24+
#...........#
25+
#...........#
26+
#...........#
27+
#...........#
28+
#...........#
29+
#...........#
30+
#...........#
31+
#############""",
32+
)
33+
34+
print(level_one)

classes/class_pydantic.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from typing import Annotated
2+
from pydantic import BaseModel, BeforeValidator, ValidationError
3+
4+
5+
def parse_level(level: str) -> list[list[str]]:
6+
return [list(row) for row in level.strip().split()]
7+
8+
9+
class Level(BaseModel):
10+
name: str
11+
level: Annotated[list[list[str]], BeforeValidator(parse_level)]
12+
13+
def __str__(self) -> str:
14+
return "\n".join("".join(row) for row in self.level)
15+
16+
class Config:
17+
extra = "forbid" # Disallow unknown fields
18+
19+
20+
level_one = Level(
21+
name="empty level",
22+
level="""
23+
#############
24+
#...........#
25+
#...........#
26+
#...........#
27+
#...........#
28+
#...........#
29+
#...........#
30+
#...........#
31+
#...........#
32+
#...........#
33+
#...........#
34+
#############""", # type: ignore
35+
)
36+
37+
print(level_one)

classes/class_typedict.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from typing import TypedDict, List
2+
3+
4+
class Level(TypedDict):
5+
name: str
6+
level: List[List[str]]
7+
8+
9+
def create_level(name: str, level: str) -> Level:
10+
level_matrix = [list(row) for row in level.strip().split()]
11+
return Level(name=name, level=level_matrix)
12+
13+
14+
def print_level(level: Level) -> None:
15+
print("\n".join("".join(row) for row in level["level"]))
16+
17+
18+
level_one = create_level(
19+
name="empty level",
20+
level="""
21+
#############
22+
#...........#
23+
#...........#
24+
#...........#
25+
#...........#
26+
#...........#
27+
#...........#
28+
#...........#
29+
#...........#
30+
#...........#
31+
#...........#
32+
#############""",
33+
)
34+
print(level_one["name"])
35+
print_level(level_one)
36+
print(type(level_one))

classes/class_vanilla.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class Level:
2+
3+
def __init__(self, name: str, level: str):
4+
self.name = name
5+
self.level = [list(row) for row in level.strip().split()]
6+
7+
def __repr__(self) -> str:
8+
return "\n".join(["".join(row) for row in self.level])
9+
10+
11+
level_one = Level(
12+
name="empty level",
13+
level="""
14+
#############
15+
#...........#
16+
#...........#
17+
#...........#
18+
#...........#
19+
#...........#
20+
#...........#
21+
#...........#
22+
#...........#
23+
#...........#
24+
#...........#
25+
#############""",
26+
)
27+
print(level_one)

classes/classes.png

304 KB
Loading

classes/classes.rst

Lines changed: 38 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Classes
44
What are classes?
55
-----------------
66

7+
.. figure:: classes.png
8+
79
Classes are a tool to manage complexity in a program. They group two
810
things in a single structural unit: **attributes (data)** and **methods
911
(behavior)**.
@@ -13,8 +15,7 @@ so that your main program becomes simple. In my opinion, this way of
1315
structuring code should be the main motivation to using classes in
1416
Python.
1517

16-
In this article, you find an example how to use a class to structure
17-
your code.
18+
In this chapter, you find an example how to use a class to structure your code.
1819

1920
--------------
2021

@@ -27,116 +28,83 @@ To define a class, you need to define three things:
2728
- define attributes (variables that belong to the class)
2829
- define methods (functions that belong to the class)
2930

30-
In the code below, a class for a bank account is defined:
31-
32-
.. code:: python3
33-
34-
class Account:
35-
"""
36-
Account of a bank client.
37-
"""
38-
def __init__(self, owner, start_balance=0):
39-
self.name = owner
40-
self.balance = start_balance
41-
42-
def deposit(self, amt):
43-
self.balance += amt
31+
In the code below, a class for a **Planet** is defined:
4432

45-
def withdraw(self, amt):
46-
self.balance -= amt
33+
.. literalinclude:: planet.py
4734

48-
The class ``Account`` contains two attributes (``name`` and ``balance``)
49-
and two methods (``deposit`` and ``withdraw``).
35+
The class ``Planet`` contains three attributes
36+
and two methods.
5037

5138
Note that you need to add the word ``self`` every time you refer to an
5239
attribute. You also must use ``self`` as the first parameter in every
5340
method of a class.
5441

55-
--------------
56-
5742
Creating Objects
5843
----------------
5944

6045
To use a class, you need to create an object from it first. Objects are
6146
*“live versions”* of a class, the class being an idealized abstration
6247
(in the sense of `Platos Theory of Forms <https://en.wikipedia.org/wiki/Theory_of_forms>`__).
63-
If you think of **BankAccount** as a class, the actual accounts of **Ada Lovelace** and **Mahatma Gandi**
48+
If you think of **Planet** as a class, the actual planets **Earth** and **Pandalor**
6449
would be the objects of that class.
6550

6651
You can create multiple objects from a class, and each objects has its
67-
own, independent attributes (e.g. if **BankAccount** has an attribute **balance**,
68-
then **Ada** and **Mahatma** could have a different amount of money).
69-
70-
Syntactically, you can think of a class as a function that returns
52+
own, independent attributes. Syntactically, you can think of a class as a function that returns
7153
objects. (This is a gross oversimplification to what textbooks on
7254
classes say, but in Python it is more or less what happens).
7355

74-
To create ``Account`` objects, you need to call the class. Creating an
56+
To create ``Planet`` objects, you need to call the class. Creating an
7557
object will automatically call the constructor ``__init__(self)`` with
7658
the parameters supplied.
7759

7860
.. code:: python3
7961
80-
a = Account('Ada Lovelace', 1234)
81-
m = Account('Mahatma Gandhi', 10)
62+
earth = Planet(name="Earth", description="the blue planet")
63+
pandalor = Planet(name="Pandalor", description="home of the space pandas")
64+
arcturus = Planet(name="Arcturus", description="an icy planet, home of penguins")
65+
8266
8367
Then you can access the attributes like any variable using the dot (``.``) syntax:
8468

8569
.. code:: python3
8670
87-
print(a.name)
88-
print(m.balance)
71+
print(earth.name)
72+
print(earth.balance)
73+
74+
Exercise: Methods
75+
-----------------
8976

9077
And you can call methods in a similar way:
9178

9279
.. code:: python3
9380
94-
a.deposit(100)
95-
a.withdraw(10)
96-
print(a.balance)
81+
earth.add_connection(pandalor)
82+
earth.add_connection(arcturus)
9783
98-
Note that these methods modify the state of *Adas* account object, but
99-
not *Mahatmas*.
84+
earth.show_connections()
10085
101-
--------------
102-
103-
Making classes printable
104-
------------------------
105-
106-
One disadvantage of classes is that when you print an object, you will
107-
see something like this:
108-
109-
::
110-
111-
<__main__.Account at 0x7f64519d8438>
86+
Note that these methods modify the state of the planet *Earth*, but not the other two.
11287

113-
A good workaround is to add a special method, ``__repr__(self)`` to the
114-
class that returns a string. This method will be called every time a
115-
string representation is needed: when printing and object, when an
116-
object appears inside a list or in error messages.
88+
Implement the `show_connections()` method using the code from the program `space_game.py`.
11789

118-
Typically, you would build a short string in ``__repr__(self)`` that
119-
describes the object:
90+
Exercise: Refactor using classes
91+
--------------------------------
12092

121-
.. code:: python3
122-
123-
def __repr__(self):
124-
return f"<Account of '{self.name}' with {self.balance} galactic credits>"
125-
126-
With this method defined, the instruction
127-
128-
.. code:: python3
93+
Simplify `space_game.py` using the `Planet` class.
12994

130-
print(a)
13195

132-
would result in the output
96+
Four Ways to create classes
97+
---------------------------
13398

134-
::
99+
Python knows multiple flavors of defining and using classes.
100+
These are recent developments (~2018+), strongly relying on the availability of **Type Hints**.
135101

136-
<Account of 'Ada Lovelace' with 1324 galactic credits>"
102+
Execute and examine the following code examples, defining a level for a point-eating game:
137103

138-
It is a good idea to implement ``__repr__(self)`` as the first method in
139-
a new class.
104+
- :download:`class_vanilla.py`
105+
- :download:`class_typedict.py`
106+
- :download:`class_dataclass.py`
107+
- :download:`class_pydantic.py`
140108

141109
--------------
142110

@@ -151,9 +119,9 @@ the same purpose equally well.
151119

152120
Another motivation for using classes you find in textbooks is
153121
**encapsulation**, isolating parts of your program from the rest.
154-
Encapsulation does not exist in Python (e.g. you cannot declare parts of
122+
Encapsulation does not exist in Python (e.g. you cannot declare parts of
155123
a class as ``private`` in a way that cannot be circumvented). If you
156-
depend on your code being strictly isolated from other parts (e.g. in a
124+
depend on your code being strictly isolated from other parts (e.g. in a
157125
security-critical application or when organizing a very large program),
158126
**consider other programming languages than Python.**
159127

classes/composition.png

348 KB
Loading

0 commit comments

Comments
 (0)