Variables and Objects

It's pointers all the way down

trey.io/news — a weekly Python tip

Trey Hunner
Truthful Technology
Python Morsels

Variables & Objects

Mental Models

Gotchas

1

Changing first doesn't always change second


>>> first = [2, 3]
>>> second = first
>>> first.append(5)
>>> second
[2, 3, 5]
              

>>> first = [2, 3]
>>> second = first
>>> first = [2, 3, 5]
>>> second
[2, 3]
              
2

>>> def square_all(numbers):
...     for i, n in enumerate(numbers):
...         numbers[i] = n ** 2
...     return numbers
...
>>> numbers = [2, 1, 3, 4, 7]
>>> square_all(numbers)
[4, 1, 9, 16, 49]
>>> numbers
[4, 1, 9, 16, 49]
          
3

>>> matrix = [[0] * 3] * 3
>>> matrix
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
>>> matrix[1][1] = 9
>>> matrix
[[0, 9, 0], [0, 9, 0], [0, 9, 0]]
          
4

>>> colors = ["purple", "blue", "green"]
>>> groups = dict.fromkeys(colors, [])
>>> groups
{'purple': [], 'blue': [], 'green': []}
>>> groups["purple"].append("duck")
>>> groups
{'purple': ['duck'], 'blue': ['duck'], 'green': ['duck']}
          
5

>>> class TodoList:
...     def __init__(self, tasks=[]):
...         self.tasks = tasks
...
>>> mon = TodoList()
>>> tue = TodoList()
>>> mon.tasks.append("Work on talk")
>>> tue.tasks
['Work on talk']
          

Gotchas

  1. 1. second = first
  2. 2. for i, n in enumerate(numbers): numbers[i] = n ** 2
  3. 3. matrix = [[0] * 3] * 3
  4. 4. groups = dict.fromkeys(colors, [])
  5. 5. def __init__(self, tasks=[]): self.tasks = tasks

Buckets


>>> first = []
>>> second = first
>>> second.append(9)
>>> second
[9]
>>> first
[9]
first [9] second [9]

This mental model breaks down

Pointers  🤔


>>> first = []
>>> second = first
>>> second.append(9)
>>> second
[9]
>>> first
[9]
>>> rows = [second]
>>> 
            
Object Land Variable Land list first second [] [9]

➡️

“But they're not like pointers in C”

binding 🪢
reference 📖
pointer 👉

“Change”

1

mutation


>>> first = [2, 3]
>>> second = first
>>> first.append(5)
>>> second
[2, 3, 5]
              

assignment


>>> first = [2, 3]
>>> second = first
>>> first = [2, 3, 5]
>>> second
[2, 3]
              

“change”


>>> first = [2, 1, 3, 4]
>>> second = first
>>> first.append(7)
>>> second
[2, 1, 3, 4, 7]
>>> first = [100, 200, 300]
>>> second
[2, 1, 3, 4, 7]
>>>
            
Object Land Variable Land list second first [2, 1, 3, 4] [2, 1, 3, 4, 7] list [100, 200, 300]

Remember: variables point to objects

The 2 Types of Change

  • Mutations change an object
  • Assignments change a variable

“We changed x

Changed the variable?


>>> x = [2, 3]
>>> x = [2, 3, 5]
              

assignment

Changed the object?


>>> x = [2, 3]
>>> x.append(5)
              

mutation

Gotchas

  1. 1. second = first
  2. 2. for i, n in enumerate(numbers): numbers[i] = n ** 2
  3. 3. matrix = [[0] * 3] * 3
  4. 4. groups = dict.fromkeys(colors, [])
  5. 5. def __init__(self, tasks=[]): self.tasks = tasks

2 types of

“are they the same?”


>>> first == second  # Equality
True
>>> first is second  # Identity
True

Identity: the exact same object

Equality: an equivalent object

Equality is about objects

Identity is about pointers


>>> first == second  # Are these objects equal to the other?
True
>>> first is second  # Do these variables point to the same object?
True

Equality:
the usual way to ask
“are they the same?”


value is None
          

value == another_value
          

Assignments happen


>>> e = 2.7
>>> from math import e
>>> for e in range(3):
...     print(e)
...
0
1
2
>>> def e(): return 2.7
...
>>> class e: pass
...
>>> e
<class '__main__.e'>
          
2

Function arguments are also assignments


def square_all(numbers):
    for i, n in enumerate(numbers):
        numbers[i] = n ** 2
    return numbers
          

>>> numbers = [2, 1, 3, 4, 7]
>>> square_all(numbers)
[4, 1, 9, 16, 49]
>>> numbers
[4, 1, 9, 16, 49]
          
2

def square_all(numbers):
    for i, n in enumerate(numbers):
        numbers[i] = n ** 2  # DON'T DO THIS
    return numbers
          

