Mutability Revisited - Learning to Program with Python 3 (basics)




Hello and welcome to part 8 of the Python 3 basics tutorials. In this part, we're going to revisit the topic of mutable and immutable objects. This concept is masked pretty well in Python, which, like dynamic typing, can be great... or not. It can really bite you one day if you don't have a good understanding of how it works, so let's talk about it.

So far, we've been defining our TicTacToe game here with a function, and that function is modifying a variable that sits outside of that function, but this variable isn't actually a global variable. It's just a list of lists, which happens to be mutable.

The issue with this method, however, is that you might get used to doing things this way, then, one day, you'll be working with some other object that isn't actually mutable, and things are going to go poorly, and you'll be pulling out your hair trying to figure out why your program, which is infinitely logical, is betraying you!

Our code right now:

game = [[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]]


def game_board(player=0, row=0, column=0, just_display=False):
    print("   0  1  2")
    if not just_display:
        game[row][column] = player
    for count, row in enumerate(game):
        print(count, row)


game_board()
game_board(player=2, row=0, column=0)

Okay, what if we try to instead pass and return the game board? Here's our new function:

def game_board(current_game, player=0, row=0, column=0, just_display=False):
    print("   0  1  2")
    if not just_display:
        game[row][column] = player
    for count, row in enumerate(game):
        print(count, row)
    return current_game

Full code:

game = [[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]]


def game_board(current_game, player=0, row=0, column=0, just_display=False):
    print("   0  1  2")
    if not just_display:
        game[row][column] = player
    for count, row in enumerate(current_game):
        print(count, row)
    return current_game


game_board(game)
game_board(game, player=2, row=0, column=0)

Same output as before:

   0  1  2
0 [0, 0, 0]
1 [0, 0, 0]
2 [0, 0, 0]
   0  1  2
0 [2, 0, 0]
1 [0, 0, 0]
2 [0, 0, 0]
>>> 

If we print game at the end too:

print(game)
[[2, 0, 0], [0, 0, 0], [0, 0, 0]]

So our game variable has still been modified, despite it instead being passed as a parameter with a different name.

One nifty thing we can do to know for sure if a thing is actually pointing to the same object in memory is to check it's ID in memory with id(), another built-in function in Python. Let's see:

game = [[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]]


def game_board(current_game, player=0, row=0, column=0, just_display=False):
    print("   0  1  2")
    if not just_display:
        game[row][column] = player
    for count, row in enumerate(current_game):
        print(count, row)
    return current_game


game_board(game)
game_board(game, player=2, row=0, column=0)

print(game)
print(id(game))

Output:

2190893038216
   0  1  2
0 [0, 0, 0]
1 [0, 0, 0]
2 [0, 0, 0]
2190893038216
   0  1  2
0 [2, 0, 0]
1 [0, 0, 0]
2 [0, 0, 0]
[[2, 0, 0], [0, 0, 0], [0, 0, 0]]
2190893038216

The ID in memory is always the same one. So clearly we aren't doing anything extra by doing all this jazz...so what the heck man, why are you leading us down this path?!

Alright, let's make some slight changes to our script!

game = "I want to play a game"

def game_board():
    game = "A game"

game_board()
print(game)

All we did here was change the game_board function to just set a new value to game, and game is initially a different string above.

The actual logic above is exactly the same as we've done to modify our game.

Without running it, you would have to assume that, when we print game at the end, we're going to see "A game... but alas:

I want to play a game
>>> 

Whaaaaaaat?

Surprise, Python strings are immutable. Oops. Hope you knew that! If you were to quiz Python developers, I suspect a staggering number of the would not be able to confidentally answer if strings are mutable or not. So what do you do if you want to still use a function? Well, you can re-define:

game = "I want to play a game"

def game_board():
    game = "A game"
    return game

game = game_board()
print(game)
A game
>>> 

Let's watch the unique id:

game = "I want to play a game"
print(id(game))

def game_board():
    game = "A game"
    print(id(game))
    return game

game = game_board()
print(game)
print(id(game))
1877997465648
1877997362064
A game
1877997362064
>>> 

So you can see that the initial id only shows up once, because the next id we print is the one in the game_board function, which is unique. Then, we re-assign game to be this same object, so it gets the same unique id.

Okay, so that's how we can handle immutable objects. What about mutable? Should we treat them different? Well, we can actually handle them the same way:

game = [1, 2, 3]
print(id(game))


def game_board():
    game[1] = 99
    print(id(game))
    return game


game = game_board()
print(game)
print(id(game))

Output here:

2196144151304
2196144151304
[1, 99, 3]
2196144151304
>>> 

Everything appears to have worked as we expected, and our code handled for varying types of objects. To me, it just makes more sense to go ahead and plan for this, unless of course you're a programmer who never makes a mistake. I just know that's not me.

But wait, there's more! Global!!! Let's check that out. Remember our game from before?

game = "I want to play a game"


def game_board():
    game = "A game"
    return game


game_board()
print(game)
I want to play a game
>>> 

Notice that we are NOT re-defining here in this example.

Alright, now, what if we set game to be a global?

game = "I want to play a game"


def game_board():
    global game
    game = "A game"
    return game


game = game_board()
print(game)
A game
>>> 

Ooooooo, would you look at that. Let's check the ids:

game = "I want to play a game"
print(id(game))


def game_board():

    global game
    print(id(game))
    game = "A game"
    print(id(game))
    return game


game_board()
print(game)

So, we can also use globals to modify variables like this. I went a very long way in Python before I really understood mutable vs immutable. I wound up treating *everything* in Python in a situation like this as if it was immutable, and... it served me pretty darn well. I cant imagine the reverse of that going very well!

Let's get back to our TicTacToe game, but, before I leave for the next tutorial, here's a little quiz. If you think you're all set on mutable vs immutable, write down the values you expect to see in order from the following quiz, then run it:

x = 1
def test():
    x = 2
test()
print(x)


x = 1
def test():
    global x
    x = 2
test()
print(x)


x = [1]
def test():
    x = [2]
test()
print(x)


x = [1]
def test():
    global x
    x = [2]
test()
print(x)


x = [1]
def test():
    x[0] = 2
test()
print(x)

I took this quiz after 7 years of Python, prepared to be tricked, and still missed one. Chances are too, as you quickly are writing code, you'll probably even be less accurate than you were on the quiz. Alright. Let's get back to TicTacToe and start talking about more things that can go wrong!

The next tutorial:





  • Introduction to Python 3 (basics) - Learning to Program with Python 3
  • Tuples, Strings, Loops - Learning to Program with Python 3 (basics)
  • Lists and Tic Tac Toe Game - Learning to Program with Python 3 (basics)
  • Built-in Functions - Learning to Program with Python 3 (basics)
  • Indexes and Slices - Learning to Program with Python 3 (basics)
  • Functions - Learning to Program with Python 3 (basics)
  • Function Parameters and Typing - Learning to Program with Python 3 (basics)
  • Mutability Revisited - Learning to Program with Python 3 (basics)
  • Error Handling - Learning to Program with Python 3 (basics)
  • Calculating Horizontal Winner (tic tac toe) - Learning to Program with Python 3 (basics)
  • Vertical Winners - Learning to Program with Python 3 (basics)
  • Diagonal Winners - Learning to Program with Python 3 (basics)
  • Bringing Things Together - Learning to Program with Python 3 (basics)
  • Wrapping up Tic Tac Toe - Learning to Program with Python 3 (basics)
  • Conclusion - Learning to Program with Python 3 (basics)