Let’s look at a second function example that does something a bit more useful than multiplying arguments and further illustrates function basics.
In Chapter 13, we coded a for loop that collected items held in common in two strings. We noted there that the code wasn’t as useful as it could be because it was set up to work only on specific variables and could not be rerun later. Of course, we could copy the code and paste it into each place where it needs to be run, but this solution is neither good nor general—we’d still have to edit each copy to support different sequence names, and changing the algorithm would then require changing multiple copies.
By now, you can probably guess that the solution to this dilemma is to package the for loop inside a function. Doing so offers a number of advantages:
Putting the code in a function makes it a tool that you can run as many times as you like.
Because callers can pass in arbitrary arguments, functions are general enough to work on any two sequences (or other iterables) you wish to intersect.
When the logic is packaged in a function, you have to change code in only one place if you ever need to change the way the intersection works.
Coding the function in a module file means it can be imported and reused by any program run on your machine.
In effect, wrapping the code in a function makes it a general intersection utility:
def intersect(seq1, seq2):
res = [] # Start empty
for x in seq1: # Scan seq1
if x in seq2: # Common item?
res.append(x) # Add to end
return res
The transformation from the simple code of Chapter 13 to this function is straightforward; we’ve just nested the original logic under a def header and made the objects on which it operates passed-in parameter names. Because this function computes a result, we’ve also added a return statement to send a result object back to the caller.
Before you can call a function, you have to make it. To do this, run its def statement, either by typing it interactively or by coding it in a module file and importing the file. Once you’ve run the def, you can call the function by passing any two sequence objects in parentheses:
>>>s1 = "SPAM">>>s2 = "SCAM">>>intersect(s1, s2)# Strings ['S', 'A', 'M']
Here, we’ve passed in two strings, and we get back a list containing the characters in common. The algorithm the function uses is simple: “for every item in the first argument, if that item is also in the second argument, append the item to the result.” It’s a little shorter to say that in Python than in English, but it works out the same.
To be fair, our intersect function is fairly slow (it executes nested loops), isn’t really mathematical intersection (there may be duplicates in the result), and isn’t required at all (as we’ve seen, Python’s set data type provides a built-in intersection operation). Indeed, the function could be replaced with a single list comprehension expression, as it exhibits the classic loop collector code pattern:
>>> [x for x in s1 if x in s2]
['S', 'A', 'M']
As a function basics example, though, it does the job—this single piece of code can apply to an entire range of object types, as the next section explains. In fact, we’ll improve and extend this to support arbitrarily many operands in Chapter 18, after we learn more about argument passing modes.
Like all good functions in Python, intersect is polymorphic. That is, it works on arbitrary types, as long as they support the expected object interface:
>>>x = intersect([1, 2, 3], (1, 4))# Mixed types >>>x# Saved result object [1]
This time, we passed in different types of objects to our function—a list and a tuple (mixed types)—and it still picked out the common items. Because you don’t have to specify the types of arguments ahead of time, the intersect function happily iterates through any kind of sequence objects you send it, as long as they support the expected interfaces.
For intersect, this means that the first argument has to support the for loop, and the second has to support the in membership test. Any two such objects will work, regardless of their specific types—that includes physically stored sequences like strings and lists; all the iterable objects we met in Chapter 14, including files and dictionaries; and even any class-based objects we code that apply operator overloading techniques we’ll discuss later in the book.[34]
Here again, if we pass in objects that do not support these interfaces (e.g., numbers), Python will automatically detect the mismatch and raise an exception for us—which is exactly what we want, and the best we could do on our own if we coded explicit type tests. By not coding type tests and allowing Python to detect the mismatches for us, we both reduce the amount of code we need to write and increase our code’s flexibility.
Probably the most interesting part of this example, though, is its names. It turns out that the variable res inside intersect is what in Python is called a local variable—a name that is visible only to code inside the function def and that exists only while the function runs. In fact, because all names assigned in any way inside a function are classified as local variables by default, nearly all the names in intersect are local variables:
res is obviously assigned, so it is a local variable.
Arguments are passed by assignment, so seq1 and seq2 are, too.
The for loop assigns items to a variable, so the name x is also local.
All these local variables appear when the function is called and disappear when the function exits—the return statement at the end of intersect sends back the result object, but the name res goes away. Because of this, a function’s variables won’t remember values between calls; although the object returned by a function lives on, retaining other sorts of state information requires other sorts of techniques. To fully explore the notion of locals and state, though, we need to move on to the scopes coverage of Chapter 17.
[34] This code will always work if we intersect files’ contents obtained with file.readlines(). It may not work to intersect lines in open input files directly, though, depending on the file object’s implementation of the in operator or general iteration. Files must generally be rewound (e.g., with a file.seek(0) or another open) after they have been read to end-of-file once, and so are single-pass iterators. As we’ll see in Chapter 30 when we study operator overloading, objects implement the in operator either by providing the specific __contains__ method or by supporting the general iteration protocol with the __iter__ or older __getitem__ methods; classes can code these methods arbitrarily to define what iteration means for their data.