# How Programs with Objects Execute
We finished last class with this `DJData` class.
```python
class DJData:
def __init__(self):
self.queue = []
self.num_callers = 0
def request(self, caller: str, song: str) -> str:
self.queue.append(song)
self.num_callers += 1
if self.num_callers % 1000 == 0:
return "Congrats, " + caller + "! You get a prize!"
else:
return "Cool, " + caller
```
The big difference compared to what we've done before is, the `request` function is bundled into the `DJData` class, instead of being some external thing that operates on a `DJData` object.
One thing that changes when we use non-`dataclass` classes is that we can (or even have to) implement our own versions of things like `__init__`. These methods need to take a `self` argument, but otherwise, they can do whatever we want them to do.
Requesting songs alone isn't so interesting. We should also be able to play the songs that have been requested! Let's extend our `DJData` class.
```python
class DJData:
def __init__(self):
self.queue = []
self.num_callers = 0
def request(self, caller: str, song: str) -> str:
self.queue.append(song)
self.num_callers += 1
if self.num_callers % 1000 == 0:
return "Congrats, " + caller + "! You get a prize!"
else:
return "Cool, " + caller
def play(self) -> str:
song = self.queue[0]
del self.queue[0]
return song
```
But this isn't good enough. What if you try to play a song when the queue is empty? Here we face a design decision, with a number of possible solutions. One possibility -- a vague approximation to what a radio station might do -- is to have a default song to play when nothing is on the queue. Of course, different DJs will have different default songs. So we should store the default in our class, and be able to set it.
```python
class DJData:
def __init__(self, default: str):
self.queue = []
self.num_callers = 0
self.default = default
def request(self, caller: str, song: str) -> str:
self.queue.append(song)
self.num_callers += 1
if self.num_callers % 1000 == 0:
return "Congrats, " + caller + "! You get a prize!"
else:
return "Cool, " + caller
def play(self) -> str:
if len(self.queue) == 0:
return self.default
else:
song = self.queue[0]
del self.queue[0]
return song
```
Now if we play a song when the queue is empty, we'll get the DJ's default song as output:
```python
dj1 = DJData("Once in a Lifetime")
dj1.play() # "Once in a Lifetime"
```
This kind of approach is really really powerful: we've set up an abstraction barrier between users of `DJData` and the implementation of its functionality. Users need to know how to create one, and that they can `request` and `play` songs. But they don't need to know anything about what data is really stored in a DJData, or how these methods are implemented.
----
Now that we've made a case for putting up abstraction barriers: let's tear some barriers down and see what's going on in memory!
Using the class we defined above, suppose we introduce two `DJData`s:
```python
dj1 = DJData("Once in a Lifetime")
dj2 = DJData("Respect")
```
This is represented in our memory and program dictionary:
**Memory**
| Location | Data |
| --- | --- |
| loc 1 | `DJData(queue=loc 2, num_callers=0,default="Once in a Lifetime")` |
| loc 2 | `[]` |
| loc 3 | `DJData(queue=loc 4, num_callers=0,default="Respect")` |
| loc 4 | `[]` |
**Program dictionary**
| Name | Value |
| --- | --- |
| `dj1` | loc 1 |
| `dj2` | loc 3 |
The data of `dj1` is stored in memory at `loc 1`. All the values that we assigned to it using `self` are seen in the constructor: `num_callers` and `default_song` are atomic, while `queue` is a reference to another location in memory. The program dictionary associates the name `dj1` to this `DJData` stored at `loc 1`, and similarly for `dj2`.
What happens if we call `dj1.request("Rob", "The Final Countdown")`?
First, we see in the program dictionary that `dj1` is stored at loc 1. Checking loc 1, we see that it is a `DJData` object. Thus, to resolve the function name `request`, we look for the method with this name in the `DJData` class. This method takes three arguments -- `self`, `caller`, `song` -- so we add these to our program dictionary. Since we called this function on `dj1`, we assign the value of `dj1` to be the value of `self`. The values of `caller` and `song` are our two arguments, "Rob" and "The Final Countdown" respectively.
**Program dictionary**
| Name | Value |
| --- | --- |
| `dj1` | loc 1 |
| `dj2` | loc 3 |
| `self` | loc 1 |
| `caller` | "Rob" |
| `song` | "The Final Countdown" |
The first command in `request` is `self.queue.append(song)`. `self` resolves to loc 1; in loc 1, the `queue` field points to loc 2. `song` resolves to "The Final Countdown." So calling `append("The Final Countdown")` mutates the list at loc 2:
**Memory**
| Location | Data |
| --- | --- |
| loc 1 | `DJData(queue=loc 2, num_callers=0,default="Once in a Lifetime")` |
| loc 2 | `["The Final Countdown"]` |
| loc 3 | `DJData(queue=loc 4, num_callers=0,default="Respect")` |
| loc 4 | `[]` |
The next command in `request` is `self.num_callers += 1`. Again, `self` resolves to loc 1, where the `num_callers` field is 0. `+= 1` updates this in place:
**Memory**
| Location | Data |
| --- | --- |
| loc 1 | `DJData(queue=loc 2, num_callers=1,default="Once in a Lifetime")` |
| loc 2 | `["The Final Countdown"]` |
| loc 3 | `DJData(queue=loc 4, num_callers=0,default="Respect")` |
| loc 4 | `[]` |
The conditional check evaluates to `False`, so we end up in the `else` branch: `return "Cool, " + caller`. From our program table, `caller` is `"Rob"` so we return the string `"Cool, Rob"`.
Exercise: run through the full execution of calling `dj1.play()` and `dj2.play()`!
---
We'll almost never work through the memory model of a program execution at this level of detail. But it's helpful to know what's going on, especially if you come across unexpected behavior and need to debug. But also note that there's nothing going on here that we haven't seen already! Really all that's new in this execution trace is the `self` name, which gets automatically bound to the thing before the dot. The real value of classes is "over the hood": they're a tool for us, as programmers, to organize our data and constrain the ways that programs interact with it.