Object identity and aliasing: who else holds this object?

List every variable, list slot and field that holds an object, then predict which paths see a field write, a slot replacement, an append or a rebinding. Objects and access · Object-oriented programming.

You change a player's number through one list, and a second list shows the change too. Then you replace a player in one list, and a second name for that list shows it, while a separate list does not. Both outcomes follow from one question: which places hold which object? In the previous lesson, c = a gave one ticket two names. An object can also be reached from a list position or from a field of another object. This lesson teaches you to list those places and predict which ones notice a write.

Who holds an object

Start small, with a Player object that has two fields.

class Player:
    def __init__(self, name, number):
        self.name = name
        self.number = number

mira = Player("Mira", 7)    # P1
starters = [mira]           # L1, a list whose slot 0 points at P1
starters[0].number = 10
print(mira.number)          # 10

P1 has two holders: the variable mira and slot 0 of L1. A holder is any variable, list slot or object field that currently points at an object. What it keeps is a reference, the link to that one object. The write starters[0].number = 10 follows slot 0 to P1 and changes its field, so mira reads 10 as well.

A list is itself an object, and its numbered positions are slots; slot 0 is the first. A field can hold a reference too, such as the field squad of a Team object.

The roster question and its starting state

A coach keeps two lists of players. After three edits, which expressions show Mira's new number 10, and which still reach the original Mira object? The edits: set Mira's number to 10 through captain; replace slot 1 of bench with a fresh Mira numbered 10; put Ravi into slot 0 of starters.

class Team:
    def __init__(self, starters, squad):
        self.starters = starters
        self.squad = squad

mira = Player("Mira", 7)       # P1
ravi = Player("Ravi", 4)       # P2
starters = [mira]              # L1
squad = [ravi, mira]           # L2
bench = squad                  # second name for L2
team = Team(starters, squad)   # T1
captain = team.squad[1]        # reads slot 1 of L2
print(bench is squad)          # True: one list, two names

P1, P2, L1, L2 and T1 are teaching labels, not Python's id() values, and L2[1] means slot 1 of list L2. Holders at the start:

ObjectHoldersCount
P1 (Mira, 7)mira, captain, L1[0], L2[1]4
P2 (Ravi, 4)ravi, L2[0]2
L1starters, field team.starters2
L2squad, bench, field team.squad3
T1team1

A path is the chain of reads you type to reach an object. squad[1], bench[1] and team.squad[1] are three paths to the one slot L2[1], so we count holders, not paths.

Three writes, one at a time

Write 1: captain.number = 10. The left side is a field of the object captain holds, so this writes into P1, exactly like the small example. P1's number goes from 7 to 10, and every path ending at P1 reads 10, such as starters[0].number. ravi and L2[0] hold P2, so ravi.number is still 4.

Write 2: bench[1] = Player("Mira", 10). The coach re-registers Mira as a fresh player. Python builds the new object P3, then stores a reference to it in slot 1 of L2. This slot replacement puts a different reference into one slot. P1 keeps number 10 but now has three holders: mira, captain and L1[0]. P3 has one, L2[1]. squad and team.squad hold the same list, so squad[1] and team.squad[1] reach P3 too. mira still holds P1; a variable does not follow a slot.

Write 3: starters[0] = ravi. Slot 0 of L1 moves from P1 to P2. P1 drops to two holders, mira and captain. P2 has three: ravi, L2[0] and L1[0]. L2 is untouched, so no L2 path changes. team.starters[0] reaches P2 because the field team.starters holds L1.

A name read earlier keeps its object, not its slot. Suppose captain.number = 11 ran now. captain still holds P1, and P1's holders are only mira and captain, so mira.number and captain.number read 11 while squad[1].number reaches P3 and still reads 10.

Watch the writes play back.

Roster: one write, many holders

Read the steps as text

  1. Start state. Every arrow is one holder, and each badge counts the holders of that object.
  2. team.squad[1] follows team to T1, then its squad field to L2, then slot 1 to P1. 3 paths end at L2[1]: one holder, not 3.
  3. captain.number = 10 follows captain to P1 and changes number from 7 to 10. All 4 holders of P1 see it; objects held elsewhere are unchanged.
  4. Python builds a new Player, P3. Nothing holds it yet. bench[1] = Player("Mira", 10) moves slot 1 of L2 from P1 to P3. L2 has 3 holders, and each sees the change. P1 now has 3 holders. L1 is unchanged.
  5. starters[0] = ravi moves slot 0 of L1 from P1 to P2. L1 has 2 holders, and each sees the change. P1 now has 2 holders. L2 is unchanged.
  6. mira reaches P1. Its 2 holders: mira and captain.

