Python for Programmers: Wrapping Functions
Welcome to the Wrapping Functions lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've seen that Python functions are first-class values: we can assign them to variables and we can pass them as arguments. Functions can also return other functions.
In the next example,
define_multiplierdefines and returns a function that multiplies its argument by some number. For example,define_multiplier(1.05)returns a function that multiplies any number by 1.05.>
def define_multiplier(x):def multiplier(y):return x * yreturn multiplierIf we assign the returned function to a name, we can call it just like any other function.
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add_5_percent = define_multiplier(1.05)add_5_percent(100)Result:
105.0
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add_20_percent = define_multiplier(1.2)(add_20_percent(100), add_20_percent(200))Result:
(120.0, 240.0)
We can also call the returned function immediately, without assigning it to a variable.
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
define_multiplier(2.5)(200)Result:
500.0
The
define_multiplierfunction took a number and returned a function. But we can also write functions that take functions as arguments, then define and return new functions. This is called "wrapping a function".Wrapping functions is useful because the wrapper can add new behavior. For example, we can wrap a function to cache its return values, or to count how many times it's called, or to validate its argument types.
Let's say we have an
add1function that expects anintargument.>
def add1(x):return x + 1- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add1(2)Result:
3
Python doesn't let us add strings to numbers, so passing a string to
add1is an error.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add1("2")Result:
TypeError: can only concatenate str (not "int") to str
Now we want
add1to work with strings, soadd("2")returns 3. We could modifyadd1to handle strings. But maybe we have dozens of functions that need that behavior: they all need to automatically convert string arguments into integers.A better solution is to write a wrapper function
convert_arg_to_int. It takes any functionfunc, then wraps it in a new functionwrapped. The new function converts its argument to anint, then calls the original function with thatint.>
def convert_arg_to_int(func):def wrapped(x):x_as_int = int(x)return func(x_as_int)return wrappedNow we can wrap
add1withconvert_arg_to_int. The next example assigns the returned function toadd1, replacing the original function with the wrapped version. This is a bit awkward, but we'll see a way to improve it later.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
def add1(x):return x + 1add1 = convert_arg_to_int(add1) Now the
add1function works with any argument that it can convert into an integer, like"2".- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add1(2)Result:
3
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add1("2")Result:
3
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add1("3")Result:
4
The
int(x)call will still error if the string doesn't contain an integer.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add1("not an integer")Result:
ValueError: invalid literal for int() with base 10: 'not an integer'
We can use
convert_arg_to_intto wrap any function. This can save a lot of code when we need to add the same functionality to many functions. For example, we can wrapdoubleinstead ofadd1.>
def convert_arg_to_int(func):def wrapped(x):x_as_int = int(x)return func(x_as_int)return wrappeddef double(x):return x * 2double = convert_arg_to_int(double)- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
double(2)Result:
4
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
double("3000")Result:
6000
This approach lets the individual functions like
add1anddoubleremain simple. They only need to handle integers; they don't need to worry about converting strings into integers.Here's a code problem:
Define a
convert_arg_to_strwrapper function. It takes a function,func, as an argument. That function only works on string arguments.convert_arg_to_strreturns a new function that takes any argument, callsstron it to convert it into a string, then passes that string tofunc.def convert_arg_to_str(func):def wrapped(value):return func(str(value))return wrappeddef add_s(string):return string + "s"add_s = convert_arg_to_str(add_s)assert add_s(4) == "4s"def add_quotes(string):return f'"{string}"'add_quotes = convert_arg_to_str(add_quotes)assert add_quotes(11.2) == '"11.2"'- Goal:
No errors.
- Yours:
No errors.
We still haven't addressed the awkward
add1 = convert_arg_to_int(add1)line. A different lesson will use Python's decorator syntax to remove that line entirely.