.. _ch03-python-functions-lambdas: ============================================================= Python functions and lambdas ============================================================= * Link to `incomplete Jupyter Notebook for this section of the notes (for you to fill out while following along with lecture) `_ * Link to `completed Jupyter Notebook for this section of the notes `_ 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: .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python >>> 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: .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python >>> 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: .. code-block:: python 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: .. code-block:: python >>> 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: .. code-block:: python lambda : For example, we could write a function that doubles its input. In full form this would be: .. code-block:: python def f(x): return 2*x and the corresponding lambda function is: .. code-block:: python lambda x: 2*x Lambda functions are *anonymous functions*. We can still assign them names by capturing them. For instance we could write: .. code-block:: python >>> 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: .. code-block:: python >>> 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: .. code-block:: python >>> 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