Python functions and lambdas

Writing your own functions in Python is quite straightforward, and in fact we’ve already seen this a few times. In this section we’ll make the syntax for this explicit, explore some very special arguments, and wrap up by discussing lambda functions.

Defining your own functions

Functions are defined in Python using the following syntax:

def myfun(in1,in2):
    # Function body
    return someval

There can be any number of arguments into the function, including zero. The return statement is optional as well, and can be omitted if that makes sense for what you are writing.

Returning multiple values from a function can easily be accomplished by using a tuple. Consider this example function that calculates the area and circumference of a circle with a given radius:

def circum_info(rad):
    circum = 2*np.pi*rad
    area = np.pi*rad**2
    return circum,area

We could then call this function in a couple of ways:

>>> ca = circum_info(3)
>>> ca[0]
18.84955592153876
>>> ca[1]
28.274333882308138
>>> c,a = circum_area(3)
>>> c
18.84955592153876
>>> a
28.274333882308138

We should make some observations about our function and how we called it:

  • The input argument rad is simply assumed to be a number

  • There is really only one return, a tuple containing circum and area

  • We can capture this return into a single tuple, like ca in the above

  • Alternatively, we can immediately unpack the tuple, like c,a = ... in the above - This is essentially the same as having multiple returns - This is also the most readable

Special arguments to functions: *args and **kwargs

Functions can take in any number of arguments, and in fact can take in a variable number of arguments. Additional arguments that don’t explicitly appear as part of the function signature are lumped into two special types of arguments. We can adapt our above presentation of function syntax to include these like so:

def myfun(in1,in2,*args,**kwargs):
    # function body
    return someval

As before, this function takes in two arguments and returns a value. It additionally can take in an a-priori unknown number of arguments through *args and **kwargs .

Note

Technically these don’t need to be called *args and **kwargs. Rather, these names are convention and make it a bit easier for people approaching your code to figure out what is going on.

However, you can only have one of each of these, and *args (or whatever you choose to call it) must go first. You can also choose to use only one of the two.

The *args argument

The *args argument must come after all required arguments, and before **kwargs if it is used. The interpreter will create args as a tuple containing all arguments after the initial set of required ones, and since tuples can be any length this can hold any number of additional arguments. This is best seen through an example. Consider the following function:

def myfun(in1,in2,*args):
    print('in1: ',in1,type(in1))
    print('in2: ',in2,type(in2))
    print('Called with extra args: ',len(args))
    print('args: ',args,type(args))

Try calling this function in a few different ways:

>>> myfun(2,'a string')
>>> myfun(2,'a string',3.4,5,['a','list'])
>>> myfun(2,'a string',['a','list'],('a','tuple'))

What does each call write to the terminal and why?

The **kwargs argument

We’ve seen that using *args allows you to capture any number of additional arguments beyond those that the function explicitly requires. These arguments are called positional arguments because their order is preserved when the tuple args is created.

We can also capture additional arguments by name. These are called key word arguments, and can come in any order. Again, this is best shown with an example. Consider the following modification to the previous function:

def myfun(in1,in2,**kwargs):
    print('in1: ',in1,type(in1))
    print('in2: ',in2,type(in2))
    print('Called with extra args: ',len(kwargs))
    print('kwargs: ',kwargs,type(kwargs))

Try calling this function in a few different ways:

>>> myfun(2,'a string')
>>> myfun(2,'a string',my_list=['a','list'],my_tuple=('a','tuple'))
>>> myfun(2,'a string',my_tuple=('a','tuple'),my_list=['a','list'])

We can make some useful observations from this:

  • kwargs is a dictionary where each argument name is used as a key - The keys are always strings, as set by the interpreter - The values are whatever each name is set to

  • Like with *args we can capture as many additional arguments as we want, including none

Question: What happens if you repeat a key?

Exercise: Adapt myfun one more time to use both *args and **kwargs. What can you observe from calling it in a few different ways?

Lambda functions

Occasionally you only need a simple function for a one-off task. Rather than defining a whole function just for this case you can instead use a lambda function. These can consist only of a single expression that transforms a group of inputs to a desired output. The general syntax is:

lambda <arguments>: <single expression>

For example, we could write a function that doubles its input. In full form this would be:

def f(x):
    return 2*x

and the corresponding lambda function is:

lambda x: 2*x

Lambda functions are anonymous functions. We can still assign them names by capturing them. For instance we could write:

>>> f = lambda x: 2*x
>>> type(f)
function
>>> f(3)
6
>>> f('a string')
'a stringa string'

Lambda functions can do more inside the body, so long as they remain a single expression:

>>> g = lambda : [x**2 for x in range(10)]
>>> g()
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Lambda functions are particularly useful for providing inputs to functions that themselves act on functions. Consider a function func which takes two other functions as arguments:

>>> def func(fInner,fOuter,x,y):
        return fOuter(fInner(x),fInner(y))
>>> h = lambda x,y: 2*x + y
>>> func(f,h,3,4)
20
>>> func(lambda x: x**2,h,3,4)
34