From Dictionaries to Objects
By Zed A. Shaw
Date: Apr 17, 2024
Sample Chapter is provided courtesy of Addison Wesley.
You should review the following exercises before doing this one:
Exercise 24, Introductory Dictionaries, to refresh your understanding of Dictionaries
Exercise 25, Dictionaries and Functions, for how you can put functions in dictionaries and call them
Exercise 26, Dictionaries and Modules, for how modules are just dictionaries behind the scenes and how changing the underlying __dict__ changes the module
In this exercise you’ll learn about Object-Oriented Programming by creating your own little object system using the previous information.
Step 1: Passing a Dict to a Function
Imagine you want to record information about people and then have them say things. Maybe this is a little video game with little people growing food in a small town. You want to know their name, age, and hair color. You also need a way to make them talk. Using what you know, you may invent this code:
Listing 44.1: ex44_1.py
1 becky = {
2 "name": "Becky",
3 "age": 34,
4 "eyes": "green"
5 }
6
7 def talk(who, words):
8 print(f"I am {who['name']} and {words}")
9
10 talk(becky, "I am talking here!")
Let’s break this down to confirm you understand the code:
I create a becky variable that has all the information about Becky
I then have a function named talk that accepts this becky variable and prints out a little dialogue from that character
Then I call this function, passing in the becky variable and something for Becky to say
What’s interesting is you can use this function on anything that has the same “signature” as the becky variable. If you created 1,000 characters with this same dict, the talk() function would still work.
What You Should See
When you run the code for Step 1, you should see this output:
1 I am Becky and I am talking here!
This doesn’t change in the later versions of this code.
Step 2: talk inside the Dict
The first problem with this design is any part of your code that wants to make these characters talk has to know about the talk() function. That’s not too large a problem, but maybe you want each character to have special talk() functions that do something different.
One way to fix this is to “attach” the talk() function to the dict itself like this:
Listing 44.2: ex44_2.py
1 def talk(who, words):
2 print(f"I am {who['name']} and {words}")
3
4 becky = {
5 "name": "Becky",
6 "age": 34,
7 "eyes": "green",
8 "talk": talk # see this?
9 }
10
11 becky['talk'](becky, "I am talking here!")
The differences between this version and the first one are:
I move the talk() function to the top so we can reference it later.
I then put the function in the becky dictionary with "talk": talk. Remember, a function is just like any other variable, which means you can pass it to other functions and place it in a list or dict
Then the last line calls the talk() function in one move.
That last line might cause you problems, so let’s break just that line down:
becky['talk']: Python gets the contents of the becky dictionary assigned the 'talk' key. It’s exactly like if you did print(becky['age']) to get the age key of becky. Don’t get confused by the characters after this.
(becky, "I am talking here!"): You know that Python sees () after a variable as a function call, and you just got the contents of becky['talk'], so this calls those contents as a function. It then passes the variable becky to it and the string "I am talking here!"
You can take the next step to study this code by breaking it apart into two lines of code like this:
1 becky_talk = becky['talk'] 2 becky_talk(becky, "I am talking here!")
What confuses people is they see all those characters in the original “one-liner” and their brain treats it like one big word. The way you analyze these is to break them apart into separate lines using variables.
Step 3: Closures
The next thing to learn is the concept of a “closure.” A closure is any function that’s created inside another function but accesses data in its parent. Let’s look at this code to see a closure in action:
Listing 44.3: ex44_3.py
1 # this function makes functions
2 def constructor(color, size):
3 print(">>> constructor color:", color, "size:", size)
4
5 # watch the indent!
6 def repeater():
7 # notice this function is using color, size
8 print("### repeater color:", color, "size:", size)
9
10 print("<<< exit constructor");
11 return repeater
12
13 # what's returned are repeater functions
14 blue_xl = constructor("blue", "xl")
15 green_sm = constructor("green", "sm")
16
17 # see how these repeaters "know" the parameters?
18 for i in range(0,4):
19 blue_xl()
20 green_sm()
Breaking down this code, we have the following:
I start a function named def constructor(color, size), which will create functions for me.
I start off with a simple print() to trace this function.
Then I define the repeater() function, but notice it gets indented under constructor. This places that function inside constructor so that it’s only usable in that block.
Under def repeater(), I do a print(), but carefully look at what this print() line is using. It’s using the variables color and size from the def constructor(color, size), but those are function parameters. That means they’re temporary, and when constructor exits, they “die,” right? Nope.
Then I print another tracing line saying the constructor is exiting.
I return repeater() so the caller can have it, but remember color and size should be dead, right? Isn’t this an error?
After I’ve defined constructor, I use it to craft two repeater() functions named blue_xl and green_sm
I then have a for-loop that uses those two functions to repeatedly print the correct size and color I gave to constructor
This means that functions created inside other functions keep access to the variables they use.
The key here is how def repeater() is indented under the def constructor but tries to use color and size. Python detects this and creates a closure, which is a function that keeps references to any variables it used. These references are retained even when the parent function has long exited.
What You Should See
When you run this closure code, you should see the following output:
1 >>> constructor color: blue size: xl 2 <<< exit constructor 3 >>> constructor color: green size: sm 4 <<< exit constructor 5 ### repeater color: blue size: xl 6 ### repeater color: green size: sm 7 ### repeater color: blue size: xl 8 ### repeater color: green size: sm 9 ### repeater color: blue size: xl 10 ### repeater color: green size: sm 11 ### repeater color: blue size: xl 12 ### repeater color: green size: sm
If you get something different, review your constructor() to make sure you’ve indented things correctly.
Step 4: A Person Constructor
What happens when you want to create 100 people? In the Step 2 code you’d have to manually create every dict and put the talk() function in it, which is ridiculous. We have computers for repetitive boring work, so let’s use what we know so far to create a new constructor() for our people.
We’ll use everything you know so far to create a function that “constructs” people:
Listing 44.4: ex44_4.py
1 def Person_new(name, age, eyes):
2 person = {
3 "name": name,
4 "age": age,
5 "eyes": eyes,
6 }
7
8 def talk(words):
9 print(f"I am {person['name']} and {words}")
10
11 person['talk'] = talk
12
13 return person
14
15 becky = Person_new("Becky", 39, "green")
16
17 becky['talk']("I am talking here!")
This code is using the following concepts:
Person_new() is a constructor, which means it creates a new person dict and attaches the talk() function to it.
The talk() function is a closure, which means it has access to the person that is created at the top of the Person_new() function.
It adds this talk() function to the person just like you did in Step 2, but since this is a closure from Step 3, we don’t have to manually give it the person
It returns this new person with its closure-based talk(), and then we can use it just like before, but it’s a bit cleaner.
If we compare the Step 2 final line with this line, we have the following:
1 # from step 2, see the two becky uses?
2 becky['talk'](becky, "I am talking here!")
3
4 # from step 4, now only one becky
5 becky['talk']("I am talking here!")
With the Person_new() constructor, we can remove this extra becky variable, which makes it far more reliable to use. This also means we could potentially give different kinds of people different talk() functions if we wanted.
Study Drills
Use Person_new() to create a few more people.
Add a new function hit(), which makes one person hit another person.
Give people hit points in their dict and have hit() randomly reduce each person’s hit points. You’ll need random() for this.
Add a job attribute to person and give different jobs different hit points, damage, and dialogue. For example, a “boxer” would have more HP and damage than a person with the job “baby.” Python has way better tools for this same problem, but for this code it is a fun challenge.
Finally, have your code run a little fight club using loops to make different people battle.