def square_all(numbers):
    squares = []
    for n in numbers:
        squares.append(n ** 2)
    return squares
          

def square_all(numbers):
    return [
        n ** 2
        for n in numbers
    ]
          

Assignments that mutate


def square_all(numbers):
    for i, n in enumerate(numbers):
        numbers[i] = n ** 2
    return numbers
          

>>> some_list[0] = 8
>>> some_object.attribute = 4
          

Gotchas

  1. 1. second = first
  2. 2. for i, n in enumerate(numbers): numbers[i] = n ** 2
  3. 3. matrix = [[0] * 3] * 3
  4. 4. groups = dict.fromkeys(colors, [])
  5. 5. def __init__(self, tasks=[]): self.tasks = tasks

Objects

Variables

Data structures don't contain data*

*for some definitions of “data”

Data structures don't contain objects

Pointers


>>> first = []
>>> second = first
>>> second.append(9)
>>> second
[9]
>>> first
[9]
>>> rows = [second]
>>>
>>> 
            
Object Land Variable Land list [9] first second int 9 0

Pointers


>>> first = []
>>> second = first
>>> second.append(9)
>>> second
[9]
>>> first
[9]
>>> rows = [second]
>>>
            
Object Land Variable Land list first second int 9 0 rows list 0

“a list of lists”

“a list of references to lists”

Containment

... is a lie


x in y
          

“y contains x”

3

>>> matrix = [[0] * 3] * 3
>>> matrix
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
>>> matrix[1][1] = 9
>>> matrix
[[0, 9, 0], [0, 9, 0], [0, 9, 0]]



          
int 0 list 0 1 2 list 0 1 2 matrix
3

>>> matrix = [[0] * 3] * 3
>>> matrix
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
>>> matrix[1][1] = 9
>>> matrix
[[0, 9, 0], [0, 9, 0], [0, 9, 0]]



          
int 0 int 9 list 0 1 2 list 0 1 2 matrix
3

>>> matrix = [[0] * 3 for _ in range(3)]
>>> matrix[1][1] = 9
>>> matrix
[[0, 0, 0], [0, 9, 0], [0, 0, 0]]





          
list 0 1 2 list [0, 0, 0] list [0, 0, 0] [0, 9, 0] list [0, 0, 0] matrix

Gotchas

  1. 1. second = first
  2. 2. for i, n in enumerate(numbers): numbers[i] = n ** 2
  3. 3. matrix = [[0] * 3] * 3
  4. 4. groups = dict.fromkeys(colors, [])
  5. 5. def __init__(self, tasks=[]): self.tasks = tasks
4

>>> colors = ["purple", "blue", "green"]
>>> groups = dict.fromkeys(colors, [])
>>> groups["purple"].append("duck")
>>> groups
{'purple': ['duck'], 'blue': ['duck'], 'green': ['duck']}
          
list [] ["duck"] dict "purple" "blue" "green" groups
4

>>> colors = ["purple", "blue", "green"]
>>> groups = {color: [] for color in colors}
>>> groups["purple"].append("duck")
>>> groups
{'purple': ['duck'], 'blue': [], 'green': []}




          
list [] ["duck"] list [] list [] dict "purple" "blue" "green" groups

Gotchas

  1. 1. second = first
  2. 2. for i, n in enumerate(numbers): numbers[i] = n ** 2
  3. 3. matrix = [[0] * 3] * 3
  4. 4. groups = dict.fromkeys(colors, [])
  5. 5. def __init__(self, tasks=[]): self.tasks = tasks
5

>>> class TodoList:
...     def __init__(self, tasks=[]):
...         self.tasks = tasks
...
>>> mon = TodoList()
>>> tue = TodoList()
>>> mon.tasks.append("Work on talk")
>>> tue.tasks
['Work on talk']
>>> TodoList.__init__.__defaults__[0]
['Work on talk']

          
list [] ["Work on talk"] TodoList tasks TodoList tasks mon tue
5

>>> class TodoList:
...     def __init__(self, tasks=[]):
...         self.tasks = list(tasks)
...
>>> mon = TodoList()
>>> tue = TodoList()
>>> mon.tasks.append("Work on talk")
>>> tue.tasks
[]

          
list [] ["Work on talk"] list [] list [] TodoList tasks TodoList tasks mon tue
5

>>> class TodoList:
...     def __init__(self, tasks=[]):
...         self.tasks = list(tasks)
...
>>> default_todos = ["Reflect on last week"]
>>> mon = TodoList(default_todos)
>>> tue = TodoList(default_todos)
>>> mon.tasks.append("Work on talk")
>>> mon.tasks
['Reflect on last week', 'Work on talk']
>>> tue.tasks
['Reflect on last week']
>>> wed = TodoList({"Here is", "a set", "of tasks"})
          

