Forcing keyword-only arguments with the * separator
There are some situations where we have a large number of positional parameters for a function. Perhaps we've followed the Designing functions with optional parameters recipe and that led us to designing a function with so many parameters that it gets confusing.
Pragmatically, a function with more than about three parameters can be confusing. A great deal of conventional mathematics seems to focus on one and two-parameter functions. There don't seem to be too many common mathematical operators that involve three or more operands.
When it gets difficult to remember the required order for the parameters, there are too many parameters.
Getting ready
We'll look at a function that has a large number of parameters. We'll use a function that prepares a wind-chill table and writes the data to a CSV format output file.
We need to provide a range of temperatures, a range of wind speeds, and information on the file we'd like to create. This is a lot of parameters.
A formula for the apparent temperature, the wind chill, is this:
Twc(Ta, V) = 13.12 + 0.6215Ta - 11.37V0.16 + 0.3965TaV0.16
The wind chill temperature, Twc, is based on the air temperature, Ta, in degrees, C, and the wind speed, V, in KPH.
For Americans, this requires some conversions:
- Convert from F into C: C = 5(F-32) / 9
- Convert windspeed from MPH, Vm , into KPH, Vk: Vk = Vm × 1.609344
- The result needs to be converted from C back into F: F = 32 + C(9/5)
We won't fold these conversions into this solution. We'll leave that as an exercise for you.
The function to compute the wind-chill temperature, Twc(), is based on the definition provided previously. It looks like this:
def Twc(T: float, V: float) -> float:
return 13.12 + 0.6215*T - 11.37*V**0.16 + 0.3965*T*V**0.16
One approach to creating a wind-chill table is to create something like this:
import csv
def wind_chill(
start_T: int, stop_T: int, step_T: int,
start_V: int, stop_V: int, step_V: int,
target: TextIO
) -> None:
"""Wind Chill Table."""
writer= csv.writer(target)
heading = ['']+[str(t) for t in range(start_T, stop_T, step_T)]
writer.writerow(heading)
for V in range(start_V, stop_V, step_V):
row = [float(V)] + [
Twc(T, V) for T in range(start_T, stop_T, step_T)
]
writer.writerow(row)
Before we get to the design problem, let's look at the essential processing. We've opened an output file using the with context. This follows the Managing a context using the with statement recipe in Chapter 2, Statements and Syntax. Within this context, we've created a write for the CSV output file. We'll look at this in more depth in Chapter 10, Input/Output, Physical Format, and Logical Layout.
We've used an expression, ['']+[str(t) for t in range(start_T, stop_T, step_T)], to create a heading row. This expression includes a list literal and a generator expression that builds a list. We'll look at lists in Chapter 4, Built-In Data Structures Part 1: Lists and Sets. We'll look at the generator expression online in Chapter 9, Functional Programming Features (link provided in the Preface).
Similarly, each cell of the table is built by a generator expression, [Twc(T, V)for T in range(start_T, stop_T, step_T)]. This is a comprehension that builds a list object. The list consists of values computed by the wind-chill function, Twc(). We provide the wind velocity based on the row in the table. We provide a temperature based on the column in the table.
The def wind_chill line presents a problem: the function has seven distinct positional parameters. When we try to use this function, we wind up with code like the following:
>>> p = Path('data/wc1.csv')
>>> with p.open('w', newline='') as target:
... wind_chill(0, -45, -5, 0, 20, 2, target)
What are all those numbers? Is there something we can do to help explain the purposes behind all those numbers?
How to do it...
When we have a large number of parameters, it helps to use keyword arguments instead of positional arguments.
In Python 3, we have a technique that mandates the use of keyword arguments. We can use the * as a separator between two groups of parameters:
- Before *, we list the argument values that can be either positional or named by keyword. In this example, we don't have any of these parameters.
- After *, we list the argument values that must be given with a keyword. For our example, this is all of the parameters.
For our example, the resulting function definition has the following stub definition:
def wind_chill(
*,
start_T: int, stop_T: int, step_T: int,
start_V: int, stop_V: int, step_V: int,
path: Path
) -> None:
Let's see how it works in practice with different kinds of parameters.
- When we try to use the confusing positional parameters, we'll see this:
>>> wind_chill(0, -45, -5, 0, 20, 2, target) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: wind_chill() takes 0 positional arguments but 7 were given
- We must use the function with explicit parameter names, as follows:
>>> p = Path('data/wc1.csv') >>> with p.open('w', newline='') as output_file: ... wind_chill(start_T=0, stop_T=-45, step_T=-5, ... start_V=0, stop_V=20, step_V=2, ... target=output_file)
This use of mandatory keyword parameters forces us to write a clear statement each time we use this complex function.
How it works...
The * character has a number of distinct meanings in the definition of a function:
- * is used as a prefix for a special parameter that receives all the unmatched positional arguments. We often use *args to collect all of the positional arguments into a single parameter named args.
- ** is used a prefix for a special parameter that receives all the unmatched named arguments. We often use **kwargs to collect the named values into a parameter named kwargs.
- *, when used by itself as a separator between parameters, separates those parameters. It can be applied positionally or by keyword. The remaining parameters can only be provided by keyword.
The print() function exemplifies this. It has three keyword-only parameters for the output file, the field separator string, and the line end string.
There's more...
We can, of course, combine this technique with default values for the various parameters. We might, for example, make a change to this, thus introducing a single default value:
import sys
def wind_chill(
*,
start_T: int, stop_T: int, step_T: int,
start_V: int, stop_V: int, step_V: int,
target: TextIO=sys.stdout
) -> None:
We can now use this function in two ways:
- Here's a way to print the table on the console, using the default target:
wind_chill( start_T=0, stop_T=-45, step_T=-5, start_V=0, stop_V=20, step_V=2)
- Here's a way to write to a file using an explicit target:
path = pathlib.Path("code/wc.csv") with path.open('w', newline='') as output_file: wind_chill(target=output_file, start_T=0, stop_T=-45, step_T=-5, start_V=0, stop_V=20, step_V=2)
We can be more confident in these changes because the parameters must be provided by name. We don't have to check carefully to be sure about the order of the parameters.
As a general pattern, we suggest doing this when there are more than three parameters for a function. It's easy to remember one or two. Most mathematical operators are unary or binary. While a third parameter may still be easy to remember, the fourth (and subsequent) parameter will become very difficult to recall, and forcing the named parameter evaluation of the function seems to be a helpful policy.
See also
- See the Picking an order for parameters based on partial functions recipe for another application of this technique.