Advanced Functions

In this chapter, we go beyond the basics of using functions. I’ll assume you can define and work with functions taking default arguments:

  1. >>> def foo(a, b, x=3, y=2):
  2. ... return (a+b)/(x+y)
  3. ...
  4. >>> foo(5, 0)
  5. 1.0
  6. >>> foo(10, 2, y=3)
  7. 2.0
  8. >>> foo(b=4, x=8, a=1)
  9. 0.5

Notice the last way foo is called: with the arguments out of order, and everything specified by key-value pairs. Not everyone knows that you can call any function in Python this way. So long as the value of each argument is unambiguously specified, Python doesn’t care how you call the function (and this case, we specify b, x and a out of order, letting y be its default value). We’ll leverage this flexibility later.

This chapter’s topics are useful and valuable on their own. And they are important building blocks for some extremely powerful patterns, which you learn in later chapters. Let’s get started!

Accepting & Passing Variable Arguments

The foo function above can be called with either 2, 3, or 4 arguments. Sometimes you want to define a function that can take any number of arguments - zero or more, in other words. In Python, it looks like this:

  1. # Note the asterisk. That's the magic part
  2. def takes_any_args(*args):
  3. print("Type of args: " + str(type(args)))
  4. print("Value of args: " + str(args))

See carefully the syntax here. takes_any_args() is just like a regular function, except you put an asterisk right before the argument args. Within the function, args is a tuple:

  1. >>> takes_any_args("x", "y", "z")
  2. Type of args: <class 'tuple'>
  3. Value of args: ('x', 'y', 'z')
  4. >>> takes_any_args(1)
  5. Type of args: <class 'tuple'>
  6. Value of args: (1,)
  7. >>> takes_any_args()
  8. Type of args: <class 'tuple'>
  9. Value of args: ()
  10. >>> takes_any_args(5, 4, 3, 2, 1)
  11. Type of args: <class 'tuple'>
  12. Value of args: (5, 4, 3, 2, 1)
  13. >>> takes_any_args(["first", "list"], ["another","list"])
  14. Type of args: <class 'tuple'>
  15. Value of args: (['first', 'list'], ['another', 'list'])

If you call the function with no arguments, args is an empty tuple. Otherwise, it is a tuple composed of those arguments passed, in order. This is different from declaring a function that takes a single argument, which happens to be of type list or tuple:

  1. >>> def takes_a_list(items):
  2. ... print("Type of items: " + str(type(items)))
  3. ... print("Value of items: " + str(items))
  4. ...
  5. >>> takes_a_list(["x", "y", "z"])
  6. Type of items: <class 'list'>
  7. Value of items: ['x', 'y', 'z']
  8. >>> takes_any_args(["x", "y", "z"])
  9. Type of args: <class 'tuple'>
  10. Value of args: (['x', 'y', 'z'],)

In these calls to takes_a_list and takes_any_args, the argument items is a list of strings. We’re calling both functions the exact same way, but what happens in each function is different. Within takes_any_args, the tuple named args has one element - and that element is the list ["x", "y", "z"]. But in takes_a_list, items is the list itself.

This *args idiom gives you some very helpful programming patterns. You can work with arguments as an abstract sequence, while providing a potentially more natural interface for whomever calls the function.

Above, I’ve always named the argument args in the function signature. Writing *args is a well-followed convention, but you can choose a different name - the asterisk is what makes it a variable argument. For instance, this takes paths of several files as arguments:

  1. def read_files(*paths):
  2. data = ""
  3. for path in paths:
  4. with open(path) as handle:
  5. data += handle.read()
  6. return data

Most Python programmers use *args unless there is a reason to name it something else.[6] That reason is usually readability; read_files is a good example. If naming it something other than args makes the code more understandable, do it.

Argument Unpacking

The star modifier works in the other direction too. Intriguingly, you can use it with any function. For example, suppose a library provides this function:

  1. def order_book(title, author, isbn):
  2. """
  3. Place an order for a book.
  4. """
  5. print("Ordering '{}' by {} ({})".format(
  6. title, author, isbn))
  7. # ...

