Functions: arguments and scope

In our first post on Python functions, we saw how to define a simple function and pass arguments to it. Python offers a lot of flexibility in how arguments can be passed to functions, so we’ll explore those here.

In the functions we’ve seen so far, a list of arguments is provided when the function is defined, as in

def simpleFunction(arg1, arg2, arg3):
    <statements>

When such a function is called, the number of arguments in the calling statement must match those in the function definition, both in the number of arguments and their order. Thus if we call simpleFunction() and we want arg1 to have the value 42, arg2 to be 71 and arg3 to be -95, we have to write the call as simpleFunction(42, 71, -95). Arguments passed this way are known as positional arguments, as their values within the function are determined by their positions in the calling statement.

Keyword arguments

It is possible to pass arguments to a function in any order if we specify the argument’s name together with its value in the call. For example, we could perform the call above with the statement

simpleFunction(arg3 = -95, arg1 = 42, arg2 = 71)

It’s possible to mix positional and keyword arguments in the same call, but only if all the positional arguments are given before all of the keyword arguments. The positional arguments are matched to the corresponding arguments in the function definition, and any arguments left over must be provided by the keyword arguments. Values must be provided for all the function arguments. Consider the following calls to simpleFunction():

simpleFunction(12,arg3=24,arg2=34)
simpleFunction(12,arg3=24,arg1=34)
simpleFunction(arg3=24,arg1=34)
simpleFunction(arg3=24,arg1=34,12)

The first call is valid, and assigns 12 to arg1, 34 to arg2 and 24 to arg3. All the other calls are invalid, however. simpleFunction(12,arg3=24,arg1=34) attempts to assign two different values to arg1 (and provides no value for arg2). simpleFunction(arg3=24,arg1=34) provides no value for arg2. simpleFunction(arg3=24,arg1=34,12) has keyword arguments before the positional argument.

Default arguments

When defining a function, default values can be specified for some or all of its arguments. For example, we might modify simpleFunction() above to look like this:

def simple2(arg1, arg2, arg3, arg4 = 'fourth', arg5 = 'fifth'):
    print(arg1, arg2, arg3, arg4, arg5)

Valid calls include

simple2(1,2,3)
simple2(1,2,3,4)
simple2(1,2,3,4,5)

The output from the first call is 1 2 3 fourth fifth, from the second is 1 2 3 4 fifth and from the last is 1 2 3 4 5.

Arguments with defaults must always come after arguments without defaults. Thus wrong(arg1 = 'something', arg2) is an invalid function definition.

You can mix default arguments and keyword arguments in a single function call. For example, with the definition of simple2 above, the following are all valid calls:

simple2(1, 2, arg4 = 'out of order', arg3 = 99)
simple2(arg2 = 12, arg3 = 66, arg1 = -34)
simple2(arg2 = 12, arg3 = 66, arg1 = -34, arg5 = 55)
simple2(arg5 = 234, arg4 = -123, arg1 = 1, arg2 = 2, arg3 = 3)

The key point is that values must be provided for all positional arguments (though the order of the arguments doesn’t matter if you’re using keywords). Providing values for default arguments is optional. Any arguments passed without keywords must occur at the beginning of the call.

Thus the statement simple2(1, 2, arg4 = 'out of order', arg3 = 99) assigns arg1 to 1 and arg2 to 2, uses the keyword assignments for arg3 and arg4, and the default argument for arg5.

simple2(arg2 = 12, arg3 = 66, arg1 = -34) uses keywords for arg1, arg2 and arg3 and default values for arg4 and arg5. Similar rules apply to the other two calls. Note that values are always provided for arg1, arg2 and arg3 in some form in all valid calls. Omitting any of these positional arguments results in an error.

Arbitrary number of arguments

If you want a function to handle a variable number of arguments, you can do this with the following syntax:

def arbArgs(*staff):
    print(staff)
    for i in staff:
        print(i)

The argument *staff indicates that staff is a tuple containing an arbitrary number of elements. Thus we can call arbArgs() as:

arbArgs('Alice', 'Bob', 'Carol', 'Dave')
arbArgs('Alice', 'Bob', 'Carol', 'Dave', 'Ethel')
arbArgs()

The first two calls pass all the arguments into arbArgs() as a single tuple. The last call, without arguments, passes an empty tuple into the function.

If we have a number of arguments that are all to be treated similarly within the function, it’s probably more intuitive to pass these arguments together as a list or tuple and then sort them out within the function. For example, we could replace the above example with:

def staffList(staff):
    for i in staff:
        print(i)

staffMembers = ['Alice', 'Bob', 'Carol', 'Dave']
staffList(staffMembers)

It’s also possible to define a function that takes an arbitrary number of keyword arguments, as in the following example:

def arbKeys(**data):
     print(data['first'], data['last'], data['age'])
     print(data)

arbKeys(first = 'Alice', last = 'Smith', age = 34)

