Even more memory

An example: ecology simulator

Imagine we’re doing a simulation of animal behavior patterns, as in this Python file. We have a couple of classes:

@dataclass
class Habitat:
  food: int
  # could have other fields here--location, temperature, etc.

@dataclass
class Animal:
  time_since_food: int
  food_consumption: int
  habitat: Habitat

Each habitat is going to support multiple animals, who will consume the food in that habitat via this function:

def eat(animal: Animal):
    animal.habitat.food = animal.habitat.food - animal.food_consumption
    animal.time_since_food = 0

Let’s say we wanted to create a couple of animals sharing the same habitat, which will start with 50 food. How would we do it?

Option 1:

> animal1 = Animal(0, 10, Habitat(50))
> animal2 = Animal(0, 20, Habitat(50))

Option 2:

> animal1 = Animal(0, 10, Habitat(50))
> animal2 = Animal(0, 20, animal1.habitat)

Option 3:

> habitat = Habitat(50)
> animal1 = Animal(0, 10, habitat)
> animal2 = Animal(0, 20, habitat)

Option 4:

> food = 50
> animal1 = Animal(0, 10, Habitat(food))
> animal2 = Animal(0, 20, Habitat(food))

See the lecture capture for the details.

Circular references

Let’s say we want a Habitat to have a list of all of the animals in that habitat:

@dataclass
class Habitat:
  food: int
  animals: list

We could try to set this up as follows:

habitat = Habitat(50, [Animal(0, 10,

We get stuck here–what should we list as the habitat of the animal we’re building?

We can do something like this:

habitat = Habitat(50, [])
habitat.animals.append(Animal(0, 10, habitat))
habitat.animals.append(Animal(0, 20, habitat))

We have circular references here–the habitat references each animal, and each animal references the habitat. It can be helpful to draw arrows in memory to keep track of this (see the lecture capture).

We could write a function to do this:

def animal_in_habitat(habitat: Habitat, food_consumption: int) -> Animal:
  animal = Animal(0, food_consumption, habitat)
  h.animals.append(animal)
  return animal

How would we test this function?

def test_animal_in_habitat():
  h = Habitat(50, [])
  a = animal_in_habitat(h, 10)
  #test("habitat correct", h, Habitat(50, [Animal(0, 10, ...)]))
  test("habitat has animal", h.animals. [a])
  test("animal has habitat", a.habitat, h)

Dictionaries in memory

In memory, dictionaries work very much like lists: when we build a new dictionary we add it to memory, and its keys/values work much like individual indexes in a list.

Going back to our Coord example, what happens when we execute the following code?

@dataclass
class Coord:
    x: int
    y: int

objects = {
    "tree": Coord(1, 2),
    "rock": Coord(4, 3)
}

objects["rock"].x = 5
objects["bird"] = objects["tree"]