Value types and reference types

  • C++ and others primarily use value types.
    • Variables are named memory locations (boxes) that hold values.
    • Each variable is its own named box and holds its own value.
  • Python variables are different than in many languages.
    • For convenience, we can think of Python as using reference types.
    • The object is what is special.
    • Variable names are like tags bound to a given object.

  • The id function in Python shows the memory location of a given object.
In [13]:
x = 5.5
y = x
print(f"Location of x is {id(x)}")
print(f"Location of y is {id(y)}, the same as x")
Location of x is 4588908880
Location of y is 4588908880, the same as x
  • Given variables x and y, if we now set y=6.6, we create a new object, 6.6, and tag it with the variable named y
In [14]:
y = 6.6
print(f"Location of y is {id(y)}, now different than x above")
Location of y is 4588908720, now different than x above
  • For simple variables and assignments like this the difference is hardly noticeable and we can practically ignore it (as we have been).
    • This is because we have mostly been working with immutable objects.

Mutable and immutable objects

  • Python is object-centric.
  • Objects come in two flavors:
    • immutable (not changeable),
      • numbers, strings, booleans, tuples, etc.
    • mutable (changeable).
      • lists, dictionaries, numpy arrays, etc.
In [3]:
import numpy as np

a = np.array([1,2,3,4])

b = a                         # try b = a.copy() instead

a[0] = 101325           
print(b)
id(a), id(b)
[101325      2      3      4]
Out[3]:
(4620964288, 4620964288)
  • Variables a and b both tag the same list object.
  • If we change that object through a[0] = 101325, then b appears to have also changed.
  • This seems strange and confuses beginners.
  • But it makes sense when we realize that a and b both tag the same object.
    • So, if the underlying object changes, that will be reflected in both a[0] and b[0].
  • If you want a and b to refer to separate objects so that setting a[0]=101325 won't affect b, the use b=a.copy()
In [11]:
a = [1,2,3,4]
print(f"id(a) = {id(a)}")

b = a.copy()
print(f"id(b) = {id(b)}, is different than a")

a[0] = 101325
print(f"setting        a[0]= 101325")
print(f"b[0] hasn't changed: {b[0]}")    
id(a) = 4362623648
id(b) = 4363030352, is different than a
setting        a[0]= 101325
b[0] hasn't changed: 1
In [23]:
a = np.array([1,2,3,4])

b = a*2      # assigns b to a new object array a*2; a is unchanged

a[0] = 101325   # b is unchanged
b
id(a), id(b)
Out[23]:
(4764209600, 4764210080)
In [15]:
def f(x):
    return "kittens"

print = f

print("101325")
Out[15]:
'kittens'

Function arguments and scope

  • Variable names (tags) are bound to objects.
  • Variables can be seen and used in enclosing scopes.
    • Functions can see variables defined in the global scope.
  • A function cannot rebind a global variable to a different object.
    • But it can make a different variable with the same name and bind that to a new object.
      • (in a lower scope)
    • That is: same name, different variable, bound to a different object.
    • The global scope variable-object binding are unchanged.

Example 1

  • The same variable name can be used to tag different objects in different scopes.
    • The variables are different, but the names are the same.
    • Think of two different people with the same name.
  1. In the following example, outside the function in the global scope we create an immutable object 2 (an integer) and tag it with variable x.
    • x here is a global variable.
  2. We call the function f(x)
    1. Then x=6 creates a new object 6, and tags it with the local variable x.
      • Same name, different object, different scope
      • That is, we are not allowed to change the global tag-object binding x to 2, so we create a local x tag that is bound to the object 6.
  3. The outside object 2 still exists and is still tagged by the global variable x.
In [26]:
x = 2                       # 1. create integer object 2 tagged by x
#-----------------------------------
def f(x):        
    print("x top of func:", x, id(x))
    x = 6                   # 2A. create LOCAL tag x bound to different object 6
    print("x end of func:", x, id(x))
#-----------------------------------
f(x)                        # 2. call the function
print("x final:      ", x, id(x))  # 3. outer object x is unchanged
x top of func: 2 4555722544
x end of func: 6 4555722672
x final:       2 4555722544

Example 2

  1. Create the mutable list object tagged by variable myx.
  2. Pass this to the function.
  3. The function tags this same object with local variable x.
  4. The function then changes an element of this list object.
  5. That object is still changed outside the function, because it is the same object.
    • The list container itself is the object, not what it holds.

So, you can change mutable objects through the function argument list, without using a return statement.

In [6]:
def f(x):                    # 3. same object tagged with variable name x
    x[1] = 6                 # 4. change value of element in same object
    
#---------------------

myx = [2,2]                  # 1. define list object tagged by variable name myx

f(myx)                       # 2. call function: changes object, prints address

print(myx)                   # 5. print values: list is still changed outside
[2, 6]
In [ ]: