Lesson 9: Functions and Modules

So far, we've only used functions (and object methods, which are similar) but we haven't created any of our own. Functions are very powerful, allowing you to re-use code over and over with different inputs. We will also cover modules, which package many functions into single files (or batches of files) that you can reference from a script or the REPL.

Function Declaration and Usage

you declare a function using the def statement. this has the pattern of def name(arguments): and is followed by a block. Once a function is defined, you can call it by entering that name, followed by an argument list in parntheses.

You can create a function with no arguments

def noarg():
pass

which can then be called with noarg(),

or one with a list of arguments (the names will become variables in the function's scope, and will not get the value of any global vars with the same names)

def somearg(a, b):
pass

The above can be called by somearg(1, 2) for instance. That would set a to 1 and b to 2. The names are filled in in the order that they appear. If you try to call somearg with less than two arguments, the call will fail.

Furthermore, you can have functions with default arguments, which correspond to keywords. This acts similarly to the print() function we've been using a few times, when we reference its sep and end options.

def keyed(a, b, c=3, d=4):
pass

This can be called with two arguments like keyed(1, 2) and the other two arguments will fill in. You can also call it with keyed(1, 2, 5, 6) to fill in c with 5 and d with 6, or with keyed(1, 2, c=5) to fill in c with 5 but leave d default, and etc. Basically, you can enter the arguments in order as expected, or you can leave them out and keep default values, or you can explicitly call them by their keywords using the = notation.

Functions are therefore very flexible, indeed. Use whichever style of function you feel is best for the particular code you want. No matter what, you should put a comment at the top of a function to explain what it does, so you don't have to sort that out again when you look back at your code days or months later.

There are also ways to enter functions with a varying number of arguments, similar to how the print() statement works with lists of items, but we'll save that for Lesson 11, when we can also cover arbitrary keywords using dictionaries.

All functions have the option to return a value. This can be anything that a variable can contain, including lists, tuples, objects and dictionaries. So, if you want to return multiple items, consider putting them in a tuple or dictionary. (We will cover dictionaries in Lesson 11.)

def echo(e):
return e # returns what we were given

def echo2(a, b):
return (a, b) # returns the two items as a tuple

Function Scope

All arguments for a function, and all variables assigned inside of a function, are local to that function. This means their scope is within that function only, and they can't be accessed from outside of that function.

x = 2 # global variable
def ex():
x = 3 # local variable declared here
print(x) # prints 3
ex()
print(x) # prints 2

However, you can access a global variable from inside a function, as long as you never attempt to assign to that variable.

x = 2
def ex():
print(x) # prints 2 if the next line is not present
x += 1 # this produces an error, because x was referenced before this!

Basically, the above makes Python believe x is a local variable (because it was assigned) but it's being referenced beforehand. Thus, it won't try to read x from global scope, and will throw an exception.

To prevent this, we can use the global keyword, but it will cause x to be the one in global scope at all times inside the function. That is, there will be no local x, only the global x, when inside the function.

x = 2
def ex():
global x # x is now the global variable
print(x)
x += 1
ex()
print(x) # prints 3, because ex added 1 to it.

The global keyword can also take multiple variable names, separated by commas.

You can also embed functions inside of other functions! Such functions are local names inside the function containing them, and cannot be called outside that function's scope. When you do this, each contained function also has its own scope, where names referenced that aren't declared inside that function are taken from the innermost containing function above it. So you get oddities like the below.

x = 2
y = 4
def outer():
x = 3
def inner():
global y
print("x", x, "y", y, "inner")
y = 6
print("after assignment...")
print("x", x, "y", y, "inner")
print("x", x, "y", y, "outer")
inner()
print("after global y altered...")
print("x", x, "y", y, "outer")
print("x", x, "y", y, "global")
outer()
print("after functions run...")
print("x", x, "y", y, "global")

The output from this is:

x 2 y 4 global
x 3 y 4 outer
x 3 y 4 inner
after assignment...
x 3 y 6 inner
after global y altered...
x 3 y 6 outer
after functions run...
x 2 y 6 global

If you follow the execution by hand, you can see why this is happening. The x value visible to inner() is always that of outer(), since the global x is overridden. Meanwhile both outer() and inner() reference the global y, and inner() uses the global keyword to assign to that variable as well.

So what if you wanted to reference the outer() function's x from inside inner() and assign to it? You would need to use the nonlocal keyword. nonlocal works exactly the same as global, where you list one or more variable names, but instead of referencing the global scope, it references the innermost containing scope that declares that local variable.

x = 2
y = 3
def outer():
x = 4 # new local variable
def inner():
nonlocal x
global y
# now we can assign to both x and y from here!
# the x affected will alter outer's, not the global x.
x = 5
y = 6
print("x", x, "y", y, "inner")
print("x", x, "y", y, "outer")
inner()
print("x", x, "y", y, "outer")
print("x", x, "y", y, "global")
outer()
print("x", x, "y", y, "global")

Note that we need global y here, because if we use nonlocal x, y Python will be unhappy with the lack of a y variable in a scope it can reach. Global variables are treated as an ultimate outer scope that nonlocal cannot touch, thus we need global y.

If this seems confusing, don't worry, it is! Play with scope a bit until you understand what's going on. In general, when you think you might need to rely on a global variable, you're better off using either a function argument or an object to store the data instead (we'll get to objects and object methods in Lesson 11).