Notice there’s no asterisk. Suppose in another, completely different library, you fetch the book info from this function:

  1. def get_required_textbook(class_id):
  2. """
  3. Returns a tuple (title, author, ISBN)
  4. """
  5. # ...

Again, no asterisk. Now, one way you can bridge these two functions is to store the tuple result from get_required_textbook, then unpack it element by element:

  1. >>> book_info = get_required_textbook(4242)
  2. >>> order_book(book_info[0], book_info[1], book_info[2])
  3. Ordering 'Writing Great Code' by Randall Hyde (1593270038)

Writing code this way is tedious and error-prone; not ideal.

Fortunately, Python provides a better way. Let’s look at a different function:

  1. def normal_function(a, b, c):
  2. print("a: {} b: {} c: {}".format(a,b,c))

No trick here - it really is a normal, boring function, taking three arguments. If we have those three arguments as a list or tuple, Python can automatically "unpack" them for us. We just need to pass in that collection, prefixed with an asterisk:

  1. >>> numbers = (7, 5, 3)
  2. >>> normal_function(*numbers)
  3. a: 7 b: 5 c: 3

Again, normal_function is just a regular function. We did not use an asterisk on the def line. But when we call it, we take a tuple called numbers, and pass it in with the asterisk in front. This is then unpacked within the function to the arguments a, b, and c.

There is a duality here. We can use the asterisk syntax both in defining a function, and in calling a function. The syntax looks very similar. But realize they are doing two different things. One is packing arguments into a tuple automatically - called "variable arguments"; the other is un-packing them - called "argument unpacking". Be clear on the distinction between the two in your mind.

Armed with this complete understanding, we can bridge the two book functions in a much better way:

  1. >>> book_info = get_required_textbook(4242)
  2. >>> order_book(*book_info)
  3. Ordering 'Writing Great Code' by Randall Hyde (1593270038)

This is more concise (less tedious to type), and more maintainable. As you get used to the concepts, you’ll find it increasingly natural and easy to use in the code you write.

Variable Keyword Arguments

So far we have just looked at functions with positional arguments - the kind where you declare a function like def foo(a, b):, and then invoke it like foo(7, 2). You know that a=7 and b=2 within the function, because of the order of the arguments. Of course, Python also has keyword arguments:

  1. >>> def get_rental_cars(size, doors=4,
  2. ... transmission='automatic'):
  3. ... template = "Looking for a {}-door {} car with {} transmission...."
  4. ... print(template.format(doors, size, transmission))
  5. ...
  6. >>> get_rental_cars("economy", transmission='manual')
  7. Looking for a 4-door economy car with manual transmission....

And remember, Python lets you call any function just using keyword arguments:

  1. >>> def bar(x, y, z):
  2. ... return x + y * z
  3. ...
  4. >>> bar(z=2, y=3, x=4)
  5. 10

These keyword arguments won’t be captured by the *args idiom. Instead, Python provides a different syntax - using two asterisks instead of one:

  1. def print_kwargs(**kwargs):
  2. for key, value in kwargs.items():
  3. print("{} -> {}".format(key, value))

The variable kwargs is a dictionary. (In contrast to args - remember, that was a tuple.) It’s just a regular dict, so we can iterate through its key-value pairs with .items():

  1. >>> print_kwargs(hero="Homer", antihero="Bart",
  2. ... genius="Lisa")
  3. hero -> Homer
  4. antihero -> Bart
  5. genius -> Lisa

The arguments to print_kwargs are key-value pairs. This is regular Python syntax for calling functions; what’s interesting is happening inside the function. There, a variable called kwargs is defined. It’s a Python dictionary, consisting of the key-value pairs passed in when the function was called.

Here’s another example, which has a regular positional argument, followed by arbitrary key-value pairs:

  1. def set_config_defaults(config, **kwargs):
  2. for key, value in kwargs.items():
  3. # Do not overwrite existing values.
  4. if key not in config:
  5. config[key] = value

This is perfectly valid. You can define a function that takes some normal arguments, followed by zero or more key-value pairs:

  1. >>> config = {"verbosity": 3, "theme": "Blue Steel"}
  2. >>> set_config_defaults(config, bass=11, verbosity=2)
  3. >>> config
  4. {'verbosity': 3, 'theme': 'Blue Steel', 'bass': 11}