The same write, a different audience

Writes 2 and 3 are both slot replacements, yet different paths see them, because the lists have different holders. A change in L2 shows through squad, bench and team.squad; one in L1 only through starters and team.starters.

To predict who notices a write:

  1. Classify the left side: a bare variable, a slot such as x[1], or a field such as x.number.
  2. A bare variable moves only itself. For a slot or field, find the object reached by the part before the last [ or ..
  3. List the holders of that object. Each sees the write.

Change the list or move the name

Start again from the setup, where L2 holds Ravi and Mira. The new player Asha is P4, and the empty list is L3. Two operations both look like "changing bench" yet act on different targets.

bench.append(Player("Asha", 9))   # list mutation
bench = []                        # variable rebinding

bench.append(...) calls a method on the list that bench reaches. L2 gains a slot 2 holding Asha and keeps its three holders, so squad[2] and team.squad[2] now reach Asha.

bench = [] creates a new empty list, L3, and moves the variable bench to it. This is rebinding: one holder changes what it keeps. L2 is untouched and keeps all three players; its holders are now squad and team.squad, so squad[2] still reaches Asha.

Squad: append versus rebind

Read the steps as text

  1. Start state. Every arrow is one holder, and each badge counts the holders of that object.
  2. Python builds a new Player, P4. Nothing holds it yet. bench.append(Player("Asha", 9)) adds slot 2 to L2 and points it at P4. L2 has 3 holders, and each sees the new slot.
  3. Python builds a new empty list, L3. Nothing holds it yet. bench = [] moves only the name bench, from L2 to L3. L2 keeps 2 holders and all 3 of its slots.
  4. team.squad[2] follows team to T1, then its squad field to L2, then slot 2 to P4. 2 paths end at L2[2]: one holder, not 2.

Two mix-ups to avoid

One more case, new objects

The next scene uses songs and a queue, so the same holder reasoning is tried on fresh data.

Songs: replace first, then retitle

Read the steps as text

  1. Start state. Every arrow is one holder, and each badge counts the holders of that object.
  2. shuffle[0] = song_c moves slot 0 of L1 from S1 to S3. L1 has 3 holders, and each sees the change. S1 now has 2 holders.
  3. now.title = "Dawn (live)" follows now to S1 and changes title from "Dawn" to "Dawn (live)". Both holders of S1 see it; objects held elsewhere are unchanged.
  4. Python builds a new empty list, L2. Nothing holds it yet. shuffle = [] moves only the name shuffle, from L1 to L2. L1 keeps 2 holders and both of its slots.
  5. now reaches S1. Its 2 holders: now and song_a.

Back to the roster question

Five paths show number 10: mira.number, captain.number, and the three through L2, squad[1], bench[1] and team.squad[1]. Only mira and captain still reach the original Mira, P1.

Where this shows up in real code

A scoreboard screen and a report page both keep squad. If one calls squad.append(...), the other sees the new player, because both hold one list. The constructor Team(starters, squad) makes the same sharing: it stores the caller's list, so a later change by the caller appears inside team.squad. Sharing is sometimes wanted and sometimes a bug; the copy lesson at the end shows how to give a writer its own object.

Recap

  • An object's holders are every variable, slot and field that keeps it, and a field write is seen through all of them. Count holders, not paths.
  • A slot replacement moves one holder inside a list. Every holder of that list sees it, a separate list does not, and the old object keeps its other holders.
  • Mutating a list changes the one shared list. Rebinding a variable moves only that variable.
Java contrast: reference values

Java describes the same idea in its own words. A variable of class type, an element of an object array and a field of class type each hold a reference value that leads to an object, or null. A field of type int holds the number itself. On two object references, == asks whether they hold the same reference value, so it answers the identity question, not whether the fields match.

Player[] squad = { ravi, mira };
Player[] bench = squad;               // second name for one array
System.out.println(bench == squad);   // true: same array
bench[1] = new Player("Mira", 10);    // slot replacement: squad[1] sees it
bench = new Player[0];                // rebinding: squad is unchanged

A Java array has a fixed length, so growing the list uses a list such as ArrayList and its add method. The rule is unchanged: a method call on the shared list changes it for every holder, while assigning the variable moves only that variable.

Next, encapsulation and access asks which writes an object should accept at all, and shallow and deep copy shows how to give a write a separate object to land on. To practise following and changing links between nodes, try Linked List Fundamentals.