Comet writes a quick report program. It gets Pip's samples, prints them, then clears its own list to start the next report.
She runs it twice and stares. "The second report says Pip has nothing. Where did the samples go?"
Wren reads the code line by line. "What do you notice?" he asks. "You cleared found. Is found Pip's list, or a list of your own?"
Nova projects two arrows, one from found and one from inside Pip. "Would you like a hint?" she asks. "Check what get_samples hands back."
"Lead programmer," Comet says, "can you prove which version leaks?"
Suppose get_samples() gave back self._samples itself, not a copy. The caller would get an alias of Pip's own list.
Anything the caller does to that list would happen to Pip's list too. Here is a copy of rover.py with that bug, saved as rover_leaky.py:
def get_samples(self):
return self._samples # leak.py
from rover_leaky import Rover
from sample import Sample
pip = Rover("Pip")
pip.pick_up(Sample("S01", "basalt", 42, 0, 2))
pip.pick_up(Sample("S02", "quartz", 18, 2, 4))
found = pip.get_samples()
found.clear()
print(len(pip.get_samples()), "samples left")
print("Total:", pip.total_grams(), "g") 0 samples left Total: 0 g
An accessor method should give back a copy of a value, not the object's own data. The canonical get_samples returns list(self._samples):
The same rule works going in. A constructor or method that keeps a list it is handed should keep a copy.
Then outside code cannot change the object by accident.
def get_samples(self):
return list(self._samples) # leak.py
from rover import Rover
from sample import Sample
pip = Rover("Pip")
pip.pick_up(Sample("S01", "basalt", 42, 0, 2))
pip.pick_up(Sample("S02", "quartz", 18, 2, 4))
found = pip.get_samples()
found.clear()
print(len(pip.get_samples()), "samples left")
print("Total:", pip.total_grams(), "g") list.copy() gives a shallow copy of a list, and list(x) also makes a new list. The shell shows both are new lists.
Shallow means the list is new, but the objects inside it are the same objects. shallow.py shows that.
>>> grams = [42, 18] >>> a = list(grams) >>> b = grams.copy() >>> a == grams True >>> a is grams False >>> b is grams False
# shallow.py
from sample import Sample
bag = [Sample("S01", "basalt", 42, 0, 2)]
copy_bag = list(bag)
copy_bag.append(Sample("S02", "quartz", 18, 2, 4))
copy_bag[0].grams = 41
print(len(bag), len(copy_bag))
print(bag[0].describe())
print(copy_bag[0] is bag[0]) 1 2 S01 basalt 41 g at (0, 2) True
| Statement | True or false? |
|---|---|
| list(x) makes a new list. | ? |
| A shallow copy also copies every object inside the list. | ? |
| The canonical get_samples() hands back Pip's own _samples list. | ? |
| Clearing a copy leaves the original list alone. | ? |
An error to read: here is leak.py with one slip: len(pip.get_samples) has no brackets after get_samples. The method is never called.
Traceback (most recent call last):
File "leak.py", line 10, in <module>
print(len(pip.get_samples), "samples left")
~~~^^^^^^^^^^^^^^^^^
TypeError: object of type 'method' has no len() You found and fixed a leak, which is real defensive design. Tomorrow is Trace Friday with lists of objects.