Like with *args, naming this variable kwargs is just a strong convention; you can choose a different name if that improves readability.

Keyword Unpacking

Just like with *args, double-star works the other way too. We can take a regular function, and pass it a dictionary using two asterisks:

  1. >>> def normal_function(a, b, c):
  2. ... print("a: {} b: {} c: {}".format(a,b,c))
  3. ...
  4. >>> numbers = {"a": 7, "b": 5, "c": 3}
  5. >>> normal_function(**numbers)
  6. a: 7 b: 5 c: 3

Note the keys of the dictionary must match up with how the function was declared. Otherwise you get an error:

  1. >>> bad_numbers = {"a": 7, "b": 5, "z": 3}
  2. >>> normal_function(**bad_numbers)
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. TypeError: normal_function() got an unexpected keyword argument 'z'

This is called keyword argument unpacking. It works regardless of whether that function has default values for some of its arguments or not. So long as the value of each argument is specified one way or another, you have valid code:

  1. >>> def another_function(x, y, z=2):
  2. ... print("x: {} y: {} z: {}".format(x,y,z))
  3. ...
  4. >>> all_numbers = {"x": 2, "y": 7, "z": 10}
  5. >>> some_numbers = {"x": 2, "y": 7}
  6. >>> missing_numbers = {"x": 2}
  7. >>> another_function(**all_numbers)
  8. x: 2 y: 7 z: 10
  9. >>> another_function(**some_numbers)
  10. x: 2 y: 7 z: 2
  11. >>> another_function(**missing_numbers)
  12. Traceback (most recent call last):
  13. File "<stdin>", line 1, in <module>
  14. TypeError: another_function() missing 1 required positional argument: 'y'

Combining Positional and Keyword Arguments

You can combine the syntax to use both positional and keyword arguments. In a function signature, just separate *args and **kwargs by a comma:

  1. >>> def general_function(*args, **kwargs):
  2. ... for arg in args:
  3. ... print(arg)
  4. ... for key, value in kwargs.items():
  5. ... print("{} -> {}".format(key, value))
  6. ...
  7. >>> general_function("foo", "bar", x=7, y=33)
  8. foo
  9. bar
  10. y -> 33
  11. x -> 7

This usage - declaring a function like def general_function(*args, **kwargs) - is the most general way to define a function in Python. A function so declared can be called in any way, with any valid combination of keyword and non-keyword arguments - including no arguments.

Similarly, you can call a function using both - and both will be unpacked:

  1. >>> def addup(a, b, c=1, d=2, e=3):
  2. ... return a + b + c + d + e
  3. ...
  4. >>> nums = (3, 4)
  5. >>> extras = {"d": 5, "e": 2}
  6. >>> addup(*nums, **extras)
  7. 15

There’s one last point to understand, on argument ordering. When you def the function, you specify the arguments in this order:

  • Named, regular (non-keyword) arguments, then
  • the *args non-keyword variable arguments, then
  • the **kwargs keyword variable arguments, and finally
  • required keyword-only arguments.

You can omit any of these when defining a function. But any that are present must be in this order.

  1. # All these are valid function definitions.
  2. def combined1(a, b, *args): pass
  3. def combined2(x, y, z, **kwargs): pass
  4. def combined3(*args, **kwargs): pass
  5. def combined4(x, *args): pass
  6. def combined5(u, v, w, *args, **kwargs): pass
  7. def combined6(*args, x, y): pass

Violating this order will cause errors:

  1. >>> def bad_combo(**kwargs, *args): pass
  2. File "<stdin>", line 1
  3. def bad_combo(**kwargs, *args): pass
  4. ^
  5. SyntaxError: invalid syntax

Sometimes you might want to define a function that takes 0 or more positional arguments, and 1 or more required keyword arguments. You can define a function like this with *args followed by regular arguments, forming a special category, called keyword-only arguments. If present, whenever that function is called, all must specified as key-value pairs, after the non-keyword arguments:

  1. >>> def read_data_from_files(*paths, format):
  2. ... """Read and merge data from several files,
  3. ... which are in XML, JSON, or YAML format."""
  4. ... # ...
  5. ...
  6. >>> housing_files = ["houses.json", "condos.json"]
  7. >>> housing_data = read_data_from_files(
  8. ... *housing_files, format="json")
  9. >>> commodities_data = read_data_from_files(
  10. "commodities.xml", format="xml")

