Introduction¶A function is a sequence of statements which performs some kind of task. We use functions to eliminate code duplication – instead of writing all the statements at every place in our code where we want to perform the same task, we define them in one place and refer to them by the function name. If we want to change how that task is performed, we will now mostly only need to change code in one place. Show
Here is a definition of a simple function which takes no parameters and doesn’t return any values: def print_a_message(): print("Hello, world!") We use the Functions do things, so you should always choose a function name which explains as simply as accurately as possible what the function does. This will usually be a verb or some phrase containing a verb. If you change a function so much that the name no longer accurately reflects what it does, you should consider updating the name – although this may sometimes be inconvenient. This particular function always does exactly the same thing: it prints the message Defining a function does not make it run – when the flow of control reaches the function definition and executes it, Python just learns about the function and what it will do when we run it. To run a function, we have to call it. To call the function we use its name followed by round brackets (with any parameters that the function takes in between them): Of course we have already used many of Python’s built-in functions, such as print("Hello") len([1, 2, 3]) Many objects in Python are callable, which means that you can call them like functions – a callable object has a special method defined which is executed when the object is called. For example, types such as num_str = str(3) num = int("3") people = list() # make a new (empty) list people = list((1, 2, 3)) # convert a tuple to a new list In general, classes (of which types are a subset) are callable – when we call a class we call its constructor method, which is used to create a new object of that class. We will learn more about classes in the next chapter, but you may recall that we already called some classes to make new objects when we raised exceptions: raise ValueError("There's something wrong with your number!") Because functions are objects in Python, we can treat them just like any other object – we can assign a function as the value of a variable. To refer to a function without calling it, we just use the function name without round brackets: my_function = print_a_message # later we can call the function using the variable name my_function() Because defining a function does not cause it to execute, we can use an identifier inside a function even if it hasn’t been defined yet – as long as it becomes defined by the time we run the function. For example, if we define several functions which all call each other, the order in which we define them doesn’t matter as long as they are all defined before we start using them: def my_function(): my_other_function() def my_other_function(): print("Hello!") # this is fine, because my_other_function is now defined my_function() If we were to move that function call up, we would get an error: def my_function(): my_other_function() # this is not fine, because my_other_function is not defined yet! my_function() def my_other_function(): print("Hello!") Because of this, it’s a good idea to put all function definitions near the top of your program, so that they are executed before any of your other statements. Exercise 1¶
Input parameters¶It is very seldom the case that the task that we want to perform with a function is always exactly the same. There are usually minor differences to what we need to do under different circumstances. We don’t want to write a slightly different function for each of these slightly different cases – that would defeat the object of the exercise! Instead, we want to pass information into the function and use it inside the function to tailor the function’s behaviour to our exact needs. We express this information as a series of input parameters. For example, we can make the function we defined above more useful if we make the message customisable: def print_a_message(message): print(message) More usefully, we can pass in two numbers and add them together: def print_sum(a, b): print(a + b)
print_sum() # this won't work print_sum(2, 3) # this is correct In the example above, we are passing In languages which are statically typed, we have to declare the types of parameters when we define the function, and we can only use variables of those types when we call the function. If we want to perform a similar task with variables of different types, we must define a separate function which accepts those types. In Python, parameters have no declared types. We can pass any kind of variable to the The advantage of this is that we don’t have to write a lot of different This is why it is important for us to test our code thoroughly – something we will look at in a later chapter. If we intend to write code which is robust, especially if it is also going to be used by other people, it is also often a good idea to check function parameters early in the function and give the user feedback (by raising exceptions) if the are incorrect. Exercise 2¶
Return values¶The function examples we have seen above don’t return any values – they just result in a message
being printed. We often want to use a function to calculate some kind of value and then return it to us, so that we can store it in a variable and use it later. Output which is returned from a function is called a return value. We can rewrite the def add(a, b): return a + b We use the Here the return value of the function will be assigned to A function can only have a single return value, but that value can be a list or tuple, so in practice you can return as many different values from a function as you like. It usually only makes sense to return multiple values if they are tied to each other in some way. If you place several values after the def divide(dividend, divisor): quotient = dividend // divisor remainder = dividend % divisor return quotient, remainder # you can do this q, r = divide(35, 4) # but you can also do this result = divide(67, 9) q1 = result[0] q2 = result[1] # by the way, you can also do this a, b = (1, 2) # or this c, d = [5, 6] What happens if you try to assign one of our first examples, which don’t have a return value, to a variable? mystery_output = print_message("Boo!") print(mystery_output) All functions do actually return something, even if we don’t define a return value – the default return value is
When a def divide(dividend, divisor): if not divisor: return None, None # instead of dividing by zero quotient = dividend // divisor remainder = dividend % divisor return quotient, remainder If the This technique can be useful whenever we want to check parameters at the beginning of a function – it means that we don’t have to indent the main part of the function inside an def divide(dividend, divisor): if not divisor: raise ValueError("The divisor cannot be zero!") quotient = dividend // divisor remainder = dividend % divisor return quotient, remainder Having multiple exit points scattered throughout your function can make your code difficult to read – most people expect a single Note in some other languages, only functions that return a value are called functions (because of their similarity to mathematical functions). Functions which have no return value are known as procedures instead. Exercise 3¶
The stack¶Python stores information about functions which have been called in a call stack. Whenever a function is called, a new stack frame is added to the stack – all of the function’s parameters are added to it, and as the body of the function is executed, local variables will be created there. When the function finishes executing, its stack frame is discarded, and the flow of control returns to wherever you were before you called the function, at the previous level of the stack. If you recall the section about variable scope from the beginning of the course, this explains a little more about the way that variable names are resolved. When you use an identifier, Python will first look for it on the current level of the stack, and if it doesn’t find it it will check the previous level, and so on – until either the variable is found or it isn’t found anywhere and you get an error. This is why a local variable will always take precedence over a global variable with the same name. Python also searches the stack whenever it handles an exception: first it checks if the exception can be handled in the current function, and if it cannot, it terminates the function and tries the next one down – until either the exception is handled on some level or the program itself has to terminate. The traceback you see when an exception is printed shows the path that Python took through the stack. Recursion¶We can make a function call itself. This is known as recursion.
A common example is a function which calculates numbers in the Fibonacci sequence: the zeroth number is def fibonacci(n): if n == 0: return 0 if n == 1: return 1 return fibonacci(n - 1) + fibonacci(n - 2) Whenever we write a recursive function, we need to include some kind of condition which will allow it to stop recursing – an end case in which the function doesn’t call itself. In this example, that happens at the beginning of the sequence: the first two numbers are not calculated from any previous numbers – they are constants. What would happen if we omitted that condition from our function? When we got to n = 2, we would keep calling the function, trying to calculate Writing fail-safe recursive functions is difficult. What if we called the function above with a parameter of Any recursive function can be re-written in an iterative way which avoids recursion. For example: def fibonacci(n): current, next = 0, 1 for i in range(n): current, next = next, current + next return current This function uses iteration to count up to the desired value of n, updating variables to keep track of the calculation. All the iteration happens within a single instance of the function. Note that we assign new values to both variables at the same time, so that we can use both old values to calculate both new values on the right-hand side. Exercise 4¶
Default parameters¶The combination of the function name and the number of parameters that it takes is called the function signature. In statically typed languages, there can be multiple functions with the same name in the same scope as long as they have different numbers or types of parameters (in these languages, parameter types and return types are also part of the signature). In Python, there can only be one function with a particular name defined in the scope – if you define another function with the same name, you will overwrite the first function. You must call this function with the correct number of parameters, otherwise you will get an error. Sometimes there is a good reason to want to have two versions of the same function with different sets of parameters. You can achieve something similar to this by making some parameters optional. To make a parameter optional, we need to supply a default value for it. Optional parameters must come after all the required parameters in the function definition: def make_greeting(title, name, surname, formal=True): if formal: return "Hello, %s %s!" % (title, surname) return "Hello, %s!" % name print(make_greeting("Mr", "John", "Smith")) print(make_greeting("Mr", "John", "Smith", False)) When we call the function, we can leave the optional parameter out – if we do, the default value will be used. If we include the parameter, our value will override the default value. We can define multiple optional parameters: def make_greeting(title, name, surname, formal=True, time=None): if formal: fullname = "%s %s" % (title, surname) else: fullname = name if time is None: greeting = "Hello" else: greeting = "Good %s" % time return "%s, %s!" % (greeting, fullname) print(make_greeting("Mr", "John", "Smith")) print(make_greeting("Mr", "John", "Smith", False)) print(make_greeting("Mr", "John", "Smith", False, "evening")) What if we want to pass in the second optional parameter, but not the first? So far we have been passing positional parameters to all these functions – a tuple of values which are matched up with parameters in the function signature based on their positions. We can also, however, pass these values in as keyword parameters – we can explicitly specify the parameter names along with the values: print(make_greeting(title="Mr", name="John", surname="Smith")) print(make_greeting(title="Mr", name="John", surname="Smith", formal=False, time="evening")) We can mix positional and keyword parameters, but the keyword parameters must come after any positional parameters: # this is OK print(make_greeting("Mr", "John", surname="Smith")) # this will give you an error print(make_greeting(title="Mr", "John", "Smith")) We can specify keyword parameters in any order – they don’t have to match the order in the function definition: print(make_greeting(surname="Smith", name="John", title="Mr")) Now we can easily pass in the second optional parameter and not the first: print(make_greeting("Mr", "John", "Smith", time="evening")) Mutable types and default parameters¶We should be careful when using mutable types as default parameter values in function definitions if we intend to modify them in-place: def add_pet_to_list(pet, pets=[]): pets.append(pet) return pets list_with_cat = add_pet_to_list("cat") list_with_dog = add_pet_to_list("dog") print(list_with_cat) print(list_with_dog) # oops Remember that although we can execute a function body many times, a function definition is executed only once – that means that the empty list which is created in this function definition will be the same list for all instances of the function. What we really want to do in this case is to create an empty list inside the function body: def add_pet_to_list(pet, pets=None): if pets is None: pets = [] pets.append(pet) return pets Exercise 4¶
*args and **kwargs¶Sometimes we may want to pass a variable-length list of positional or keyword parameters into a function. We can put def print_args(*args): for arg in args: print(arg) def print_kwargs(**kwargs): for k, v in kwargs.items(): print("%s: %s" % (k, v)) Inside the function, we can access print_args("one", "two", "three") print_args("one", "two", "three", "four") print_kwargs(name="Jane", surname="Doe") print_kwargs(age=10) We can use my_list = ["one", "two", "three"] print_args(*my_list) my_dict = {"name": "Jane", "surname": "Doe"} print_kwargs(**my_dict) This makes it easier to build lists of parameters programmatically. Note that we can use this for any function, not just one which uses my_dict = { "title": "Mr", "name": "John", "surname": "Smith", "formal": False, "time": "evening", } print(make_greeting(**my_dict)) We can mix ordinary parameters, def print_everything(name, time="morning", *args, **kwargs): print("Good %s, %s." % (time, name)) for arg in args: print(arg) for k, v in kwargs.items(): print("%s: %s" % (k, v)) If we use a def print_everything(*args, **kwargs): for arg in args: print(arg) for k, v in kwargs.items(): print("%s: %s" % (k, v)) # we can write all the parameters individually print_everything("cat", "dog", day="Tuesday") t = ("cat", "dog") d = {"day": "Tuesday"} # we can unpack a tuple and a dictionary print_everything(*t, **d) # or just one of them print_everything(*t, day="Tuesday") print_everything("cat", "dog", **d) # we can mix * and ** with explicit parameters print_everything("Jane", *t, **d) print_everything("Jane", *t, time="evening", **d) print_everything(time="evening", *t, **d) # none of these are allowed: print_everything(*t, "Jane", **d) print_everything(*t, **d, time="evening") If a function takes only Exercise 5¶
Decorators¶Sometimes we may need to modify several functions in the same way – for example, we may want to perform a particular action before and after executing each of the functions, or pass in an extra parameter, or convert the output to another format. We may also have good reasons not to write the modification into all the functions – maybe it would make the function definitions very verbose and unwieldy, and maybe we would like the option to apply the modification quickly and easily to any function (and remove it just as easily). To solve this problem, we can write a function which modifies functions. We call a function like this a decorator. Our function will take a function object as a parameter, and will return a new function object – we can then assign the new function value to the old function’s name to replace the old function with the new function. For example, here is a decorator which logs the function name and its arguments to a log file whenever the function is used: # we define a decorator def log(original_function): def new_function(*args, **kwargs): with open("log.txt", "w") as logfile: logfile.write("Function '%s' called with positional arguments %s and keyword arguments %s.\n" % (original_function.__name__, args, kwargs)) return original_function(*args, **kwargs) return new_function # here is a function to decorate def my_function(message): print(message) # and here is how we decorate it my_function = log(my_function) Inside our decorator (the outer function) we define a replacement function and return it. The replacement function (the inner function) writes a log message and then simply calls the original function and returns its value. Note that the decorator function is only called once, when we replace the original function with the decorated function, but that the inner function will be called every time we use Because the inner function takes There is a shorthand syntax for applying decorators to functions: we can use the @log def my_function(message): print(message)
We can pass additional parameters to our decorator. For example, we may want to specify a custom log file to use in our logging decorator: def log(original_function, logfilename="log.txt"): def new_function(*args, **kwargs): with open(logfilename, "w") as logfile: logfile.write("Function '%s' called with positional arguments %s and keyword arguments %s.\n" % (original_function.__name__, args, kwargs)) return original_function(*args, **kwargs) return new_function @log("someotherfilename.txt") def my_function(message): print(message) Python has several built-in decorators which are commonly used to decorate class methods. We will learn about them in the next chapter. Note A decorator doesn’t have to be a function – it can be any callable object. Some people prefer to write decorators as classes. Exercise 6¶
Lambdas¶We have already seen that when we want to use a number or a string in our program we can either write it as a literal in the place where we
want to use it or use a variable that we have already defined in our code. For example, We have also seen that we can store a function in a variable, just like any other object, by referring to it by its name (but not calling it). Is there such a thing as a function literal? Can we define a function on the fly when we
want to pass it as a parameter or assign it to a variable, just like we did with the string The answer is yes, but only for very simple functions. We can use the a = lambda: 3 # is the same as def a(): return 3 Lambdas can take parameters – they are written between the b = lambda x, y: x + y # is the same as def b(x, y): return x + y Lambdas should only be used for very simple functions. If your lambda starts looking too complicated to be readable, you should rather write it out in full as a normal, named function. Exercise 7¶
Generator functions and yield¶We have already encountered generators – sequences in which new elements are generated as they are needed, instead of all being generated up-front. We can create our own generators by writing functions which make use of the Consider this simple function which returns a range of numbers as a list: def my_list(n): i = 0 l = [] while i < n: l.append(i) i += 1 return l This function builds the full list of numbers and returns it. We can change this function into a generator function while preserving a very similar syntax, like this: def my_gen(n): i = 0 while i < n: yield i i += 1 The first important thing to know about the g = my_gen(3) print(type(g)) for x in g: print(x) What does the After the If the generator executes the entire function without encountering a Exercise 8¶
Answers to exercises¶Answer to exercise 1¶Here is an example program: def func_a(): print("This is my awesome function.") func_a() b = func_a b() Answer to exercise 2¶Here is an example program: import math def hypotenuse(x, y): print(math.sqrt(x**2 + y**2)) hypotenuse(12.3, 45.6) hypotenuse(12, 34) hypotenuse(12, 34.5) Answer to exercise 3¶Here is an example program: import math def hypotenuse(x, y): try: return math.sqrt(x**2 + y**2) except TypeError: return None print(hypotenuse(12, 34)) print(hypotenuse("12", "34")) print(hypotenuse(12, "34")) Answer to exercise 3¶
Answer to exercise 4¶
Answer to exercise 5¶
Answer to exercise 6¶
Answer to exercise 7¶
Answer to exercise 8¶
What is a named sequence of statements?In the context of programming, a function is a named sequence of statements that performs a computation. When you define a function, you specify the name and the sequence of statements. Later, you can “call” the function by name.
When a function is called by its name during the execution of a program it is?global. When a function is called by its name during the execution of a program, then it is. executed.
What is the variable name that is used by a function to receive passed value?The data passed to the function are referred to as the "actual" parameters. The variables within the function which receive the passed data are referred to as the "formal" parameters.
Which of the following is a group of statements which are part of another statement or functions?Compound statements contain (groups of) other statements; they affect or control the execution of those other statements in some way.
|