Names are labels, not boxes: aliasing and copies
Why changing one Python list can change another: names are labels on objects. See aliasing, shallow and deep copies, and the mutable default argument trap.
It's tempting to picture a variable as a box with a value inside. In Python that picture goes wrong. A better one: objects live on their own, and a name is a label tied to one of them. Assignment ties a label; it never copies the object.
weekend = ["laundry", "call gran"]
todo = weekend # a second label on the same list
todo.append("bake bread")
print(weekend) # ['laundry', 'call gran', 'bake bread']Two names, one object: this is called aliasing. It's often exactly what you want, and it's a puzzle when you didn't expect it.
Mutating versus rebinding
There are two very different ways to "change a variable":
- mutate the object:
xs.append(4),xs[0] = 9,d["k"] = 1. Every label on that object sees the change; - rebind the name:
xs = something_else. Only that one label moves; the old object, and any other labels on it, are untouched.
nums = [1, 2] other = nums nums += [3] # for a list, += mutates: other sees it print(other) # [1, 2, 3] nums = nums + [4] # + builds a new list, then rebinds nums print(other, nums) # [1, 2, 3] [1, 2, 3, 4]
+= mutates a list in place but has to make a new object for an int, string or tuple, since those can't change. Check it with id() before and after.
Function arguments are labels too
Calling a function ties the parameter name to the caller's object. So a function can mutate a list you pass in, but rebinding the parameter inside does nothing outside:
def add_snack(bag):
bag.append("apple") # mutates the caller's list
def replace_bag(bag):
bag = ["crisps"] # rebinds the local label only
lunch = ["sandwich"]
add_snack(lunch)
replace_bag(lunch)
print(lunch) # ['sandwich', 'apple']Copies: shallow and deep
list(xs), xs.copy(), xs[:] and copy.copy(xs) make a new outer list, but the items in it are the same objects. That's a shallow copy. With lists inside lists, the inner ones are shared. copy.deepcopy copies all the way down:
import copy grid = [[0, 0], [0, 0]] shallow = copy.copy(grid) deep = copy.deepcopy(grid) grid[0][0] = 5 print(shallow[0][0], deep[0][0]) # 5 0 print(shallow[0] is grid[0], deep[0] is grid[0]) # True False
The mutable default trap
A default value is worked out once, when def runs, and kept on the function. If it's a list and the function changes it, every later call gets the changed list. You can watch it happen:
def remember(item, seen=[]):
seen.append(item)
return seen
remember("a")
remember("b")
print(remember.__defaults__) # (['a', 'b'],): one list, shared by every callThe fix is a default that can't change, usually None, and a fresh list made inside the function each time it's called.
Try it:Run the starting code first and read its output: both surprises are there.
More in the Python docs: default argument values, how arguments are passed and copy.
Your turn
Fix both surprises in the starting code. Change collect so that each call without a list starts a new one (use None as the default), while a list you pass in still gets added to. Then make plan_b a deep copy of plan_a, so changing plan_b[0][0] to "swim" leaves plan_a alone. Print both plans.
Your task
Fix both surprises in the starting code. Change collect so that each call without a list starts a new one (use None as the default), while a list you pass in still gets added to. Then make plan_b a deep copy of plan_a, so changing plan_b[0][0] to "swim" leaves plan_a alone. Print both plans.
- Each call to collect() gets its own list (not done yet)
- collect() still adds to a list you pass in, even an empty one (not done yet)
- The default is None (not done yet)
- plan_b is a deep copy: plan_a stays the same (not done yet)
The checklist ticks itself off as you work in the editor: any way that gets the result counts.
Hints come one at a time, then one way to do it. Try each before the next.
What this lesson uses, in one place.
aliasing- Two names for one object: a change made through one shows through the other Python docs →
x += y- Changes a list in place, but makes a new object for ints, strings and tuples Python docs →
copy.copy()- A shallow copy: a new outer object holding the same items Python docs →
copy.deepcopy()- A deep copy: copies the items too, all the way down Python docs →
mutable default- A list or dict as a default is shared by every call. Use None, and make a new one inside Python docs →