See how format​'s value is specified with a key-value pair. If you try passing it without format= in front, you get an error:

  1. >>> commodities_data = read_data_from_files(
  2. ... "commodities.xml", "xml")
  3. Traceback (most recent call last):
  4. File "<stdin>", line 2, in <module>
  5. TypeError: read_data_from_files() missing 1 required keyword-only argument: 'format'

Functions As Objects

In Python, functions are ordinary objects - just like an integer, a list, or an instance of a class you create. The implications are profound, letting you do certain very useful things with functions. Leveraging this is one of those secrets separating average Python developers from great ones, because of the extremely powerful abstractions which follow.

Once you get this, it can change the way you write software forever. In fact, these advanced patterns for using functions in Python largely transfer to other languages you will use in the future.

To explain, let’s start by laying out a problematic situation, and how to solve it. Imagine you have a list of strings representing numbers:

  1. nums = ["12", "7", "30", "14", "3"]

Suppose we want to find the biggest integer in this list. The max builtin does not help us:

  1. >>> max(nums)
  2. '7'

This isn’t a bug, of course; since the objects in nums are strings, max compares each element lexicographically.[7] By that criteria, "7" is greater than "30", for the same reason "g" comes after "ca" alphabetically. Essentially, max is evaluating the element by a different criteria than what we want.

Since max​'s algorithm is simple, let’s roll our own that compares based on the integer value of the string:

  1. >>> def max_by_int_value(items):
  2. ... # For simplicity, assume len(items) > 0
  3. ... biggest = items[0]
  4. ... for item in items[1:]:
  5. ... if int(item) > int(biggest):
  6. ... biggest = item
  7. ... return biggest
  8. ...
  9. >>> max_by_int_value(nums)
  10. '30'

This gives us what we want: it returns the element in the original list which is maximal, as evaluated by our criteria. Now imagine working with different data, where you have different criteria. For example, a list of actual integers:

  1. integers = [3, -2, 7, -1, -20]

Suppose we want to find the number with the greatest absolute value - i.e., distance from zero. That would be -20 here, but standard max won’t do that:

  1. >>> max(integers)
  2. 7

Again, let’s roll our own, using the built-in abs function:

  1. >>> def max_by_abs(items):
  2. ... biggest = items[0]
  3. ... for item in items[1:]:
  4. ... if abs(item) > abs(biggest):
  5. ... biggest = item
  6. ... return biggest
  7. ...
  8. >>> max_by_abs(integers)
  9. -20

One more example - a list of dictionary objects:

  1. student_joe = {'gpa': 3.7, 'major': 'physics',
  2. 'name': 'Joe Smith'}
  3. student_jane = {'gpa': 3.8, 'major': 'chemistry',
  4. 'name': 'Jane Jones'}
  5. student_zoe = {'gpa': 3.4, 'major': 'literature',
  6. 'name': 'Zoe Fox'}
  7. students = [student_joe, student_jane, student_zoe]

Now, what if we want the record of the student with the highest GPA? Here’s a suitable max function:

  1. >>> def max_by_gpa(items):
  2. ... biggest = items[0]
  3. ... for item in items[1:]:
  4. ... if item["gpa"] > biggest["gpa"]:
  5. ... biggest = item
  6. ... return biggest
  7. ...
  8. >>> max_by_gpa(students)
  9. {'name': 'Jane Jones', 'gpa': 3.8, 'major': 'chemistry'}

Just one line of code is different between max_by_int_value, max_by_abs, and max_by_gpa: the comparison line. max_by_int_value says if int(item) > int(biggest); max_by_abs says if abs(item) > abs(biggest); and max_by_gpa compares item["gpa"] to biggest["gpa"]. Other than that, these max functions are identical.

I don’t know about you, but having nearly-identical functions like this drives me nuts. The way out is to realize the comparison is based on a value derived from the element - not the value of the element itself. In other words: each cycle through the for loop, the two elements are not themselves compared. What is compared is some derived, calculated value: int(item), or abs(item), or item["gpa"].