Modules

Python contains libraries of functions and objects inside of modules, also called packages. Modules are script files that are either included singly (as just one .py file) or in a nested directory structure, such that you can access parts of the module instead of the whole system.

Say you wanted to create a function set that handles Fibonacci numbers. This is some code that could work.

# Fibonacci numbers module

# return a list containing Fibonacci series up to the nth item in the sequence.
# F[0] is equal to a, and F[1] is equal to b.
# By default these are 0 and 1 for the classical Fibonacci sequence.
# n must be an integer or this will not work as expected.
def fib(n):
a = 0
b = 1
n = int(n)
if(n < 0):
return []
elif(n == 0):
return [a]
elif(n == 1):
return [a,b]
# else we're going to do the actual loop.
r = [a,b]
i = 1
while(i < n):
c = a + b;
a = b;
b = c;
r.append(c)
i += 1
return r

Save the above as fibo.py, and you can then include it as a module in a new file you open in the same directory. To do this, type:

import fibo

into the script you want to import into, or into the REPL. This will then import fib() into your code, though as part of a module object named fibo. Thus you would need to call it with

fibo.fib(n)

You can also check the contents of a module with the dir() built-in function. This produces a list containing all the names imported from that module. This may include a bunch of hidden names surrounded by double underscores like '__name__', along with the expected names.

Meanwhile if you type

from fibo import fib

you would get fib() as a global function. You can also take such a module with multiple names you want to import, and list them with commas in between, or you can import ALL names in a module with

from fibo import *

where the * indicates you want everything from that module.

If you'd like to create your own modules, with multiple files, please see https://docs.python.org/3/tutorial/modules.html for a more complete explanation of how package directories work. This is also where the idea for the Fibonacci series generator module came from, however theirs works a bit differently, and treats its argument as an upper limit instead of a maximum index.

Now, since we're going to do a game jam at the end of this, we're going to need to install the module, and demonstrate how to include it.

Installing pygame

Before we try to use the module for pygame, we need to install it. Thankfully, as long as you're on a modern operating system, this should be simple! Open up your command line interface, and type in

python3 -m pip install pygame --user

This should install the pygame module into your base folders. Then you can run

python3 -m pygame.examples.aliens

