More trees, introduction to objects

Mutating trees

We developed one last function on trees:

def replace_text(tree: HTMLTree, find: str, replace: str):
    for child in tree.children:
        replace_text(child, find, replace)
    if tree.tag == "text":
        tree.text = tree.text.replace(find, replace)

See the lecture capture for details.

Objects

Imagine you’re a DJ at a radio station. The station is very democratic–you only play songs that your listeners call in and request. In addition, every thousandth listener who calls in gets a prize! You want to keep track of the queue of songs you want to play, as well as enough information to give out prizes.

We could implement this with a custom data type like this:

@dataclass
class DJData:
  num_callers: int
  queue: list

We can implement a function to update our data and to figure out what we’re going to say to a listener:

def request(data: DJData, caller: str, song: str) -> str:
  data.queue.append(song)
  data.num_callers += 1
  if data.num_callers % 1000 == 0:
    return "Congrats, " + caller + "! You get a prize!"
  else:
    return "Cool, " + caller

So here we’ve got a datatype and a function that reads and modifies that datatype’s contents. We can see how it works:

> djdata = DJData(0, [])
> request(djdata, "Doug", "Bulls on Parade")
"Cool, Doug"

We could have written this slightly differently:

@dataclass
class DJData:
  num_callers: int
  queue: list

  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

We’ve put the request function inside the definition of DJData. We’ve also modified the method a bit: instead of taking a data argument, we’ve called the argument “self” and left off the type annotation. This function is now a method on the DJData class.

We’ll call it slightly differently, too:

> djdata = DJData(0, [])
> djdata.request("Doug", "Guerilla Radio")
"Cool, Doug"

We call methods by writing the name of an object (djdata, in this case), then a dot, then the method arguments--excluding self. Since we’re not passing self in, how does Python know which object to call the method on?

We’ll keep learning about classes, objects and methods. I want to emphasize, though, that you’ve seen this before. We’ve called methods on lists, sets, etc.–for instance, l.append(2)). What we’re seeing now is how to add methods to our custom objects!

The init method

Up until now we’ve been using the dataclasses library to make our custom datatypes work more like Pyret’s. From this point, we will generally not use it, so that we can see how Python’s objects work. Instead, we’ll use __init__() methods to initialize data on objects:

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

Python calls the __init__ method in order to initialize an object’s fields when it is created. We can construct instances of the DJData class like this:

> djdata = DJData()
> djdata.request("Doug", "Ironic")
"Cool, Doug"