It turns out we can abstract out that calculation, using what we’ll call a key function. A key function is a function that takes exactly one argument - an element in the list. It returns the derived value used in the comparison. In fact, int works like a function, even though it’s technically a type, because int("42") returns 42.[8] So types and other callables work, as long as we can invoke it like a one-argument function.

This lets us define a very generic max function:

  1. >>> def max_by_key(items, key):
  2. ... biggest = items[0]
  3. ... for item in items[1:]:
  4. ... if key(item) > key(biggest):
  5. ... biggest = item
  6. ... return biggest
  7. ...
  8. >>> # Old way:
  9. ... max_by_int_value(nums)
  10. '30'
  11. >>> # New way:
  12. ... max_by_key(nums, int)
  13. '30'
  14. >>> # Old way:
  15. ... max_by_abs(integers)
  16. -20
  17. >>> # New way:
  18. ... max_by_key(integers, abs)
  19. -20

Pay attention: you are passing the function object itself - int and abs. You are not invoking the key function in any direct way. In other words, you write int, not int(). This function object is then called as needed by max_by_key, to calculate the derived value:

  1. # key is actually int, abs, etc.
  2. if key(item) > key(biggest):

For sorting the students by GPA, we need a function extracting the "gpa" key from each student dictionary. There is no built-in function that does this, but we can define our own and pass it in:

  1. >>> # Old way:
  2. ... max_by_gpa(students)
  3. {'gpa': 3.8, 'name': 'Jane Jones', 'major': 'chemistry'}
  4. >>> # New way:
  5. ... def get_gpa(who):
  6. ... return who["gpa"]
  7. ...
  8. >>> max_by_key(students, get_gpa)
  9. {'gpa': 3.8, 'name': 'Jane Jones', 'major': 'chemistry'}

Again, notice get_gpa is a function object, and we are passing that function itself to max_by_key. We never invoke get_gpa directly; max_by_key does that automatically.

You may be realizing now just how powerful this can be. In Python, functions are simply objects - just as much as an integer, or a string, or an instance of a class is an object. You can store functions in variables; pass them as arguments to other functions; and even return them from other function and method calls. This all provides new ways for you to encapsulate and control the behavior of your code.

The Python standard library demonstrates some excellent ways to use such functional patterns. Let’s look at a key (ha!) example.

Key Functions in Python

Earlier, we saw the built-in max doesn’t magically do what we want when sorting a list of numbers-as-strings:

  1. >>> nums = ["12", "7", "30", "14", "3"]
  2. >>> max(nums)
  3. '7'

Again, this isn’t a bug - max just compares elements according to the data type, and "7" > "12" evaluates to True. But it turns out max is customizable. You can pass it a key function!

  1. >>> max(nums, key=int)
  2. '30'

The value of key is a function taking one argument - an element in the list - and returning a value for comparison. But max isn’t the only built-in accepting a key function. min and sorted do as well:

  1. >>> # Default behavior...
  2. ... min(nums)
  3. '12'
  4. >>> sorted(nums)
  5. ['12', '14', '3', '30', '7']
  6. >>>
  7. >>> # And with a key function:
  8. ... min(nums, key=int)
  9. '3'
  10. >>> sorted(nums, key=int)
  11. ['3', '7', '12', '14', '30']

Many algorithms can be cleanly expressed using min, max, or sorted, along with an appropriate key function. Sometimes a built-in (like int or abs) will provide what you need, but often you’ll want to create a custom function. Since this is so commonly needed, the operator module provides some helpers. Let’s revisit the example of a list of student records.

  1. >>> student_joe = {'gpa': 3.7, 'major': 'physics',
  2. 'name': 'Joe Smith'}
  3. >>> student_jane = {'gpa': 3.8, 'major': 'chemistry',
  4. 'name': 'Jane Jones'}
  5. >>> student_zoe = {'gpa': 3.4, 'major': 'literature',
  6. 'name': 'Zoe Fox'}
  7. >>> students = [student_joe, student_jane, student_zoe]
  8. >>>
  9. >>> def get_gpa(who):
  10. ... return who["gpa"]
  11. ...
  12. >>> sorted(students, key=get_gpa)
  13. [{'gpa': 3.4, 'major': 'literature', 'name': 'Zoe Fox'},
  14. {'gpa': 3.7, 'major': 'physics', 'name': 'Joe Smith'},
  15. {'gpa': 3.8, 'major': 'chemistry', 'name': 'Jane Jones'}]

