← Back to course
Python 11-12 / Week 04 / Thursday
4/6
Week 04 · Objects Working Together

Thursday

Why give back a copy
// Names, references and objects that hold objects
⏱ about 30 min

Thursday: Why Give Back a Copy

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?"

A leak in the wall

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")
PREDICT LEAK.PY
  • Read the question.
  • Tap your answer.
What does the first line show?
What does the Total line show?
0 samples left
Total: 0 g

The fix: hand out a copy

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")
PREDICT WITH THE REAL ROVER.PY
  • Read the question.
  • Tap your answer.
With from rover import Rover, what does the first line show?

How shallow is the copy?

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])
PREDICT SHALLOW.PY
  • Read the question.
  • Tap your answer.
What does the first line show?
What does the second line show?
1 2
S01 basalt 41 g at (0, 2)
True
StatementTrue 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.?
WHY THIS EXERCISEA copy is a new list of the same objects. Changing the copy's list is safe, but changing an object inside shows in both.

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()
What kind of error does the slip cause? Name it.
WHY THIS EXERCISElen() was given a method, not a list. A TypeError means an operation got the wrong type of value.
At your computer
1. In your fieldsim folder, save a copy of rover.py named rover_leaky.py. In the copy only, change get_samples to return self._samples.
2. Type leak.py with from rover_leaky import Rover. Run it and check for 0 samples left.
3. Change the import line to from rover import Rover. Run it and check for 2 samples left.
4. Remove the brackets after get_samples on the len line. Run it, read the TypeError, then put the brackets back.

You found and fixed a leak, which is real defensive design. Tomorrow is Trace Friday with lists of objects.

← Wednesday