7. Functions
Writing functions using def
is a cornerstone of all programs.
In this tutorial we will look into some of the advance usage of functions
viz.
- closures
- callback-functions
- control-flow
- keyword-only arguments
- default-arguments
- annotations
- any-number-of-arguments (
*args
&**kwargs
) etc.
7.1: Function with any number of arguments (*args, **kwargs
)
Problem
You want to write a function that accepts any number of arguments.
Solution
For a function that accepts any number of positional arguments use a
*
argument.
For a function that accepts any number of keyword arguments use a
**
argument.
To write any function that accepts any numbe rof positional arguments, use a *
argument as shown below.
In this example, rest
is a tuple
of all extra positional arguments passed.
In above code, it is treated as a sequence
in further calculations inside the function.
1 2 3 4 5 6 7 |
|
To accept any number of keyword arguments use a **
argument as shown below.
In the below code, attrs
is a dictionary that holds the passed keyword arguments (if any)
1 2 3 4 5 6 7 |
|
1 2 |
|
1 2 |
|
If you want a function that accepts both any-number-of-positional-arguments aswell as
any-number-of-keyword-arguments use both
*
& **
arguments as shown below
1 2 3 |
|
Discussion
A
*
argument can only appear as the last positional argument in a function
and a **
argumnet can only appear as the last argumnet in a function.
A subtle aspect of function definition is that arguments can still appear
after a
*
argument by they are treated as keyword argument only (as discussed in further lessons).
1 2 3 4 5 6 7 8 9 |
|
7.2: Keyword-only arguments
Problem
You want to write a function that accepts only keyword arguments.
Solution
This feature is easy to implement if you place the keyword argument after the *
argument
or a single unnamed *
as shown below.
1 2 3 4 5 6 7 8 |
|
This technique can also be used to specify keyword arguments for functions that accept varying numbe rof positional arguments as shown below.
1 2 3 4 5 6 7 8 9 10 |
|
Discussion
Keyword-only arguments are a good way to enforce greater code-clarity when
specifying optional function arguments. For example consider the following code:
1 |
|
recv
(written in above section), they might not know
what does False
mean in this scenario. On the other hand, its much clearer to the user
if someone writes the call as below:
1 |
|
The use of keyword-only arguments is often very useful for tricks involving
**kwargs
,
since they show up nicely when the user asks for help()
1 2 3 4 |
|
Keyword-only arguments also have utility in advance usage like argument injection
using
*args
& **kwargs
.
7.3: Function argument annotations
Problem
You've written a function but you want add some additional information about the arguments so that users can know more easily how a function is supposed to be used.
Solution
Function argument annotations can be very useful in givin gprogrammers hint about how a function is supposed to be used. Cosider the following annotated function.
1 2 3 4 5 6 7 |
|
The Python Interpreter does not add any semantic meaning to the attached annotations.
Neither are they type-cheked nor does Puython behave any differently than before.
However, they might give useful hunts about types that would be useful for users or third-party libraries.
Any type of object can be uased as an annotation (eg. numbers, string, instances, types, etc.).
Discussion
Function annotations are merely stored in function's __annotations__
attribute.
1 2 3 4 5 |
|
Although, annotations have many potential use-cases, their primary utility is probably just documentation. Because Python does not have type decalrations, it can be difficult to read a source code without much documentation and hind about the types of objects; function annotations are one way to resolve this problem.
More advance usage of function annotations is to implement multiple dispatch (i.e. overloaded functions)
7.7: Capturing variables in Anonymous functions
Problem
You have defined an anonymous function using lambda
and but you also need to capture the
value of certain variable at the tim eof definition.
Solution
Consider the behaviour of the following code a python console:
1 2 3 4 5 |
|
Now ask yourself a question. What is the value of a(10)
and b(10)
now?
If you think that the values will be 20
and 30
; you would be wrong.
The python console gives the follwoing results.
1 2 3 4 5 |
|
The problem here is that
x
used here (in the lambda
expression) is a free variable;
that gets bound at runtime, not at definition time. The value x
in the lambda
expression is
whatever the value of x
variable happens to be at the time of execution of the statement a(10), b(10)
.
For example see below code:
1 2 3 4 5 6 7 |
|
If you want an anonymous
lambda
function to capture a value at the point of definition
include that value as a default value in the lambda
expression definition as follows.
1 2 3 4 5 6 7 8 9 10 11 |
|
Discussion
The problem addressed here comes up very often in code that tries to be a just a bit more clever.
For example, creating a list of lambda
expressions using a list comprehension
or a loop of some kind
and expecting the lambda
expressions to remember the iteration variable at the time of defintion.
For example:
1 2 3 4 5 6 7 8 9 10 |
|
Notice, how all lambda
expressions take the same final value of n
from the range(5)
expression.
But, if we specifically captuer the value of
n
using default arguments,
then we get the behaviour we want
1 2 3 4 5 6 7 8 9 10 |
|
7.8: functools.partial()
Making an N-argumnet callable work asa callable with fewwer argumnets
You have a callable that you want to make it work with other Python code. possbly as a callabel or handler, but it takes too many argumnets and causes an exception when called.
Solution
If you need to reduce the number of arguments to a function, you should use functools.partial()
The partial()
function allows you to assign fixed values to one or more of the arguments
thus reduce the number of required argumnets in subsequent calls of the reduced function. as shown below.
1 2 |
|
functools.partial()
to fix certain arguments.
1 2 3 4 5 |
|
1 2 3 4 5 |
|
1 2 3 4 |
|
Discussion
...
7.9: Replacing single method classes with functions
Problem
You have a class that only defines a single method except __init__()
method.
However, to simplify your code, you would much rather have a simple function.
Solution
In many cases, single method classes can be turned into functions using
closures
.
Consider the following example, which allws users to fetch URLs
using a kind of template-scheme.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
This URLTemplate
class be very easily replaced with a much simpler function using closures.
as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
7.10: Carrying extra state with callback functions
Problem
You are writing code that relies on the use of callback functions (e.g. event handlers, completion callback, etc.); but you want to have the callback function carry extra state to be used inside the callback function itself.
Solution
Let's first talk about callback functions which are used in asynchronous processing via the following example:
1 2 3 4 5 6 |
|
In reality, such code might do all sorts of advanced processing involving threads, processes, & timers. But here we are just focusing on the invocation of the callback. Below, we show how to use the above code:
1 2 3 4 5 |
|
1 2 3 4 |
|
As you can notice above, the callbakc function print_result()
only accepts a single argument
i.e result
. No other information is provided.
This lack of information can be an issue when we want our callback function to interact with other contextual variables
or other parts of the code.
Below we provide some probable solutions to handle this issue:
Some ways to carry extra information with callbacks:
- Use bound methods instead of regular sinple functions.
- Use closures
- Use coroutines.
- Using
functools.partial()
Using BOUND METHODS: One way to carry extra information in a callback is to use bound methods
instead of simple function. For example, this below class keeps an internal sequence number
that is incremented everytime a result is received.
1 2 3 4 5 6 |
|
ResultHandler
, we need to create its instance and use its
handler()
method as a callback.
1 2 3 4 5 |
|
ResultHandler
class rh
uses its internal state variable
self.sequence
across two different calls of the callback function rh.handler
.
Using CLOSURES: As an alternative we can also use closures to embed additional information
in the callback function.
1 2 3 4 5 6 7 |
|
1 2 3 4 5 |
|
Using COROUTINES: Sometimes we can also use a coroutine to accomplish what we achieved
using a bound method or a closure.
1 2 3 4 5 6 7 8 |
|
1 2 3 4 5 |
|
NOTICE the use of
send
to push value to the yield
in the callback coroutine
Using
functools.partial()
: As an alternative, we can also carry state into a callback
using an extra argument and a partial function application.
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 |
|
Because,
lambda
expressions can be very useful in creating partial functions.
We can use lambda
expressions (with added contextual argumnets) as callbacks as shown below.
1 2 3 |
|
Discussion
Software based on callback functions often runs the risk of turning into a huge
tangled mess.
Part of the reason is that callback function is often disconnected from the code that made the initial request leading up the callback execution". Thus the execution environment between making the request and handling the result is effectively lost.
If you want the callback function to continue with a procedure involving multiple steps, you have to figure out how to save and restore the associated states.
There are really two main promising ways to capture and restore context states:
- You can carry it around in a class instance. Attach it to bound method perhaps.
- Or you can carry it around in a closure (an inner function).
Of the above two techniques, probably the closures approach is more natural and lightweight in that they are built from functions. They also automatically captures all the variables used.
But, if using closures, we need to pay careful attention to mutable variables.
In the above solution we use
nonlocal
declaration for variable sequence
to specify that
we were using the variable from withing the closure; otherwise we will get an ERROR
.
The use of coroutines
is interesting and very closely resembles the usage of closures.
In some sense, it is even cleaner as we don't have to worry about the
nonlocal
declarations.
Only potential downsides of using coroutines is that its somewhat difficult to understand
conceptually and we have to remembr some nitty-gritty details of its usage
(for example, making sure to call
next()
before using the couroutine as a callback etc...)
Nevertheless, coroutines are very useful in defining inlined callbacks.