Gotchas

  1. 1. second = first
  2. 2. for i, n in enumerate(numbers): numbers[i] = n ** 2
  3. 3. matrix = [[0] * 3] * 3
  4. 4. groups = dict.fromkeys(colors, [])
  5. 5. def __init__(self, tasks=[]): self.tasks = tasks

Recap

Variables

  • Variables don't contain objects
  • Variables contain pointers to objects
  • Assignments point a variable to an object
  • Assignments do not make a copy

So...

  • second = first is a problem for mutable objects
  • But that's fine for immutable objects like strings

Functions

  • Function arguments are just like every other variable
  • Function calls assign an object to each function parameter
  • Function calls do not copy any objects

So...

  • Functions should not mutate passed-in objects, unless the caller expects it
  • Functions often implicitly copy passed-in objects using comprehensions, slicing, list(...), etc.

Objects

  • Objects can't contain other objects
  • Objects can only contain pointers to other objects

So...

  • Avoid storing multiple references to the same mutable object
  • Using dict.fromkeys with a mutable default value will cause problems
  • Be mindful of whether your initializer methods store references to a passed-in object or copy those objects

It's pointers all the way down 🐢

  • Variables store object references
  • Objects store object references
  • Variables/objects do not "contain" objects
  • Function arguments are variables
  • Attributes act just like variables

We didn't discuss

  • Variable scope in Python
  • Hashability and its relation to immutability
  • CPython optimizations that affect identity
  • Direct value storage of array, data frames, etc.
  • Tuples containing mutable objects

https://trey.io/pycascades26

Names, Objects, and Plummeting From The Cliff - Brandon Rhodes

Facts and Myths about Names and Values - Ned Batchelder

But... why?

Imagine an alternative...

Would every assignment statement make a copy?

Would storing a reference to an object ever be possible?

"reference assignments" and "copying assignments" 😬

This thing is bad

Compared to what alternative?

Everything is a trade-off

Resources   https://trey.io/pycascades26

Thank you

Trey Hunner
Python Team Trainer
[email protected]

© 2024 Akiyoshi Kitaoka, used with permission

https://www.psy.ritsumei.ac.jp/akitaoka/saishin72e.html

#7c7c7c

https://www.psy.ritsumei.ac.jp/akitaoka/saishin72e.html

Mental Models

"All models are wrong,
but some models are useful." — George E. P. Box

Assignments can mutate

  • Assignments change the object a reference refers to
  • Objects can contain references to other object
  • Assigning to an object attribute/index mutates the object

Augmented Assignments

+=, -=, *=, etc.

Are these assignments?

Are these mutations?

YES

In-place addition


>>> a = [2, 1, 3]
>>> b = a
>>> b += [4, 7, 11]
>>> b
[2, 1, 3, 4, 7, 11]
>>> a
[2, 1, 3, 4, 7, 11]

>>> name = "North Bay Python"
>>> name += " 2025"  # name = name + "2025"
>>> name
'North Bay Python 2025'

Changing tuples

Tuples cannot be mutated*

* For some definitions of "mutate"

Tuples can contain lists


>>> result = (True, [2, 1, 3])
>>> result[1].append(4)
>>> result
(True, [2, 1, 3, 4])

Immutability is shallow


>>> result = (True, [2, 1, 3])
>>> result2 = (True, [2, 1, 3])
>>> result == result2
True
>>> result[1].append(4)
>>> result
(True, [2, 1, 3, 4])
>>> result == result2
False

Augmented assignments in tuples


>>> result = (True, [2, 1, 3, 4])
>>> result[1] += [7, 11, 18]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    result[1] += [7, 11, 18]
    ~~~~~~^^^
TypeError: 'tuple' object does not support item assignment
>>> result
(True, [2, 1, 3, 4, 7, 11, 18])
          

Sticky Notes


>>> first = []
>>> second = first
>>> second.append(9)
>>> second
[9]
>>> first
[9]
>>> 
            
[9] first second

Sticky Notes


>>> first = []
>>> second = first
>>> second.append(9)
>>> second
[9]
>>> first
[9]
>>> rows = [second]
>>> lists = rows
[9]

>>> first = []
>>> second = first
>>> second.append(9)
>>> second
[9]
>>> first
[9]
>>> rows = [second]
>>> lists = rows
[9] first second rows[0] lists[0]

One more puzzle


>>> x = []
>>> x.append(x)
>>> x
???
          

>>> x = []
>>> x.append(x)
>>> x
[[...]]
          

>>> x = []
>>> x.append(x)
>>> x
[[...]]
>>> x[0] is x
True
>>> x in x
True