x = [1, 2, 3]
y = x
y.append(4)
print(f"{y=}")
# spooky action at a distance
print(f"{x=}")y=[1, 2, 3, 4]
x=[1, 2, 3, 4]
Now that we’ve seen all of the built-in types we can take a second look at mutability and explore what Python is doing under the hood, so that we are less likely to be surprised by the behavior.
Remember that when we do an assignment, we are associating a name with an object, a value in memory.
It is the object that has a type, not the name.
# a name is bound to the result of the expression
x = 1 + 1
# the name is re-assigned, we aren't changing data
x = x + 1
# this is why we can re-assign to a different type
x = "hello"strtuplefrozensetint, float, complex, bool, NoneFor immutable types, this is the only option, any changes require reassignment.
listdictsetOn the other hand, mutable values can be changed in place.
x = [1, 2, 3]
x.append(4) # no re-assignment needed!
print(x)All types in Python share an internal representation as an object (PyObject in C).
ll = [1, 2, 3, 4]
yy = ll # increase ref countobject
| Field | Example | Purpose |
|---|---|---|
| id | 393239323 | uniquely identify object within Python interpreter |
| refcount | 2 | count how many names currently point to this object |
| type | list |
type of object |
| data | 0x80000000 | memory address where the actual data is stored |
| length | 4 | Only present on collection types, stores pre-computed length. |
Notice that name is not stored on the object! Why not?
Python is a garbage collected language.
We don’t free our own memory, Python does instead.
Behind the scenes, Python stores a reference counter on each object. How many names/objects reference the object.
When reference count drops to zero, Python can reclaim the memory.
The built-in id(...) function returns the identity of an object, which is an integer value guaranteed to be unique and constant for lifetime of object
In the official (“CPython”) Interpreter we are using in this class, it is the address of the memory location storing the object.
x = "Orange"
print(id(x)) # Unique integer-value for the object pointed by x140386456494144
y = "Apple"
print(id(y)) 140386120152864
fruit1 = ("Apples", 4)
fruit2 = ("Apples", 4)
fruit3 = fruit2
print(f"Fruit1 id = {id(fruit1)} \n Fruit2 id = {id(fruit2)}")
print(f"Fruit3 id= {id(fruit3)}")Fruit1 id = 140386118456896
Fruit2 id = 140386118456384
Fruit3 id= 140386118456384
fruit1 is fruit2False
Two different ways of testing if objects are the “same”:
==): Returns true if two objects are equal (i.e., have the same value)is): Returns true if two objects identities are the same.a is b means id(a) == id(b)
a = [1, 2, 3]
b = [1, 2, 3]
print("a == b", a == b)
print(id(a))
print(id(b))
print("a is b", a is b) # The id values are differenta == b True
140386118474944
140386118475136
a is b False
print(id(None))26064584
def f():
pass
id(f())26064584
is NoneIf you ever need to check if a value is None, you’d use is None or is not None
# list d
d = [1, 2, 3]
print(id(d))
d.append(4)
print(d)
print(id(d))140386120360384
[1, 2, 3, 4]
140386120360384
# str D
s = "Hello"
print(id(s))
s += " World"
print(s)
# did s change?
print(id(s))140386120335408
Hello World
140386118543280
Each time you generate a new value in your script by running an expression, Python creates a new object (i.e., a chunk of memory) to represent that value.
– Learning Python 2013
Not quite! CPython does not guarantee this, and in fact sometimes caches & reuses immutable objects for efficiency.
a = 100000000
b = 100000000
# Two different objects, two different ids.
print(a is b)False
a = 100
b = 100
# However, for small integer objects, CPython caches them
# this means that a and b point to the same object
print(a is b)True
# CPython does the same for short strings
str1 = "MPCS"
str2 = "MPCS"
print(id(str1), id(str2))
str1 is str2140386120341984 140386120341984
True
In practice this is just a quirk of the CPython interpreter, since the objects are immutable it isn’t important to know that they share memory in some cases.
If y = x does not make a copy, how can we get one?
We’ve seen the .copy() method on a few of our types. Which ones?
We can also use the copy module:
x = [1, 2, 3]
y = x.copy()
print(id(x))
print(id(y))
x.append(4)
print(x, y)140386120200832
140386118680128
[1, 2, 3, 4] [1, 2, 3]
# shallow copy example (nested mutables are not copied)
x = [[1, 2], [3, 4]]
y = x.copy() # or copy.copy(x)
print("x is y", x is y)
print("x[0] is y[0]", x[0] is y[0])
print("x[1] is y[1]", x[1] is y[1])
# print(x, y)
x[0].append(5)
print(x, "\n", y)x is y False
x[0] is y[0] True
x[1] is y[1] True
[[1, 2, 5], [3, 4]]
[[1, 2, 5], [3, 4]]
# deep copy (nested mutables are copied)
import copy
# copy.copy(obj) --> same as obj.copy()
z = copy.deepcopy(x)
print("x[0] is z[0]", x[0] is z[0])x[0] is z[0] False