This is effective, and a fine way to solve the problem. Alternatively, the operator module’s itemgetter creates and returns a key function that looks up a named dictionary field:

  1. >>> from operator import itemgetter
  2. >>>
  3. >>> # Sort by GPA...
  4. ... sorted(students, key=itemgetter("gpa"))
  5. [{'gpa': 3.4, 'major': 'literature', 'name': 'Zoe Fox'},
  6. {'gpa': 3.7, 'major': 'physics', 'name': 'Joe Smith'},
  7. {'gpa': 3.8, 'major': 'chemistry', 'name': 'Jane Jones'}]
  8. >>>
  9. >>> # Now sort by major:
  10. ... sorted(students, key=itemgetter("major"))
  11. [{'gpa': 3.8, 'major': 'chemistry', 'name': 'Jane Jones'},
  12. {'gpa': 3.4, 'major': 'literature', 'name': 'Zoe Fox'},
  13. {'gpa': 3.7, 'major': 'physics', 'name': 'Joe Smith'}]

Notice itemgetter is a function that creates and returns a function - itself a good example of how to work with function objects. In other words, the following two key functions are completely equivalent:

  1. # What we did above:
  2. def get_gpa(who):
  3. return who["gpa"]
  4. # Using itemgetter instead:
  5. from operator import itemgetter
  6. get_gpa = itemgetter("gpa")

This is how you use itemgetter when the sequence elements are dictionaries. It also works when the elements are tuples or lists - just pass a number index instead:

  1. >>> # Same data, but as a list of tuples.
  2. ... student_rows = [
  3. ... ("Joe Smith", "physics", 3.7),
  4. ... ("Jane Jones", "chemistry", 3.8),
  5. ... ("Zoe Fox", "literature", 3.4),
  6. ... ]
  7. >>>
  8. >>> # GPA is the 3rd item in the tuple, i.e. index 2.
  9. ... # Highest GPA:
  10. ... max(student_rows, key=itemgetter(2))
  11. ('Jane Jones', 'chemistry', 3.8)
  12. >>>
  13. >>> # Sort by major:
  14. ... sorted(student_rows, key=itemgetter(1))
  15. [('Jane Jones', 'chemistry', 3.8),
  16. ('Zoe Fox', 'literature', 3.4),
  17. ('Joe Smith', 'physics', 3.7)]

operator also provides attrgetter, for keying off an attribute of the element, and methodcaller for keying off a method’s return value - useful when the sequence elements are instances of your own class:

  1. >>> class Student:
  2. ... def __init__(self, name, major, gpa):
  3. ... self.name = name
  4. ... self.major = major
  5. ... self.gpa = gpa
  6. ... def __repr__(self):
  7. ... return "{}: {}".format(self.name, self.gpa)
  8. ...
  9. >>> student_objs = [
  10. ... Student("Joe Smith", "physics", 3.7),
  11. ... Student("Jane Jones", "chemistry", 3.8),
  12. ... Student("Zoe Fox", "literature", 3.4),
  13. ... ]
  14. >>> from operator import attrgetter
  15. >>> sorted(student_objs, key=attrgetter("gpa"))
  16. [Zoe Fox: 3.4, Joe Smith: 3.7, Jane Jones: 3.8]


[6] This seems to be deeply ingrained; once I abbreviated it *a, only to have my code reviewer demand I change it to *args. They wouldn’t approve it until I changed it, so I did.

[7] Meaning, alphabetically, but generalizing beyond the letters of the alphabet.

[8] Python uses the word callable to describe something that can be invoked like a function. This can be an actual function, a type or class name, or an object defining the __call__ magic method. Key functions are frequently actual functions, but can be any callable.


Next Chapter: Decorators

Previous Chapter: Creating Collections with Comprehensions