Python has a relatively straightforward system to handle asignment operations. However, this usage surprises most people, so we will cover it in detail.
Things you will learn:
The first premise is that names refer to values.
When we make a simple assignment:
x = 23
This is not assigning 23 to x; it is in fact pointing the name x to the value 23. So x refers to, or is a reference to, 23.
If we have this code:
x = 23
y = x
People coming from c-like languages would think this assigns 23 to x, and copies 23 to y.
This is incorrect. In fact, both x and y point to a single instance of 23.
Starting with the previous example:
x = 23
y = x
# now, assign a new value to x
x = 12
We start with both x and y pointing to 23.
Then we assign another value to x. y is still pointing to 23. The names do not become linked.
Reference counting is critical to garbage collection. As long as a reference exists, a value is not deleted.
For instance:
x = "hello"
x = "world"
x initially points to "hello". It is then reassigned to "world" without deleting "hello". "hello" is only removed when the reference count is checked during garbage collection.
To reiterate a point mentioned earlier, assignment does not copy the value:
nums = [1, 2, 3]
other = nums
There is only one copy of the list, and two references to it.
As a consequence of never copying data, changes are visible to all names that reference the data. For instance:
nums = [1, 2, 3]
other = nums
nums.append(4)
print(other) # prints [1, 2, 3, 4]
This is one of the key points people get confused on, so I will reiterate: changes are visible to all names. This is known as mutable aliasing.
Certain simple values are immutable, thus they cannut be used for mutable aliasing.
Immutable types: int, float, string, tuple
An Example:
x = "hello"
y = x
x = x + " there"
If we refer to changing an int, we are rebinding:
x = x + 1
An int is immutable, so cannot be mutated.
If we are changing a list, we are usually mutating:
nums.append(7)
We can also rebind lists:
nums = nums + [7]
Mutable and immutable use the same assignment. Aliasing just makes it seem different.
Suppose we have:
x += y
This is conceptually equal to:
x = x + y
But this is not what happens. Instead:
x = x.__iadd__(y)
Importantly, the list __iadd__ method is defined like:
class List:
def __iadd__(self, other):
self.extend(other)
return self
Thus, these two lines are the same:
num_list += [4, 5]
num_list.extend([4, 5]); num_list = num_list
So we are not creating a new list, just mutating the current list.
List elements are refernces too:
nums = [1, 2, 3]
x = nums[1]
All of these assign to x:
x = ...
for x in ...
class x(...):
def x(...):
def fn(x):
import x
from ... import x
except ... as x:
with ... as x:
As an example of a tricky thing, iterating through a for loop:
nums = [1, 2, 3]
for x in nums:
x = x * 10
print(nums) # prints [1, 2, 3]
So what went wrong?
When assigning to x, we are repointing the reference x to a new value, not modifying the old one.
Given this code:
def func(x):
print(x)
return
num = 17
func(num)
The x in func(x) is just another reference to 17. It works exactly like this code:
num = 17
x = num
Where this has consequences is:
def append_twice(a_list, val):
a_list.append(val)
a_list.append(val)
nums = [1, 2, 3]
append_twice(nums, 7)
print(nums) # prints [1, 2, 3, 7, 7]
Even though the action happens in the function, and in a different scope, a_list is just a reference to the list [1, 2, 3]. When it mutates the value, nums is also mutated.
I reiterate: mutable aliasing.
Remember to be careful and check what operations are actually occurring:
def append_twice(a_list, val):
a_list = a_list + [val, val]
nums = [1, 2, 3]
append_twice(nums, 7)
print(nums) # prints [1, 2, 3]
In this case, a_list is rebound inside the function so no changes are made to the initial value. To get the output you must return it:
def append_twice(a_list, val):
return a_list + [val, val]
nums = [1, 2, 3]
nums = append_twice(nums, 7)
print(nums) # prints [1, 2, 3, 7, 7]
This does not work as expected:
board = [[0] * 8] * 8
board[0][0] = 1
print(board)
# [[1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0],
# [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0],
# [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0],
# [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0]]
What went wrong? The problem is that the inner list was referenced 8 times to create the outer list. A better way to do this is:
board = [[0] * 8 for _ in range(8)]
Python does have a sense of scope, but it is very loose.
First let's define the basic namespaces in python. These are mappings from names to objects (basically dicts). The same name can be in different namespaces without causing a conflict. The namespaces include:
A scope is a region of the program where a namespace is directly accessible with an unqualified name. There are four scopes:
All scopes except the local scope are read-only unless declared otherwise (with the global keyword). Assigning to a name in a non-local scope creates a new reference in the local scope.
a = 1
def foo(c):
c = 3 # reassigns local 'c' to 3, does not change 'b'
a = 4 # makes a new name 'a' in local scope
b = 2
foo(b)
print(a) # 1
print(b) # 2
If we wanted to change a inside the function, the global keyword should be used:
def foo(c):
global a
a = 4
Default arguments in functions can trip people up. For instance:
def foo(c=1):
print(c)
c += 1
foo() # prints 1
foo() # prints 1
This is expected, because c += 1 rebinds to a new value.
def foo(c=[]):
print(c)
c += [1]
foo() # prints []
foo() # prints [1]
foo() # prints [1, 1]
Why does this happen? Two reasons:
These two reasons mean the function is mutating a single value throughout the lifetime of the program.
Class variables should not be used in instances if they are mutable (or just not at all to avoid confusion):
class Dog:
tricks = [] # mistaken use of a class variable
def __init__(self, name):
self.name = name
def add_trick(self, trick):
self.tricks.append(trick)
d = Dog('fido')
e = Dog('buddy')
d.add_trick('roll over')
e.add_trick('play dead')
print d.tricks # prints ['roll over', 'play dead']
Instead, define the tricks = [] inside __init__.