Argument Passing

Let’s expand on the notion of argument passing in Python. Earlier, we noted that arguments are passed by assignment ; this has a few ramifications that aren’t always obvious to beginners:

Arguments are passed by assigning objects to local names

Function arguments should be familiar territory by now: they’re just another instance of Python assignment at work. Function arguments are references to (possibly) shared objects referenced by the caller.

Assigning to argument names inside a function doesn’t affect the caller

Argument names in the function header become new, local names when the function runs, in the scope of the function. There is no aliasing between function argument names and names in the caller.

Changing a mutable object argument in a function may impact the caller

On the other hand, since arguments are simply assigned to objects, functions can change passed-in mutable objects, and the result may affect the caller.

Here’s an example that illustrates some of these properties at work:

>>> def changer(x, y):
...    x = 2             # changes local name's value only
...    y[0] = 'spam'     # changes shared object in place
...
>>> X = 1
>>> L = [1, 2]
>>> changer(X, L)        # pass immutable and mutable
>>> X, L                 # X unchanged, L is different
(1, ['spam', 2])

In this code, the changer function assigns to argument name x and a component in the object referenced by argument y. Since x is a local name in the function’s scope, the first assignment has no effect on the caller; it doesn’t change the binding of name X in the caller. Argument y is a local name too, but it’s passed a mutable object (the list called L in the caller); the result of the assignment to y[0] in the function impacts the value of L after the function returns. Figure 4.2 illustrates the name/object bindings that exist immediately after the function is called.

References: arguments share objects with the caller

Figure 4-2. References: arguments share objects with the caller

If you recall some of the discussion about shared mutable objects in Chapter 2, you’ll recognize that this is the exact same phenomenon at work: changing a mutable object in place can impact other references to the object. Here, its effect is to make one of the arguments an output of the function. (To avoid this, type y = y[:] to make a copy.)

Python’s pass-by-assignment scheme isn’t the same as C++’s reference parameters, but it turns out to be very similar to C’s in practice:

Immutable arguments act like C’s “by value” mode

Objects such as integers and strings are passed by object reference (assignment), but since you can’t change immutable objects in place anyhow, the effect is much like making a copy.

Mutable arguments act like C’s “by pointer” mode

Objects such as lists and dictionaries are passed by object reference too, which is similar to the way C passes arrays as pointers—mutable objects can be changed in place in the function, much like C arrays.

Of course, if you’ve never used C, Python’s argument-passing mode will be simpler still; it’s just an assignment of objects to names, which works the same whether the objects are mutable or not.

More on return

We’ve already discussed the return statement, and used it in a few examples. But here’s a trick we haven’t shown yet: because return sends back any sort of object, it can return multiple values, by packaging them in a tuple. In fact, although Python doesn’t have call by reference, we can simulate it by returning tuples and assigning back to the original argument names in the caller:

>>> def multiple(x, y):
...     x = 2                    # changes local names only
...     y = [3, 4]
...     return x, y              # return new values in a tuple
...
>>> X = 1
>>> L = [1, 2]
>>> X, L = multiple(X, L)        # assign results to caller's names
>>> X, L
(2, [3, 4])

It looks like we’re returning two values here, but it’s just one—a two-item tuple, with the surrounding parentheses omitted. If you’ve forgotten why, flip back to the discussion of tuples in Chapter 2.

Special Argument-Matching Modes

Although arguments are always passed by assignment, Python provides additional tools that alter the way the argument objects in the call are paired with argument names in the header. By default, they are matched by position, from left to right, and you must pass exactly as many arguments as there are argument names in the function header. But you can also specify a match by name, default values, and collectors for extra arguments.

Some of this section gets complicated, and before we get into syntactic details, we’d like to stress that these special modes are optional and only have to do with matching objects to names; the underlying passing mechanism is still assignment, after the matching takes place. But as an introduction, here’s a synopsis of the available matching modes:

Positionals: matched left to right

The normal case which we’ve used so far is to match arguments by position.

Keywords: matched by argument name

Callers can specify which argument in the function is to receive a value by using the argument’s name in the call.

varargs: catch unmatched positional or keyword arguments

Functions can use special arguments to collect arbitrarily many extra arguments (much as the varargs feature in C, which supports variable-length argument lists).

Defaults: specify values for arguments that aren’t passed

Functions may also specify default values for arguments to receive if the call passes too few values

Table 4.2 summarizes the syntax that specify the special matching modes.

Table 4-2. Function Argument-Matching Forms

Syntax

Location

Interpretation

func(value)

Caller

Normal argument: matched by position

func(name=value)

Caller

Keyword argument: matched by name