Here, the arguments are passed as entries in a Python dictionary. Thus we can access individual arguments by their keywords. You need to ensure that each keyword referred to in the function is provided in the arguments passed to the function.

Scope of variables

When you define a variable in a program, that portion of the program which recognizes that variable is known as variable’s scope. The topic of scope is quite broad, once we cover other aspects of Python such as classes and namespaces. For the moment, however, we’ll have a look at the scope of variables as it relates to functions.

A variable defined outside of all the functions in a program (that is, in the main block of code) has what is known as global scope, meaning that it can be referred to anywhere in the program, both in the main code block and inside functions. For example:

def func1():
    print(x)

x = 123
func1()

The output from this program is just 123, the value of x. In this case, x was initialized outside func1() but since it has global scope, its value is accessible within the function.

But now consider this code:

def func2():
     x = 456
     print('x in func2:', x)

x = 123
func2()
print(x)

We find that when x is printed inside func2() it has the value 456, but when it is printed out in the main code after calling func2(), it still has the value 123. What happened?

The rule is that a variable defined outside a function has global read-only scope. If such a variable is changed inside a function, it becomes a local variable, accessible only within the function. In the code above, when x is created on line 5, it points to the object 123. Inside func2(), the assignment x = 456 on line 2 creates a new variable x local to func2() and points it at the object 456. The original x from line 5 still points to 123.

Suppose we really do want to change the value of the global variable x within a function. To do this, we need to tell Python that we want x to refer to the global variable everywhere inside the function, even if we reassign x to point somewhere else. We can do this with the global keyword, as in this example:

def func2():
     global x
     x = 456
     print('x in func2:', x)

x = 123
func2()
print(x)

We now find that the value of x is 456 both within the function and when printed out at the end, after the function call.

If you poke about on the web, you’ll probably find arguments (of the disagreement kind, not parameters passed to functions!) raging about whether it’s good practice to use the global keyword at all. My own feeling is that it has its place (otherwise it probably wouldn’t have been included in the language in the first place), but like any language feature, it needs to be used with care.

Exercise 1

Write a program that allows the user to enter details about their comic book collection. (Ideally, this program would be linked to a database for storing the information, but we haven’t looked at databases yet. Patience.) The data stored for each comic should be: year of issue, issue number, title and publisher.

There are various ways this can be done, but here’s one suggestion. Define a function addComic() which takes as arguments the four items of data for a given comic book. The publisher should be set to a default value (choose your favourite comic book publisher, such as Marvel or DC, for example). Thus for each comic book, the user can input either 3 or 4 items of data, depending on whether they enter the publisher. For each comic book, the addComic() function should be called, using keyword arguments to pass the data to the function.

The collection of data should be stored in a master list called myComics, which is a list of tuples, with each tuple representing one comic. myComics should be a global variable and should not be passed to the addComic() function, but this function should add each comic to the list.

See answer
def addComic(year, issueNumber, title, publisher = 'Marvel'):
    global myComics
    newComic = year, issueNumber, title, publisher
    myComics += [newComic]

myComics = []
while True:
    comic = input('Enter [publisher], title, year, issue number (or \'quit\'):')
    if comic == 'quit':
        break
    comicData = comic.split(',')
    if len(comicData) == 3:
        addComic(year = comicData[1], issueNumber = comicData[2], title = comicData[0])
    elif len(comicData) == 4:
                addComic(year = comicData[2], issueNumber = comicData[3], 
                         title = comicData[1], publisher = comicData[0])
print(myComics)

The user should input the data for each comic, with each item separated from the next by a comma (with no extra spaces). On line 12, we test if the number of data items is 3, in which case we assume that the user wishes to use the default value for the publisher. We use keywords to call addComic() on line 13. Similarly, on lines 14 through 16 we handle the case where the user entered 4 data items, which includes the publisher. [I’ve left out error checks such as what to do if the user doesn’t enter 3 or 4 data items, but you can add these if you like.]

The global list myComics is initialized on line 6 and declared global within the function on line 2, so any additions to the list within the function will be reflected in its global value.

Exercise 2

Rewrite your comic book program from Exercise 1 so that the function addComic() takes a variable number of keyword arguments. In this case, there is no default value for the publisher, so the user will have to enter all 4 items of data for each comic.

See answer
def addComic(**comicData):
    global myComics
    newComic = comicData['year'], comicData['issueNumber'],  \
        comicData['title'], comicData['publisher']
    myComics += [newComic]

myComics = []
while True:
    comic = input('Enter publisher, title, year, issue number (or \'quit\'):')
    if comic == 'quit':
        break
    comicData = comic.split(',')
    if len(comicData) == 4:
        addComic(year = comicData[2], issueNumber = comicData[3], 
                    title = comicData[1], publisher = comicData[0])
print(myComics)

The data are passed to addComic() as a dictionary, so we use the keywords to sort out the 4 items for each comic. Other than that, the program works much the same as before. The backslash \ at the end of line 3 indicates that that line of code is continued on the next line.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.