to open up a test game (it's kind of like Space Invaders but not quite). Warning: the game does have sound and it may be loud. Press the Escape key to exit out of the game.

If you have problems installing, see https://www.pygame.org/wiki/GettingStarted for help with this.

Once you've installed and checked your install, you can import the module in Python using

import pygame

test that by checking the dir(pygame) output. Note that this is a very large module. Don't worry about what this huge list of stuff means, we'll cover the important parts soon enough. But since nearly all of what we need involves objects, we'll save that for Lesson 11.

Exercise: Programming Assignment L9

Before we start to use pygame, let's try making one small game with what you know so far. You'll be making Tic Tac Toe!

Note that this may take two or three days to complete, assuming you're poking at this for maybe an hour or two per day. Most of that time would be spent making sure you understand the core code you're given here.

First, download the module ttt.py made for this assignment. It includes the following useful functions:

ttt.staterate() takes two arguments. The first argument should be a list with nine elements; this is the game state for Tic Tac Toe. The second should be a string containing either "X" or "O" and refers to which player is checking state; results for the other player will be the negative of the current player. The return value is an integer that is higher the closer to winning that player is, and lower the closer their opponent is to winning. A return value of 100 means the board is a winning state, and -100 is a losing state.

ttt.nextmove() also takes those two arguments, the state first, and the player "X" or "O" second. It returns the move that player could take next which would give the best possible result, but only looks one move ahead. If multiple options look equally good, it chooses one at random.

ttt.board2i() takes a string as input and converts from a board position (such as "A2") to a board index between 0 and 8, returning the result. The letter corresponds to the row, and the number to the column, as the example board output shows below. If the input string is incorrect, it throws an Exception of "Invalid board position".

ttt.i2board() takes a board index between 0 and 8 as input, and returns a board position in the above format. If the input board index is out of bounds, it throws an Exception of "Invalid board state index".

You can use ttt.staterate(state, player) to determine if the game is in a winning or losing state at present.

Use ttt.nextmove(state, ai) to get the index of the board list that gives the computer player the best possible result, looking only at the current board state. ttt.nextmove() does not look ahead past its own move, so it's not a very smart AI system, but it'll do for Tic Tac Toe; it may be impossible to do better than tie against this AI.

Your Tic Tac Toe state should be a list with nine elements. The first three make up the first row, the next three the second row, and the last three the final row. So to translate an (x, y) pair of coordinates, you would go for state[x + 3 * y] if they're both between 0 and 2.

The state should initially be filled with nine of "."; there is an easy way to do this using a type casting function and string multiplication.

Your Tic Tac Toe game should display the board like this:

  1 2 3
A . . X
B . X O
C O . .

(this corresponds to a board state of [".", ".", "X", ".", "X", "O", "O", ".", "."])

You should display the board after each move, player's or AI's, and say where the AI moved in the same format as the player enters their moves.

You should take a string in a format like "A2" as input from the user, to determine where the player wants to put a piece. Make sure to handle errors and bad input from the player.

The player should be given the option of X or O by random choice, at the start of the game. To get this, import the "random" module from python's standard library, and use random.randint(0, 1). A 0 gives the player "O", a 1 gives them "X". X always goes first.

The game should end when either player wins, or when the game has gone on for nine plays (and thus the board is full).

When the game ends, tell the player if they won or lost, or the game ended in a draw, after showing the final game board.

The file you complete should contain at least three functions:

  • a function for the main program, which is run at the bottom of the file. This will run the game loop, which does all the displays, inputs and required function calls to make the game act as explained above. I suggest naming it main()

  • a function for attempting a move, from the player or the ai; this should return True if the move was successful, or False otherwise. It should alter the game board state, which must be one of its arguments.

  • a function for determining if the state is a win, loss, or draw. At minimum this must take the game board state and player token ("X" or "O") as arguments; it may also take the number of turns as a third argument, if you wish.
    It should return a tuple; the first item in the return value must be True if the game is over, False otherwise. The second item should be 1 if X has won, -1 if O has won, and 0 if the game is still going or ended in a draw. So for most of the game (False, 0) will be returned, and if X won it would return (True, 1).

You must also handle all exceptions that result from usage of ttt.py and Python modules.

Feel free to read through the ttt.py and learn how it works. Understanding the basic AI inside might help you design your own game AI in the future.

Previous
Next