def func(name)

Function

Normal argument: matches any by position or name

def func(name=value)

Function

Default argument value, if not passed in the call

def func(*name)

Function

Matches remaining positional args (in a tuple)

def func(**name)

Function

dictionary)

In the caller (the first two rows of the table), simple names are matched by position, but using the name=value form tells Python to match by name instead; these are called keyword arguments.

In the function header, a simple name is matched by position or name (depending on how the caller passes it), but the name=value form specifies a default value, the *name collects any extra positional arguments in a tuple, and the **name form collects extra keyword arguments in a dictionary.

As a result, special matching modes let you be fairly liberal about how many arguments must be passed to a function. If a function specifies defaults, they are used if you pass too few arguments. If a function uses the varargs forms, you can pass too many arguments; the varargs names collect the extra arguments in a data structure.

A first example

Let’s look at an example that demonstrates keywords and defaults in action. In the following, the caller must always pass at least two arguments (to match spam and eggs), but the other two are optional; if omitted, Python assigns toast and ham to the defaults specified in the header:

def func(spam, eggs, toast=0, ham=0):   # first 2 required
    print (spam, eggs, toast, ham)

func(1, 2)                              # output: (1, 2, 0, 0)
func(1, ham=1, eggs=0)                  # output: (1, 0, 0, 1)
func(spam=1, eggs=0)                    # output: (1, 0, 0, 0)
func(toast=1, eggs=2, spam=3)           # output: (3, 2, 1, 0)
func(1, 2, 3, 4)                        # output: (1, 2, 3, 4)

Notice that when keyword arguments are used in the call, the order in which arguments are listed doesn’t matter; Python matches by name, not position. The caller must supply values for spam and eggs, but they can be matched by position or name. Also notice that the form name=value means different things in the call and def: a keyword in the call, and a default in the header.

A second example: Arbitrary-argument set functions

Here’s a more useful example of special argument-matching modes at work. Earlier in the chapter, we wrote a function that returned the intersection of two sequences (it picked out items that appeared in both). Here is a version that intersects an arbitrary number of sequences (1 or more), by using the varargs matching form *args to collect all arguments passed. Because the arguments come in as a tuple, we can process them in a simple for loop. Just for fun, we’ve also coded an arbitrary-number-arguments union function too; it collects items which appear in any of the operands:

def intersect(*args):
    res = []
    for x in args[0]:                  # scan first sequence
        for other in args[1:]:         # for all other args
            if x not in other: break   # item in each one?
        else:                          # no:  break out of loop
            res.append(x)              # yes: add items to end
    return res

def union(*args):
    res = []
    for seq in args:                   # for all args
        for x in seq:                  # for all nodes
            if not x in res:
                res.append(x)          # add new items to result
    return res

Since these are tools worth reusing (and are way too big to retype interactively), we’ve stored our functions in a module file called inter2.py here (more on modules in Chapter 5). In both functions, the arguments passed in at the call come in as the args tuple. As in the original intersect, both work on any kind of sequence. Here they are processing strings, mixed types, and more than two sequences:

% python
>>> from inter2 import intersect, union
>>> s1, s2, s3 = "SPAM", "SCAM", "SLAM"

>>> intersect(s1, s2), union(s1, s2)           # 2 operands
(['S', 'A', 'M'], ['S', 'P', 'A', 'M', 'C'])

>>> intersect([1,2,3], (1,4))                  # mixed types 
[1]

>>> intersect(s1, s2, s3)                      # 3 operands
['S', 'A', 'M']

>>> union(s1, s2, s3)
['S', 'P', 'A', 'M', 'C', 'L']

The gritty details

If you choose to use and combine the special matching modes, Python has two ordering rules:

  • In the call, keyword arguments must appear after all nonkeyword arguments.

  • In a function header, the *name must be after normal arguments and defaults, and **name must be last.

Moreover, Python internally carries out the following steps to match arguments before assignment:

  1. Assign nonkeyword arguments by position

  2. Assign keyword arguments by matching names

  3. Assign extra nonkeyword arguments to *name tuple

  4. Assign extra keyword arguments to **name dictionary

  5. Assign default values to unassigned arguments in header

This is as complicated as it looks, but tracing Python’s matching algorithm helps to understand some cases, especially when modes are mixed. We’ll postpone additional examples of these special matching modes until we do the exercises at the end of this chapter.

As you can see, advanced argument matching modes can be complex. They are also entirely optional; you can get by with just simple positional matching, and it’s probably a good idea to do so if you’re just starting out. However, some Python tools make us e of them, so they’re important to know.

Get Learning Python now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.