Programming Essentials in Python | module 4 In this module we will learn about: defining and using functions different ways of passing arguments name scopes tuples and dictionaries data processing _______________________________________________________________________________________________________________ Why do we need functions? You've come across functions many times so far, but the view on their merits that we have given you has been rather one-sided. You've only invoked the functions by using them as tools to make life easier, and to simplify time-consuming and tedious tasks. When you want some data to be printed on the console, you use print(). When you want to read the value of a variable, you use input(), coupled with either int() or float(). You've also made use of some methods, which are in fact functions, but declared in a very specific way. Now you'll learn how to write your own functions, and how to use them. We'll write several functions together, from the very simple to the rather complex, which will require your focus and attention. It often happens that a particular piece of code is repeated many times in your program. It's repeated either literally, or with only a few minor modifications, consisting of the use of other variables in the same algorithm. It also happens that a programmer cannot resist simplifying the work, and begins to clone such pieces of code using the clipboard and copy-paste operations. It could end up as greatly frustrating when suddenly it turns out that there was an error in the cloned code. The programmer will have a lot of drudgery to find all the places that need corrections. There's also a high risk of the corrections causing errors. We can now define the first condition which can help you decide when to start writing your own functions: if a particular fragment of the code begins to appear in more than one place, consider the possibility of isolating it in the form of a function invoked from the points where the original code was placed before. It may happen that the algorithm you're going to implement is so complex that your code begins to grow in an uncontrolled manner, and suddenly you notice that you're not able to navigate through it so easily anymore. You can try to cope with the issue by commenting the code extensively, but soon you find that this dramatically worsens your situation - too many comments make the code larger and harder to read. Some say that a well-written function should be viewed entirely in one glance. A good and attentive developer divides the code (or more accurately: the problem) into well-isolated pieces, and encodes each of them in the form of a function. This considerably simplifies the work of the program, because each piece of code can be encoded separately, and tested separately. The process described here is often called decomposition. We can now state the second condition: if a piece of code becomes so large that reading and understating it may cause a problem, consider dividing it into separate, smaller problems, and implement each of them in the form of a separate function. This decomposition continues until you get a set of short functions, easy to understand and test. _______________________________________________________________________________________________________________ Functions Decomposition It often happens that the problem is so large and complex that it cannot be assigned to a single developer, and a team of developers have to work on it. The problem must be split between several developers in a way that ensures their efficient and seamless cooperation. It seems inconceivable that more than one programmer should write the same piece of code at the same time, so the job has to be dispersed among all the team members. This kind of decomposition has a different purpose to the one described previously - it's not only about sharing the work, but also about sharing the responsibility among many developers. Each of them writes a clearly defined and described set of functions, which when combined into the module (we'll tell you about this a bit later) will give the final product. This leads us directly to the third condition: if you're going to divide the work among multiple programmers, decompose the problem to allow the product to be implemented as a set of separately written functions packed together in different modules. Where do the functions come from? In general, functions come from at least three places: from Python itself - numerous functions (like print()) are an integral part of Python, and are always available without any additional effort on behalf of the programmer; we call these functions built-in functions; from Python's preinstalled modules - a lot of functions, very useful ones, but used significantly less often than built-in ones, are available in a number of modules installed together with Python; the use of these functions requires some additional steps from the programmer in order to make them fully accessible (we'll tell you about this in a while); directly from your code - you can write your own functions, place them inside your code, and use them freely; there is one other possibility, but it's connected with classes, so we'll omit it for now. _______________________________________________________________________________________________________________ Your first function Take a look at the snippet in the editor. It's rather simple, but we only want it to be an example of transforming a repeating part of a code into a function. The messages sent to the console by the print() function are always the same. Of course, there's nothing really bad in such a code, but try to imagine what you would have to do if your boss asked you to change the message to make it more polite, e.g., to start it with the phrase "Please,". It seems that you'd have to spend some time changing all the occurrences of the message (you'd use a clipboard, of course, but it wouldn't make your life much easier). It's obvious that you'd probably make some mistakes during the amendment process, and you (and your boss) would get a bit frustrated. Is it possible to separate such a repeatable part of the code, name it and make it reusable? It would mean that a change made once in one place would be propagated to all the places where it's used. Of course, such a code should work only when it's explicitly launched. Yes, it's possible. This is exactly what functions are for. _______________________________________________________________________________________________________________ Your first function How do you make such a function? You need to define it. The word define is significant here. This is what the simplest function definition looks like: def functionName(): functionBody It always starts with the keyword def (for define) next after def goes the name of the function (the rules for naming functions are exactly the same as for naming variables) after the function name, there's a place for a pair of parentheses (they contain nothing here, but that will change soon) the line has to be ended with a colon; the line directly after def begins the function body - a couple (at least one) of necessarily nested instructions, which will be executed every time the function is invoked; note: the function ends where the nesting ends, so you have to be careful. We're ready to define our prompting function. We'll name it message - here it is: def message(): print("Enter a value: ") The function is extremely simple, but fully usable. We've named it message, but you can label it according to your taste. Let's use it. Our code contains the function definition now: def message(): print("Enter a value: ") print("We start here.") print("We end here.") Note: we don't use the function at all - there's no invocation of it inside the code. When you run it, you see the following output: We start here. We end here. This means that Python reads the function's definitions and remembers them, but won't launch any of them without your permission. We've modified the code now - we've inserted the function's invocation between the start and end messages: def message(): print("Enter a value: ") print("We start here.") message() print("We end here.") The output looks different now: We start here. Enter a value: We end here. Test the code, modify it, experiment with it. _______________________________________________________________________________________________________________ How functions work when you invoke a function, Python remembers the place where it happened and jumps into the invoked function; the body of the function is then executed; reaching the end of the function forces Python to return to the place directly after the point of invocation. You mustn't invoke a function which is not known at the moment of invocation. Remember - Python reads your code from top to bottom. It's not going to look ahead in order to find a function you forgot to put in the right place ("right" means "before invocation".) We've inserted an error into this code - can you see the difference? print("We start here.") message() print("We end here.") def message(): print("Enter a value: ") We've moved the function to the end of the code. Is Python able to find it when the execution reaches the invocation? No, it isn't. The error message will read: NameError: name 'message' is not defined Don't try to force Python to look for functions you didn't deliver at the right time. The second catch sounds a little simpler: You mustn't have a function and a variable of the same name The following snippet is erroneous: def message(): print("Enter a value: ") message = 1 Assigning a value to the name message causes Python to forget its previous role. The function named message becomes unavailable. Fortunately, you're free to mix your code with functions - you're not obliged to put all your functions at the top of your source file. Look at the snippet: print("We start here.") def message(): print("Enter a value: ") message() print("We end here.") It may look strange, but it's completely correct, and works as intended. Let's return to our primary example, and employ the function for the right job, like here: def message(): print("Enter a value: ") message() a = int(input()) message() b = int(input()) message() c = int(input()) Modifying the prompting message is now easy and clear - you can do it by changing the code in just one place - inside the function's body. Open the mycompiler.io, and try to do it yourself. _______________________________________________________________________________________________________________ Key takeaways 1. A function is a block of code that performs a specific task when the function is called (invoked). You can use functions to make your code reusable, better organized, and more readable. Functions can have parameters and return values. 2. There are at least four basic types of functions in Python: built-in functions which are an integral part of Python (such as the print() function). You can see a complete list of Python built-in functions at https://docs.python.org/3/library/functions.html. the ones that come from pre-installed modules (you'll learn about them in Module 5 of this course) user-defined functions which are written by users for users - you can write your own functions and use them freely in your code, the lambda functions (you'll learn about them in Module 6 of this course.) You can define your own function using the def keyword and the following syntax: def yourFunction(optional parameters): # the body of the function You can define a function which doesn't take any arguments, e.g.: def message(): # defining a function print("Hello") # body of the function message() # calling the function You can define a function which takes arguments, too, just like the one-parameter function below: def hello(name): # defining a function print("Hello,", name) # body of the function name = input("Enter your name: ") hello(name) # calling the function We'll tell you more about parametrized functions in the next section. Don't worry. _______________________________________________________________________________________________________________ How functions communicate with their environment The function's full power reveals itself when it can be equipped with an interface that is able to accept data provided by the invoker. Such data can modify the function's behavior, making it more flexible and adaptable to changing conditions. A parameter is actually a variable, but there are two important factors that make parameters different and special: parameters exist only inside functions in which they have been defined, and the only place where the parameter can be defined is a space between a pair of parentheses in the def statement; assigning a value to the parameter is done at the time of the function's invocation, by specifying the corresponding argument. def function(parameter): ### Don't forget: parameters live inside functions (this is their natural environment) arguments exist outside functions, and are carriers of values passed to corresponding parameters. There is a clear and unambiguous frontier between these two worlds. Let's enrich the function above with just one parameter - we're going to use it to show the user the number of a value the function asks for. We have to rebuild the def statement - this is how it looks now: def message(number): ### The definition specifies that our function operates on just one parameter named number. You can use it as an ordinary variable, but only inside the function - it isn't visible anywhere else. Let's now improve the function's body: def message(number): print("Enter a number:", number) We've made use of the parameter. Note: we haven't assigned the parameter with any value. Is it correct? Yes, it is. A value for the parameter will arrive from the function's environment. Remember: specifying one or more parameters in a function's definition is also a requirement, and you have to fulfil it during invocation. You must provide as many arguments as there are defined parameters. Failure to do so will cause an error. _______________________________________________________________________________________________________________ Parametrized functions: continued Try to run the code in the editor. This is what you'll see in the console: TypeError: message() missing 1 required positional argument: 'number' This looks better, for sure: def message(number): print("Enter a number:", number) message(1) Moreover, it behaves better. The code will produce the following output: Enter a number: 1 Can you see how it works? The value of the argument used during invocation (1) has been passed into the function, setting the initial value of the parameter named number. We have to make you sensitive to one important circumstance. It's legal, and possible, to have a variable named the same as a function's parameter The snippet illustrates the phenomenon: def message(number): print("Enter a number:", number) number = 1234 message(1) print(number) A situation like this activates a mechanism called shadowing: parameter x shadows any variable of the same name, but... ... only inside the function defining the parameter. The parameter named number is a completely different entity from the variable named number. This means that the snippet above will produce the following output: Enter a number: 1 1234 code sample: def message(number): print("Enter a number:", number) message(1) ______________________________________________________________________________________________________________ Parametrized functions: continued A function can have as many parameters as you want, but the more parameters you have, the harder it is to memorize their roles and purposes. Let's modify the function - it has two parameters now: def message(what, number): print("Enter", what, "number", number) This also means that invoking the function will require two arguments. The first new parameter is intended to carry the name of the desired value. Here it is: def message(what, number): print("Enter", what, "number", number) message("telephone", 11) message("price", 5) message("number", "number") This is the output you're about to see: Enter telephone number 11 Enter price number 5 Enter number number number Run the code, modify it, add more parameters, and see how this affects the output. _______________________________________________________________________________________________________________ Positional parameter passing A technique which assigns the ith (first, second, and so on) argument to the ith (first, second, and so on) function parameter is called positional parameter passing, while arguments passed in this way are named positional arguments. You've used it already, but Python can offer a lot more. We're going to tell you about it now. def myFunction(a, b, c): print(a, b, c) myFunction(1, 2, 3) Note: positional parameter passing is intuitively used by people in many social occasions. For example, it may be generally accepted that when we introduce ourselves we mention our first name(s) before our last name, e.g., "My name's John Doe." Incidentally, Hungarians do it in reverse order. Let's implement that social custom in Python. The following function will be responsible for introducing somebody: def introduction(firstName, lastName): print("Hello, my name is", firstName, lastName) introduction("Luke", "Skywalker") introduction("Jesse", "Quick") introduction("Clark", "Kent") Can you guess the output? Run the code and find out if you were right. Now imagine that the same function is being used in Hungary. In this case, the code would look like this: def introduction(firstName, lastName): print("Hello, my name is", firstName, lastName) introduction("Skywalker", "Luke") introduction("Quick", "Jesse") introduction("Kent", "Clark") The output will look different. Can you guess it? Run the code to see if you were right here, too. Are you surprised? Can you make the function more culture-independent? _______________________________________________________________________________________________________________ Keyword argument passing Python offers another convention for passing arguments, where the meaning of the argument is dictated by its name, not by its position - it's called keyword argument passing. Take a look at the snippet: def introduction(firstName, lastName): print("Hello, my name is", firstName, lastName) introduction(firstName = "James", lastName = "Bond") introduction(lastName = "Skywalker", firstName = "Luke") The concept is clear - the values passed to the parameters are preceded by the target parameters' names, followed by the = sign. The position doesn't matter here - each argument's value knows its destination on the basis of the name used. You should be able to predict the output. Run the code to check if you were right. Of course, you mustn't use a non-existent parameter name. The following snippet will cause a runtime error: def introduction(firstName, lastName): print("Hello, my name is", firstName, lastName) introduction(surname="Skywalker", firstName="Luke") This is what Python will tell you: TypeError: introduction() got an unexpected keyword argument 'surname' Try it yourself! _______________________________________________________________________________________________________________ Mixing positional and keyword arguments You can mix both fashions if you want - there is only one unbreakable rule: you have to put positional arguments before keyword arguments. If you think for a moment, you'll certainly guess why. To show you how it works, we'll use the following simple three-parameter function: def adding(a, b, c): print(a, "+", b, "+", c, "=", a + b + c) Its purpose is to evaluate and present the sum of all its arguments. The function, when invoked in the following way: adding(1, 2, 3) will output: 1 + 2 + 3 = 6 It was - as you may suspect - a pure example of positional argument passing. Of course, you can replace such an invocation with a purely keyword variant, like this: adding(c = 1, a = 2, b = 3) Our program will output a line like this: 2 + 3 + 1 = 6 Note the order of the values. Let's try to mix both styles now. Look at the function invocation below: adding(3, c = 1, b = 2) Let's analyze it: the argument (3) for the a parameter is passed using the positional way; the arguments for c and b are specified as keyword ones. This is what you'll see in the console: 3 + 2 + 1 = 6 Be careful, and beware of mistakes. If you try to pass more than one value to one argument, all you'll get is a runtime error. Look at the invocation below - it seems that we've tried to set a twice: adding(3, a = 1, b = 2) Python's response: TypeError: adding() got multiple values for argument 'a' Look at the snipet below. A code like this is fully correct, but it doesn't make much sense: adding(4, 3, c = 2) Everything is right, but leaving in just one keyword argument looks a bit weird - what do you think? _______________________________________________________________________________________________________________ Parametrized functions - more details It happens at times that a particular parameter's values are in use more often than others. Such arguments may have their default (predefined) values taken into consideration when their corresponding arguments have been omitted. They say that the most popular English last name is Smith. Let's try to take this into account. The default parameter's value is set using clear and pictorial syntax: def introduction(firstName, lastName="Smith"): print("Hello, my name is", fistName, lastName) You only have to extend the parameter's name with the = sign, followed by the default value. Let's invoke the function as usual: introduction("James", "Doe") Can you guess the output of the program? Run it and check if you were right. And? Everything looks the same, but when you invoke the function in a way that looks a bit suspicious at first sight, like this: introduction("Henry") or this: introduction(firstName="William") there will be no error, and both invocations will succeed, while the console will show the following output: Hello, my name is Henry Smith Hello, my name is William Smith Test it. You can go further if it's useful. Both parameters have their default values now, look at the code below: def introduction(firstName="John", lastName="Smith"): print("Hello, my name is", firstName, lastName) This makes the following invocation absolutely valid: introduction() And this is the expected output: Hello, my name is John Smith If you use one keyword argument, the remaining one will take the default value: introduction(lastName="Hopkins") The output is: Hello, my name is John Hopkins Test it. Congratulations - you have just learned the basic ways of communicating with functions. def introduction(firstName, lastName="Smith"): print("Hello, my name is", firstName, lastName) # call the function here introduction("Michael") _______________________________________________________________________________________________________________ Section summery Key takeaways 1. You can pass information to functions by using parameters. Your functions can have as many parameters as you need An example of a one-parameter function: def hi(name): print("Hi,", name) hi("Giga" An example of a two-parameter function: def hiAll(name1, name2): print("Hi,", name2) print("Hi,", name1) hiAll("Sebastian", "Konrad") An example of a three-parameter function: def address(street, city, postalCode): print("Your address is:", street, "St.,", city, postalCode) s = input("Street: ") pC = input("Postal Code: ") c = input("City: ") address(s, c, pC) 2. You can pass arguments to a function using the following techniques: positional argument passing in which the order of arguments passed matters (Ex. 1), keyword (named) argument passing in which the order of arguments passed doesn't matter (Ex. 2), a mix of positional and keyword argument passing (Ex. 3). Ex. 1 def subtra(a, b): print(a - b) subtra(5, 2) # outputs: 3 subtra(2, 5) # outputs: -3 Ex. 2 def subtra(a, b): print(a - b) subtra(a=5, b=2) # outputs: 3 subtra(b=2, a=5) # outputs: 3 Ex. 3 def subtra(a, b): print(a - b) subtra(5, b=2) # outputs: 3 subtra(5, 2) # outputs: 3 It's important to remember that positional arguments mustn't follow keyword arguments. That's why if you try to run the following snippet: def subtra(a, b): print(a - b) subtra(5, b=2) # outputs: 3 subtra(a=5, 2) # Syntax Error Python will not let you do it by signalling a SyntaxError exercise with output: What is the output of the following snippet? def intro(a="James Bond", b="Bond"): print("My name is", b + ".", a + ".") intro() My name is Bond. James Bond. _______________________________________________________________________________________________________________ Effects and results: the return instruction All the previously presented functions have some kind of effect - they produce some text and send it to the console. Of course, functions - like their mathematical siblings - may have results. To get functions to return a value (but not only for this purpose) you use the return instruction. This word gives you a full picture of its capabilities. Note: it's a Python keyword. The return instruction has two different variants - let's consider them separately. return without an expression The first consists of the keyword itself, without anything following it. When used inside a function, it causes the immediate termination of the function's execution, and an instant return (hence the name) to the point of invocation. Note: if a function is not intended to produce a result, using the return instruction is not obligatory - it will be executed implicitly at the end of the function. Anyway, you can use it to terminate a function's activities on demand, before the control reaches the function's last line. Let's consider the following function: def happy_new_year(wishes = True): print("Three...") print("Two...") print("One...") if not wishes: return print("Happy New Year!") When invoked without any arguments: happy_new_year() The function causes a little noise - the output will look like this: Three... Two... One... Happy New Year! Providing False as an argument: happy_new_year(False) will modify the function's behavior - the return instruction will cause its termination just before the wishes - this is the updated output: Three... Two... One... return with an expression The second return variant is extended with an expression: def function(): return expression There are two consequences of using it: it causes the immediate termination of the function's execution (nothing new compared to the first variant) moreover, the function will evaluate the expression's value and will return (hence the name once again) it as the function's result. Yes, we already know - this example isn't really sophisticated: def boring_function(): return 123 x = boring_function() print("The boring_function has returned its result. It's:", x) The snippet writes the following text to the console: The boring_function has returned its result. It's: 123 Let's investigate it for a while. The return instruction, enriched with the expression (the expression is very simple here), "transports" the expression's value to the place where the function has been invoked. The result may be freely used here, e.g., to be assigned to a variable. It may also be completely ignored and lost without a trace. Note, we're not being too polite here - the function returns a value, and we ignore it (we don't use it in any way): def boring_function(): print("'Boredom Mode' ON.") return 123 print("This lesson is interesting!) boring_function() print("This lesson is boring...") The program produces the following output: This lesson is interesting! 'Boredom Mode' ON. This lesson is boring... Is it punishable? Not at all. The only disadvantage is that the result has been irretrievably lost. Don't forget: you are always allowed to ignore the function's result, and be satisfied with the function's effect (if the function has any) if a function is intended to return a useful result, it must contain the second variant of the return instruction. Wait a minute - does this mean that there are useless results, too? Yes - in some sense. _______________________________________________________________________________________________________________ A few words about None Let us introduce you to a very curious value (to be honest, a none value) named None. Its data doesn't represent any reasonable value - actually, it's not a value at all; hence, it mustn't take part in any expressions. For example, a snippet like this: print(None + 2) will cause a runtime error, described by the following diagnostic message: TypeError: unsupported operand type(s) for +: 'NoneType' and 'int' Note: None is a keyword. There are only two kinds of circumstances when None can be safely used: when you assign it to a variable (or return it as a function's result) when you compare it with a variable to diagnose its internal state. Just like here: value = None if value is None: print("Sorry, you don't carry any value") Don't forget this: if a function doesn't return a certain value using a return expression clause, it is assumed that it implicitly returns None. Let's test it. _______________________________________________________________________________________________________________ A few words about None: continued Take a look at the code in the editor. It's obvious that the strangeFunction function returns True when its argument is even. What does it return otherwise? We can use the following code to check it: print(strangeFunction(2)) print(strangeFunction(1)) This is what we see in the console: True None Don't be surprised next time you see None as a function result - it may be the symptom of a subtle mistake inside the function. def strangeFunction(n): if(n % 2 == 0): return True _______________________________________________________________________________________________________________ Effects and results: lists and functions There are two additional questions that should be answered here. The first is: may a list be sent to a function as an argument? Of course it may! Any entity recognizable by Python can play the role of a function argument, although it has to be assured that the function is able to cope with it. So, if you pass a list to a function, the function has to handle it like a list. A function like this one here: def list_sum(lst): s = 0 for elem in lst: s += elem return s and invoked like this: print(list_sum([5, 4, 3])) will return 12 as a result, but you should expect problems if you invoke it in this risky way: print(list_sum(5)) Python's response will be unequivocal: TypeError: 'int' object is not iterable This is caused by the fact that a single integer value mustn't be iterated through by the for loop. _______________________________________________________________________________________________________________ Effects and results: lists and functions - continued The second question is: may a list be a function result? Yes, of course! Any entity recognizable by Python can be a function result. Look at the code in the editor. The program's output will be like this: [4, 3, 2, 1, 0] Now you can write functions with and without results. Let's dive a little deeper into the issues connected with variables in functions. This is essential for creating effective and safe functions. _______________________________________________________________________________________________________________ 100.1.11.110 LAB: A leap year: writing your own functions Estimated time 10-15 minutes Level of difficulty easy Objectives Familiarize the student with: projecting and writing parameterized functions; utilizing the return statement; testing the functions. Scenario Your task is to write and test a function which takes one argument (a year) and returns True if the year is a leap year, or False otherwise. The seed of the function is already sown in the skeleton code in the editor. Note: we've also prepared a short testing code, which you can use to test your function. The code uses two lists - one with the test data, and the other containing the expected results. The code will tell you if any of your results are invalid. def isYearLeap(year): # # put your code here # testData = [1900, 2000, 2016, 1987] testResults = [False, True, True, False] for i in range(len(testData)): yr = testData[i] print(yr,"->",end="") result = isYearLeap(yr) if result == testResults[i]: print("OK") else: print("Failed") _______________________________________________________________________________________________________________ LAB: How many days: writing and using your own functions Estimated time 15-20 minutes Level of difficulty Medium Objectives Familiarize the student with: projecting and writing parameterized functions; utilizing the return statement; utilizing the student's own functions. Scenario Your task is to write and test a function which takes two arguments (a year and a month) and returns the number of days for the given month/year pair (while only February is sensitive to the year value, your function should be universal). The initial part of the function is ready. Now, convince the function to return None if its arguments don't make sense. Of course, you can (and should) use the previously written and tested function (LAB 4.1.3.6). It may be very helpful. We encourage you to use a list filled with the months' lengths. You can create it inside the function - this trick will significantly shorten the code. We've prepared a testing code. Expand it to include more test cases. def isYearLeap(year): # # your code from LAB 4.1.3.6 # def daysInMonth(year, month): # # put your new code here # testYears = [1900, 2000, 2016, 1987] testMonths = [2, 2, 1, 11] testResults = [28, 29, 31, 30] for i in range(len(testYears)): yr = testYears[i] mo = testMonths[i] print(yr, mo, "->", end="") result = daysInMonth(yr, mo) if result == testResults[i]: print("OK") else: print("Failed") _______________________________________________________________________________________________________________ Converting fuel consumption Scenario A car's fuel consumption may be expressed in many different ways. For example, in Europe, it is shown as the amount of fuel consumed per 100 kilometers. In the USA, it is shown as the number of miles traveled by a car using one gallon of fuel. Your task is to write a pair of functions converting l/100km into mpg, and vice versa. The functions: are named l100kmtompg and mpgtol100km respectively; take one argument (the value corresponding to their names) Complete the code in the editor. Run your code and check whether your output is the same as ours. Here is some information to help you: 1 American mile = 1609.344 metres; 1 American gallon = 3.785411784 litres. Test Data Expected output: 60.31143162393162 31.36194444444444 23.52145833333333 3.9007393587617467 7.490910297239916 10.009131205673757 ---------------------------------------------- def l100kmtompg(litros): galones = litros / 3.785411784 millas = 100 * 1000 / 1609.344 return millas / galones def mpgtol100km(millas): km100 = millas * 1609.344 / 1000 / 100 litros = 3.785411784 return litros / km100 print(l100kmtompg(3.9)) print(l100kmtompg(7.5)) print(l100kmtompg(10.)) print(mpgtol100km(60.3)) print(mpgtol100km(31.4)) print(mpgtol100km(23.5)) _______________________________________________________________________________________________________________ Section summery Key takeaways 1. You can use the return keyword to tell a function to return some value. The return statement exits the function, e.g.: def multiply(a, b): return a * b print(multiply(3, 4)) # outputs: 12 def multiply(a, b): return print(multiply(3, 4)) # outputs: None 2. The result of a function can be easily assigned to a variable, e.g.: def wishes(): return "Happy Birthday!" w = wishes() print(w) # outputs: Happy Birthday! Look at the difference in output in the following two examples: # Example 1 def wishes(): print("My Wishes") return "Happy Birthday" wishes() # outputs: My Wishes # Example 2 def wishes(): print("My Wishes") return "Happy Birthday" print(wishes()) # outputs: My Wishes # Happy Birthday 3. You can use a list as a function's argument, e.g.: def hiEverybody(myList): for name in myList: print("Hi,", name) hiEverybody(["Adam", "John", "Lucy"]) 4. A list can be a function result, too, e.g.: def createList(n): myList = [] for i in range(n): myList.append(i) return myList print(createList(5)) _______________________________________________________________________________________________________________ Scopes in Python Functions and scopes Let's start with a definition: The scope of a name (e.g., a variable name) is the part of a code where the name is properly recognizable. For example, the scope of a function's parameter is the function itself. The parameter is inaccessible outside the function. Let's check it. Look at the code in the editor. What will happen when you run it? The program will fail when run. The error message will read: NameError: name 'x' is not defined This is to be expected. We're going to conduct some experiments with you to show you how Python constructs scopes, and how you can use its habits to your benefit. def scopeTest(): x = 123 scopeTest() print(x) _______________________________________________________________________________________________________________ Functions and scopes: continued Let's start by checking whether or not a variable created outside any function is visible inside the functions. In other words, does a variable's name propagate into a function's body? Look at the code in the editor. Our guinea pig is there. The result of the test is positive - the code outputs: Do I know that variable? 1 1 The answer is: a variable existing outside a function has a scope inside the functions' bodies. This rule has a very important exception. Let's try to find it. Let's make a small change to the code: def myFunction(): var = 2 print("Do I know that variable?", var) var = 1 myFunction() print(var) The result has changed, too - the code produces a slightly different output now: Do I know that variable? 2 1 What's happened? the var variable created inside the function is not the same as when defined outside it - it seems that there two different variables of the same name; moreover, the function's variable shadows the variable coming from the outside world. We can make the previous rule more precise and adequate: A variable existing outside a function has a scope inside the functions' bodies, excluding those of them which define a variable of the same name. It also means that the scope of a variable existing outside a function is supported only when getting its value (reading). Assigning a value forces the creation of the function's own variable. Make sure you understand this well and carry out your own experiments. def myFunction(): var = 2 print("Do I know that variable?", var) var = 1 myFunction() _______________________________________________________________________________________________________________ Functions and scopes: the global keyword Hopefully, you should now have arrived at the following question: does this mean that a function is not able to modify a variable defined outside it? This would create a lot of discomfort. Fortunately, the answer is no. There's a special Python method which can extend a variable's scope in a way which includes the functions' bodies (even if you want not only to read the values, but also to modify them). Such an effect is caused by a keyword named global: global name global name1, name2, ... Using this keyword inside a function with the name (or names separated with commas) of a variable(s), forces Python to refrain from creating a new variable inside the function - the one accessible from outside will be used instead. In other words, this name becomes global (it has a global scope, and it doesn't matter whether it's the subject of read or assign). Look at the code in the editor. We've added global to the function. The code now outputs: Do I know that variable? 2 2 This should be sufficient evidence to show that the global keyword does what it promises. def myFunction(): global var var = 2 print("Do I know that variable?", var) var = 1 myFunction() print(var) _______________________________________________________________________________________________________________ How the function interacts with its arguments Now let's find out how the function interacts with its arguments. The code in the editor should teach you something. As you can see, the function changes the value of its parameter. Does the change affect the argument? Run the program and check. The code's output is: I got 1 I have 2 1 The conclusion is obvious - changing the parameter's value doesn't propagate outside the function (in any case, not when the variable is a scalar, like in the example). This also means that a function receives the argument's value, not the argument itself. This is true for scalars. Is it worth checking how it works with lists (do you recall the peculiarities of assigning list slices versus assigning lists as a whole?). The following example will shed some light on the issue: def myFunction(myList1): print(myList1) myList1 = [0, 1] myList2 = [2, 3] myFunction(myList2) print(myList2) The code's output is: [2, 3] [2, 3] It seems that the former rule still works. Finally, can you see the difference in the example below: def myFunction(myList1): print(myList1) del myList1[0] myList2 = [2, 3] myFunction(myList2) print(myList2) We don't change the value of the parameter myList1 (we already know it will not affect the argument), but instead modify the list identified by it. The output may be surprising. Run the code and check: [2, 3] [3] Can you explain it? Let's try: if the argument is a list, then changing the value of the corresponding parameter doesn't affect the list (remember: variables containing lists are stored in a different way than scalars) but if you change a list identified by the parameter (note: the list, not the parameter!), the list will reflect the change. It's time to write some example functions. You'll do that in the next section. def myFunction(n): print("I got", n) n += 1 print("I have", n) var = 1 myFunction(var) print(var) _______________________________________________________________________________________________________________ Section summery Key takeaways 1. A variable that exists outside a function has a scope inside the function body (Example 1) unless the function defines a variable of the same name (Example 2, and Example 3), e.g.: Example 1: var = 2 def multByVar(x): return x * var print(multByVar(7)) # outputs: 14 Example 2: def mult(x): var = 5 return x * var print(mult(7)) # outputs: 35 Example 3: def multip(x): var = 7 return x * var var = 3 print(multip(7)) # outputs: 49 2. A variable that exists inside a function has a scope inside the function body (Example 4), e.g.: Example 4: def adding(x): var = 7 return x + var print(adding(4)) # outputs: 11 print(var) # NameError 3. You can use the global keyword followed by a variable name to make the variable's scope global, e.g.: var = 2 print(var) # outputs: 2 def retVar(): global var var = 5 return var print(retVar()) # outputs: 5 print(var) # outputs: 5 _______________________________________________________________________________________________________________ Some simple functions: evaluating the BMI Let's get started on a function to evaluate the Body Mass Index (BMI). BMI = (weight in kilograms) / (height in meters) As you can see, the formula gets two values: weight (originally in kilograms) height (originally in meters) It seems that this new function will have two parameters. Its name will be bmi, but if you prefer any other name, use it instead. Let's code the function. The function is complete below (and in the editor window): def bmi(weight, height): return weight / height ** 2 print(bmi(52.5, 1.65)) The result produced by the sample invocation looks as follows: 19.283746556473833 The function fulfils our expectations, but it's a bit simple - it assumes that the values of both parameters are always meaningful. It's definitely worth checking if they're trustworthy. Let's check them both and return None if any of them looks suspicious. _______________________________________________________________________________________________________________ Some simple functions: evaluating BMI and converting imperial units to metric units Look at the code in the editor. There are two things we need to pay attention to. First, the test invocation ensures that the protection works properly - the output is: NoneSecond, take a look at the way the backslash (\) symbol is used. If you use it in Python code and end a line with it, it will tell Python to continue the line of code in the next line of code. It can be particularly useful when you have to deal with long lines of code and you'd like to improve code readability. Okay, but there's something we omitted too easily - the imperial measurements. This function is not too useful for people accustomed to pounds, feet and inches. What can be done for them? We can write two simple functions to convert imperial units to metric ones. Let's start with pounds. It is a well-known fact that 1 lb = 0.45359237 kg. We'll use this in our new function. This is our helper function, named lbtokg: def lbtokg(lb): return lb * 0.45359237 print(lbtokg(1)) The result of the test invocation looks good: 0.45359237 And now it's time for feet and inches: 1 ft = 0.3048 m, and 1 in = 2.54 cm = 0.0254 m. The function we've written is named ftintom: def ftintom(ft, inch): return ft * 0.3048 + inch * 0.0254 print(ftintom(1, 1)) The result of a quick test is: 0.3302 It looks as expected. Note: we wanted to name the second parameter just in, not inch, but we couldn't. Do you know why? in is a Python keyword - it cannot be used as a name. Let's convert six feet into meters: print(ftintom(6, 0)) And this is the output: 1.8288000000000002 It's quite possible that sometimes you may want to use just feet without inches. Will Python help you? Of course it will. We've modified the code a bit: def ftintom(ft, inch = 0.0): return ft * 0.3048 + inch * 0.0254 print(ftintom(6)) Now the inch parameter has its default value equal to 0.0. The code produces the following output - this is what is expected: 1.8288000000000002 Finally, the code is able to answer the question: what is the BMI of a person 5'7" tall and weighing 176 lbs? This is the code we have built: def ftintom(ft, inch = 0.0): return ft * 0.3048 + inch * 0.0254 def lbstokg(lb): return lb * 0.45359237 def bmi(weight, height): if height < 1.0 or height > 2.5 or \ weight < 20 or weight > 200: return None return weight / height ** 2 print(bmi(weight = lbstokg(176), height = ftintom(5, 7))) And the answer is: 27.565214082533313 Run the code and test it. snippet: def bmi(weight, height): if height < 1.0 or height > 2.5 or \ weight < 20 or weight > 200: return None return weight / height ** 2 print(bmi(352.5, 1.65)) _______________________________________________________________________________________________________________ Creating functions | three-parameter functions Let's play with triangles now. We'll start with a function to check whether three sides of given lengths can build a triangle. We know from school that the sum of two arbitrary sides has to be longer than the third side. It won't be a hard challenge. The function will have three parameters - one for each side. It will return True if the sides can build a triangle, and False otherwise. In this case, isItATriangle is a good name for such a function. Look at the code in the editor. You can find our function there. Run the program. It seems that it works well - these are the results: True False Can we make it more compact? It looks a bit wordy. This is a more compact version: def isItATriangle(a, b, c): if a + b <= c or b + c <= a or \ c + a <= b: return False return True print(isItATriangle(1, 1, 1)) print(isItATriangle(1, 1, 3)) Can we compact it even more? Yes, we can - look: def isItATriangle(a, b, c): return a + b > c and b + c > a and c + a > b print(isItATriangle(1, 1, 1)) print(isItATriangle(1, 1, 3)) We've negated the condition (reversed the relational operators and replaced ors with ands, receiving a universal expression for testing triangles). Let's install the function in a larger program. It'll ask the user for three values and make use of the function. def isItATriangle(a, b, c): if a + b <= c: return False if b + c <= a: return False if c + a <= b: return False return True print(isItATriangle(1, 1, 1)) print(isItATriangle(1, 1, 3)) _______________________________________________________________________________________________________________ Creating functions | testing triangles Some simple functions: triangles and the Pythagorean theorem Look at the code in the editor. It asks the user for three values. Then it makes use of the isItATriangle function. The code is ready to run. In the second step, we'll try to ensure that a certain triangle is a right-angle triangle. We will need to make use of the Pythagorean theorem c2 = a2 + b2 How do we recognize which of the three sides is the hypotenuse? The hypotenuse is the longest side. Here is the code: def isItATriangle(a, b, c): return a + b > c and b + c > a and c + a > b def isItRightTriangle(a, b, c): if not isItATriangle(a, b, c): return False if c > a and c > b: return c ** 2 == a ** 2 + b ** 2 if a > b and a > c: return a ** 2 == b ** 2 + c ** 2 print(isItRightTriangle(5, 3, 4)) print(isItRightTriangle(1, 3, 4)) Look at how we test the relationship between the hypotenuse and the remaining sides - we choose the longest side, and apply the Pythagorean theorem to check if everything is right. This requires three checks in total. snippet: def isItATriangle(a, b, c): return a + b > c and b + c > a and c + a > b a = float(input("Enter the first side's length: ")) b = float(input("Enter the second side's length: ")) c = float(input("Enter the third side's length: ")) if isItATriangle(a, b, c): print("Congratulations - it can be a triangle.") else: print("Sorry, it won't be a triangle.") _______________________________________________________________________________________________________________ Some simple functions: evaluating a triangle's field We can also evaluate a triangle's field. Heron's formula will be handy here: s = (a + b + c) / 2 A = the suare root of s(s - a)(s - b)(s - c) We're going use the exponentiation operator to find the square root - it may seem strange, but it works: The square root of x = x to the power of 1/2 This is the resulting code: def isItATriangle(a, b, c): return a + b > c and b + c > a and c + a > b def heron(a, b, c): p = (a + b + c) / 2 return (p * (p - a) * (p - b) * (p - c)) ** 0.5 def fieldOfTriangle(a, b, c): if not isItATriangle(a, b, c): return None return heron(a, b, c) print(fieldOfTriangle(1., 1., 2. ** .5)) We try it with a right-angle triangle as a half of a square with one side equal to 1. This means that its field should be equal to 0.5. It's odd - the code produces the following output: 0.49999999999999983 It's very close to 0.5, but it isn't exactly 0.5. What does it mean? Is it an error? No, it isn't. This is the specifics of floating-point calculations. We'll tell you more about it soon. def isItATriangle(a, b, c): return a + b > c and b + c > a and c + a > b a = float(input("Enter the first side's length: ")) b = float(input("Enter the second side's length: ")) c = float(input("Enter the third side's length: ")) if isItATriangle(a, b, c): print("Congratulations - it can be a triangle.") else: print("Sorry, it won't be a triangle.") _______________________________________________________________________________________________________________ Some simple functions: factorials Another function we're about to write is factorials. Do you remember how a factorial is defined? 0! = 1 (yes! it's true) 1! = 1 2! = 1 * 2 3! = 1 * 2 * 3 4! = 1 * 2 * 3 * 4 : : n! = 1 * 2 ** 3 * 4 * ... * n-1 * n It's marked with an exclamation mark, and is equal to the product of all natural numbers from one up to its argument. Let's write our code. We'll create a function and call it factorialFun. Here is the code: def factorialFun(n): if n < 0: return None if n < 2: return 1 product = 1 for i in range(2, n + 1): product *= i return product for n in range(1, 6): # testing print(n, factorialFun(n)) Notice how we mirror step by step the mathematical definition, and how we use the for loop to find the product. We add a simple testing code, and these are the results we get: 1 1 2 2 3 6 4 24 5 120 _______________________________________________________________________________________________________________ Some simple functions: Fibonacci numbers Are you familiar with Fibonacci numbers? They are a sequence of integer numbers built using a very simple rule: the first element of the sequence is equal to one (Fib1 = 1) the second is also equal to one (Fib2 = 1) every subsequent number is the sum of the two preceding numbers (Fibi = Fibi-1 + Fibi-2) Here are some of the first Fibonacci numbers: fib1 = 1 fib2 = 1 fib3 = 1 + 1 = 2 fib4 = 1 + 2 = 3 fib5 = 2 + 3 = 5 fib6 = 3 + 5 = 8 fib7 = 5 + 8 = 13 What do you think about implementing it as a function? Let's create our fib function and test it. Here it is: def fib(n): if n < 1: return None if n < 3: return 1 elem1 = elem2 = 1 sum = 0 for i in range(3, n + 1): sum = elem1 + elem2 elem1, elem2 = elem2, sum return sum for n in range(1, 10): # testing print(n, "->", fib(n)) Analyze the for loop body carefully, and find out how we move the elem1 and elem2 variables through the subsequent Fibonacci numbers. The test part of the code produces the following output: 1 -> 1 2 -> 1 3 -> 2 4 -> 3 5 -> 5 6 -> 8 7 -> 13 8 -> 21 9 -> 34 _______________________________________________________________________________________________________________ Some simple functions: recursion There's one more thing we want to show you to make everything complete - it's recursion. This term may describe many different concepts, but one of them is especially interesting - the one referring to computer programming. In this field, recursion is a technique where a function invokes itself. These two cases seem to be the best to illustrate the phenomenon - factorials and Fibonacci numbers. Especially the latter. The Fibonacci numbers definition is a clear example of recursion. We already told you that: Fibi = Fibi-1 + Fibi-2 The definition of the ith number refers to the i-1 number, and so on, till you reach the first two. Can it be used in the code? Yes, it can. It can also make the code shorter and clearer. The second version of our fib() function makes direct use of this definition: def fib(n): if n < 1: return None if n < 3: return 1 return fib(n - 1) + fib(n - 2) The code is much clearer now. But is it really safe? Does it entail any risk? Yes, there is a little risk indeed. If you forget to consider the conditions which can stop the chain of recursive invocations, the program may enter an infinite loop. You have to be careful. The factorial has a second, recursive side too. Look: n! = 1 × 2 × 3 × ... × n-1 × n It's obvious that: 1 × 2 × 3 × ... × n-1 = (n-1)! So, finally, the result is: n! = (n-1)! × n This is in fact a ready recipe for our new solution. Here it is: def factorialFun(n): if n < 0: return None if n < 2: return 1 return n * factorialFun(n - 1) Does it work? Yes, it does. Try it for yourself. Our short functional journey is almost over. The next section will take care of two curious Python data types: tuples and dictionaries. _______________________________________________________________________________________________________________ Key takeaways 1. A function can call other functions or even itself. When a function calls itself, this situation is known as recursion, and the function which calls itself and contains a specified termination condition (i.e., the base case - a condition which doesn't tell the function to make any further calls to that function) is called a recursive function. 2. You can use recursive functions in Python to write clean, elegant code, and divide it into smaller, organized chunks. On the other hand, you need to be very careful as it might be easy to make a mistake and create a function which never terminates. You also need to remember that recursive calls consume a lot of memory, and therefore may sometimes be inefficient. When using recursion, you need to take all its advantages and disadvantages into consideration. The factorial function is a classic example of how the concept of recursion can be put in practice: # Recursive implementation of the factorial function def factorial(n): if n == 1: # the base case (termination condition) return 1 else: return n * factorial(n - 1) print(factorial(4)) # 4 * 3 * 2 * 1 = 24 _______________________________________________________________________________________________________________ Tuples and dictionaries Sequence types and mutability Before we start talking about tuples and dictionaries, we have to introduce two important concepts: sequence types and mutability. A sequence type is a type of data in Python which is able to store more than one value (or less than one, as a sequence may be empty), and these values can be sequentially (hence the name) browsed, element by element. As the for loop is a tool especially designed to iterate through sequences, we can express the definition as: a sequence is data which can be scanned by the for loop. You've encountered one Python sequence so far - the list. The list is a classic example of a Python sequence, although there are some other sequences worth mentioning, and we're going to present them to you now. The second notion - mutability - is a property of any of Python's data that describes its readiness to be freely changed during program execution. There are two kinds of Python data: mutable and immutable. Mutable data can be freely updated at any time - we call such an operation in situ. In situ is a Latin phrase that translates as literally in position. For example, the following instruction modifies the data in situ: list.append(1) Immutable data cannot be modified in this way Imagine that a list can only be assigned and read over. You would be able neither to append an element to it, nor remove any element from it. This means that appending an element to the end of the list would require the recreation of the list from scratch. You would have to build a completely new list, consisting of the all elements of the already existing list, plus the new element. The data type we want to tell you about now is a tuple. A tuple is an immutable sequence type. It can behave like a list, but it mustn't be modified in situ. What is a tuple? The first and the clearest distinction between lists and tuples is the syntax used to create them - tuples prefer to use parenthesis, whereas lists like to see brackets, although it's also possible to create a tuple just from a set of values separated by commas. Look at the example: tuple1 = (1, 2, 4, 8) tuple2 = 1., .5, .25, .125 There are two tuples, both containing four elements. Let's print them: print(tuple1) print(tuple2) This is what you should see in the console: (1, 2, 4, 8) (1.0, 0.5, 0.25, 0.125) Note: each tuple element may be of a different type (floating-point, integer, or any other not-as-yet-introduced kind of data). How to create a tuple? It is possible to create an empty tuple - parentheses are required then: emptyTuple = () If you want to create a one-element tuple, you have to take into consideration the fact that, due to syntax reasons (a tuple has to be distinguishable from an ordinary, single value), you must end the value with a comma: oneElementTuple1 = (1, ) oneElementTuple2 = 1., Removing the commas won't spoil the program in any syntactical sense, but you will instead get two single variables, not tuples. _______________________________________________________________________________________________________________ How to use a tuple? If you want to get the elements of a tuple in order to read them over, you can use the same conventions to which you're accustomed while using lists. Take a look at the code in the editor. The program should produce the following output - run it and check: 1 1000 (10, 100, 1000) (1, 10) 1 10 100 1000 The similarities may be misleading - don't try to modify a tuple's contents! It's not a list! All of these instructions (except the topmost one) will cause a runtime error: myTuple = (1, 10, 100, 1000) myTuple.append(10000) del myTuple[0] myTuple[1] = -10 This is the message that Python will give you in the console window: AttributeError: 'tuple' object has no attribute 'append' example: myTuple = (1, 10, 100, 1000) print(myTuple[0]) print(myTuple[-1]) print(myTuple[1:]) print(myTuple[:-2]) for elem in myTuple: print(elem) _______________________________________________________________________________________________________________ How to use a tuple: continued What else can tuples do for you? the len() function accepts tuples, and returns the number of elements contained inside; the + operator can join tuples together (we've shown you this already) the * operator can multiply tuples, just like lists; the in and not in operators work in the same way as in lists. The snippet in the editor presents them all. The output should look as follows: 9 (1, 10, 100, 1000, 10000) (1, 10, 100, 1, 10, 100, 1, 10, 100) True True One of the most useful tuple properties is their ability to appear on the left side of the assignment operator. You saw this phenomenon some time ago, when it was necessary to find an elegant tool to swap two variables' values. Take a look at the snippet below: var = 123 t1 = (1, ) t2 = (2, ) t3 = (3, var) t1, t2, t3 = t2, t3, t1 print(t1, t2, t3) It shows three tuples interacting - in effect, the values stored in them "circulate" - t1 becomes t2, t2 becomes t3, and t3 becomes t1 Note: the example presents one more important fact: a tuple's elements can be variables, not only literals. Moreover, they can be expressions if they're on the right side of the assignment operator. myTuple = (1, 10, 100) t1 = myTuple + (1000, 10000) t2 = myTuple * 3 print(len(t2)) print(t1) print(t2) print(10 in myTuple) print(-10 not in myTuple) _______________________________________________________________________________________________________________ What is a dictionary? The dictionary is another Python data structure. It's not a sequence type (but can be easily adapted to sequence processing) and it is mutable. To explain what the Python dictionary actually is, it is important to understand that it is literally a dictionary. The Python dictionary works in the same way as a bilingual dictionary. For example, you have an English word (e.g., cat) and need its French equivalent. You browse the dictionary in order to find the word (you may use different techniques to do that - it doesn't matter) and eventually you get it. Next, you check the French counterpart and it is (most probably) the word "chat". Tuples Dictionary In Python's world, the word you look for is named a key. The word you get from the dictionary is called a value. This means that a dictionary is a set of key-value pairs. Note: each key must be unique - it's not possible to have more than one key of the same value; a key may be data of any type (except list): it may be a number (integer or float), or even a string; a dictionary is not a list - a list contains a set of numbered values, while a dictionary holds pairs of values; the len() function works for dictionaries, too - it returns the numbers of key-value elements in the dictionary; a dictionary is a one-way tool - if you have an English-French dictionary, you can look for French equivalents of English terms, but not vice versa Now we can show you some working examples. How to make a dictionary? If you want to assign some initial pairs to a dictionary, you should use the following syntax: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} phone_numbers = {'boss' : 5551234567, 'Suzy' : 22657854310} empty_dictionary = {} print(dictionary) print(phone_numbers) print(empty_dictionary) In the first example, the dictionary uses keys and values which are both strings. In the second one, the keys are strings, but the values are integers. The reverse layout (keys → numbers, values → strings) is also possible, as well as number-number combination. The list of pairs is surrounded by curly braces, while the pairs themselves are separated by commas, and the keys and values by colons. The first of our dictionaries is a very simple English-French dictionary. The second - a very tiny telephone directory. The empty dictionaries are constructed by an empty pair of curly braces - nothing unusual. The dictionary as a whole can be printed with a single print() invocation. The snippet may produce the following output: {'dog': 'chien', 'horse': 'cheval', 'cat': 'chat'} {'Suzy': 5557654321, 'boss': 5551234567} {} Have you noticed anything surprising? The order of the printed pairs is different than in the initial assignment. What does that mean? First of all, it's a confirmation that dictionaries are not lists - they don't preserve the order of their data, as the order is completely meaningless (unlike in real, paper dictionaries). The order in which a dictionary stores its data is completely out of your control, and your expectations. That's normal. (*) Note: (*) In Python 3.6x dictionaries have become ordered collections by default. Your results may vary depending on what Python version you're using. _______________________________________________________________________________________________________________ How to use a dictionary? If you want to get any of the values, you have to deliver a valid key value: print(dictionary['cat']) print(phone_numbers['Suzy']) Getting a dictionary's value resembles indexing, especially thanks to the brackets surrounding the key's value. Note: if the key is a string, you have to specify it as a string; keys are case-sensitive: 'Suzy' is something different from 'suzy'. The snippet outputs two lines of text: chat 5557654321 And now the most important news: you mustn't use a non-existent key. Trying something like this: print(phone_numbers['president']) will cause a runtime error. Try to do it. Fortunately, there's a simple way to avoid such a situation. The in operator, together with its companion, not in, can salvage this situation. The following code safely searches for some French words: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} words = ['cat', 'lion', 'horse'] for word in words: if word in dictionary: print(word, "->", dictionary[word]) else: print(word, "is not in dictionary") The code's output looks as follows: cat -> chat lion is not in dictionary horse -> cheval dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} phone_numbers = {'boss' : 5551234567, 'Suzy' : 22657854310} empty_dictionary = {} ### print(dictionary['cat']) print(phone_numbers['Suzy']) _______________________________________________________________________________________________________________ How to use a dictionary: the keys() Can dictionaries be browsed using the for loop, like lists or tuples? No and yes. No, because a dictionary is not a sequence type - the for loop is useless with it. Yes, because there are simple and very effective tools that can adapt any dictionary to the for loop requirements (in other words, building an intermediate link between the dictionary and a temporary sequence entity). The first of them is a method named keys(), possessed by each dictionary. The method returns an iterable object consisting of all the keys gathered within the dictionary. Having a group of keys enables you to access the whole dictionary in an easy and handy way. Just like here: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} for key in dictionary.keys(): print(key, "->", dictionary[key] The code's output looks as follows: horse -> cheval dog -> chien cat -> chat The sorted() function Do you want it sorted? Just enrich the for loop to get such a form: for key in sorted(dictionary.keys()): The sorted() function will do its best - the output will look like this: cat -> chat dog -> chien horse -> cheval _______________________________________________________________________________________________________________ Tuples and dictionaries | methods How to use a dictionary: The items() and values() methods Another way is based on using a dictionary's method named items(). The method returns tuples (this is the first example where tuples are something more than just an example of themselves) where each tuple is a key-value pair. This is how it works: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} for english, french in dictionary.items(): print(english, "->", french) Note the way in which the tuple has been used as a for loop variable. The example prints: cat -> chat dog -> chien horse -> cheval There is also a method named values(), which works similarly to keys(), but returns values. Here is a simple example: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} for french in dictionary.values(): print(french) As the dictionary is not able to automatically find a key for a given value, the role of this method is rather limited. Here is the expected output: cheval chien chat dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} for english, french in dictionary.items(): print(english, "->", french) _______________________________________________________________________________________________________________ How to use a dictionary: modifying and adding values Assigning a new value to an existing key is simple - as dictionaries are fully mutable, there are no obstacles to modifying them. We're going to replace the value "chat" with "minou", which is not very accurate, but it will work well with our example. Look: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} dictionary['cat'] = 'minou' print(dictionary) The output is: {'dog': 'chien', 'horse': 'cheval', 'cat': 'minou'} Adding a new key Adding a new key-value pair to a dictionary is as simple as changing a value - you only have to assign a value to a new, previously non-existent key. Note: this is very different behavior compared to lists, which don't allow you to assign values to non-existing indices. Let's add a new pair of words to the dictionary - a bit weird, but still valid: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} dictionary['swan'] = 'cygne' print(dictionary) The example output: {'swan': 'cygne', 'horse': 'cheval', 'dog': 'chien', 'cat': 'chat'} Extra You can also insert an item to a dictionary by using the update() method, e.g.: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} dictionary.update({"duck" : "canard"}) print(dictionary) Removing a key Can you guess how to remove a key from a dictionary? Note: removing a key will always cause the removal of the associated value. Values cannot exist without their keys. This is done with the del instruction. Here's the example: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} del dictionary['dog'] print(dictionary) Note: removing a non-existing key causes an error. The example outputs: {'cat': 'chat', 'horse': 'cheval'} Extra To remove the last item in a dictionary, you can use the popitem() method: dictionary = {"cat" : "chat", "dog" : "chien", "horse" : "cheval"} dictionary.popitem() print(dictionary) # outputs: {'cat' : 'chat', 'dog' : 'chien'} In the older versions of Python, i.e., before 3.6.7, the popitem() method removes a random item from a dictionary. _______________________________________________________________________________________________________________ Tuples and dictionaries Tuples and dictionaries can work together We've prepared a simple example, showing how tuples and dictionaries can work together. Let's imagine the following problem: you need a program to evaluate the students' average scores; the program should ask for the student's name, followed by her/his single score; the names may be entered in any order; entering an empty name finishes the inputting of the data; a list of all names, together with the evaluated average score, should be then emitted. Look at the code in the editor. This how to do it. Now, let's analyze it line by line: line 1: create an empty dictionary for the input data; the student's name is used as a key, while all the associated scores are stored in a tuple (the tuple may be a dictionary value - that's not a problem at all) line 3: enter an "infinite" loop (don't worry, it'll break at the right moment) line 4: read the student's name here; line 5-6: if the name is exit, leave the loop; line 8: ask for one of the student's scores (an integer from the range 1-10) line 10-11: if the student's name is already in the dictionary, lengthen the associated tuple with the new score (note the += operator) line 12-13: if this is a new student (unknown to the dictionary), create a new entry - its value is a one-element tuple containing the entered score; line 15: iterate through the sorted students' names; line 16-17: initialize the data needed to evaluate the average (sum and counter) line 18-20: we iterate through the tuple, taking all the subsequent scores and updating the sum, together with the counter; line 21: evaluate and print the student's name and average score. This is a record of a conversation we had with our program: Enter the student's name and press Enter to stop: Bob Enter the student's score (0-10): 7 Enter the student's name (or type exit to stop): Andy Enter the student's score (0-10): 3 Enter the student's name (or type exit to stop): Bob Enter the student's score (0-10): 2 Enter the student's name (or type exit to stop): Andy Enter the student's score (0-10): 10 Enter the student's name (or type exit to stop): Andy Enter the student's score (0-10): 3 Enter the student's name (or type exit to stop): Bob Enter the student's score (0-10): 9 Enter the student's name (or type exit to stop): Andy : 5.333333333333333 Bob : 6.0 snippet: school_class = {} while True: name = input("Enter the student's name (or type exit to stop): ") if name == 'exit': break score = int(input("Enter the student's score (0-10): ")) if name in school_class: school_class[name] += (score,) else: school_class[name] = (score,) for name in sorted(school_class.keys()): adding = 0 counter = 0 for score in school_class[name]: adding += score counter += 1 print(name, ":", adding / counter) _______________________________________________________________________________________________________________ Key takeaways: tuples 1. Tuples are ordered and unchangeable (immutable) collections of data. They can be thought of as immutable lists. They are written in round brackets: myTuple = (1, 2, True, "a string", (3, 4), [5, 6], None) print(myTuple) myList = [1, 2, True, "a string", (3, 4), [5, 6], None] print(myList) Each tuple element may be of a different type (i.e., integers, strings, booleans, etc.). What is more, tuples can contain other tuples or lists (and the other way round). 2. You can create an empty tuple like this: emptyTuple = () print(type(emptyTuple)) # outputs: 3. A one-element tuple may be created as follows: oneElemTup1 = ("one", ) # brackets and a comma oneElemTup2 = "one", # no brackets, just a comma If you remove the comma, you will tell Python to create a variable, not a tuple: myTup1 = 1, print(type(myTup1)) # outputs: myTup2 = 1 print(type(myTup2)) # outputs: 4. You can access tuple elements by indexing them: myTuple = (1, 2.0, "string", [3, 4], (5, ), True) print(myTuple[3]) # outputs: [3, 4] 5. Tuples are immutable, which means you cannot change their elements (you cannot append tuples, or modify, or remove tuple elements). The following snippet will cause an exception: myTuple = (1, 2.0, "string", [3, 4], (5, ), True) myTuple[2] = "guitar" # a TypeError exception will be raised However, you can delete a tuple as a whole: myTuple = 1, 2, 3, del myTuple print(myTuple) # NameError: name 'myTuple' is not defined 6. You can loop through a tuple elements (Example 1), check if a specific element is (not)present in a tuple (Example 2), use the len() function to check how many elements there are in a tuple (Example 3), or even join/multiply tuples (Example 4): # Example 1 t1 = (1, 2, 3) for elem in t1: print(elem) # Example 2 t2 = (1, 2, 3, 4) print(5 in t2) print(5 not in t2) # Example 3 t3 = (1, 2, 3, 5) print(len(t3)) # Example 4 t4 = t1 + t2 t5 = t3 * 2 print(t4) print(t5) Extra: You can also create a tuple using a Python built-in function called tuple(). This is particularly useful when you want to convert a certain iterable (e.g., a list, range, string, etc.) to a tuple: myTup = tuple((1, 2, "string")) print(myTup) lst = [2, 4, 6] print(lst) # outputs: [2, 4, 6] print(type(lst)) # outputs: tup = tuple(lst) print(tup) # outputs: (2, 4, 6) print(type(tup)) # outputs: By the same fashion, when you want to convert an iterable to a list, you can use a Python built-in function called list(): tup = 1, 2, 3, lst = list(tup) print(type(lst)) # outputs: _______________________________________________________________________________________________________________ Key takeaways: dictionaries 1. Dictionaries are unordered*, changeable (mutable), and indexed collections of data. (*In Python 3.6x dictionaries have become ordered by default. Each dictionary is a set of key : value pairs. You can create it by using the following syntax: myDictionary = { key1 : value1, key2 : value2, key3 : value3, } 2. If you want to access a dictionary item, you can do so by making a reference to its key inside a pair of square brackets (ex. 1) or by using the get() method (ex. 2): polEngDict = { "kwiat" : "flower", "woda" : "water", "gleba" : "soil" } item1 = polEngDict["gleba"] # ex. 1 print(item1) # outputs: soil item2 = polEngDict.get("woda") print(item2) # outputs: water 3. If you want to change the value associated with a specific key, you can do so by referring to the item's key name in the following way: polEngDict = { "zamek" : "castle", "woda" : "water", "gleba" : "soil" } polEngDict["zamek"] = "lock" item = polEngDict["zamek"] # outputs: lock 4. To add or remove a key (and the associated value), use the following syntax: myPhonebook = {} # an empty dictionary myPhonebook["Adam"] = 3456783958 # create/add a key-value pair print(myPhonebook) # outputs: {'Adam': 3456783958} del myPhonebook["Adam"] print(myPhonebook) # outputs: {} You can also insert an item to a dictionary by using the update() method, and remove the last element by using the popitem() method, e.g.: polEngDict = {"kwiat" : "flower"} polEngDict = update("gleba" : "soil") print(polEngDict) # outputs: {'kwiat' : 'flower', 'gleba' : 'soil'} polEngDict.popitem() print(polEngDict) # outputs: {'kwiat' : 'flower'} 5. You can use the for loop to loop through a dictionary, e.g.: polEngDict = { "zamek" : "castle", "woda" : "water", "gleba" : "soil" } for item in polEngDict: print(item) # outputs: zamek # woda # gleba 6. If you want to loop through a dictionary's keys and values, you can use the items() method, e.g.: polEngDict = { "zamek" : "castle", "woda" : "water", "gleba" : "soil" } for key, value in polEngDict.items(): print("Pol/Eng ->", key, ":", value) 7. To check if a given key exists in a dictionary, you can use the in keyword: polEngDict = { "zamek" : "castle", "woda" : "water", "gleba" : "soil" } if "zamek" in polEngDict: print("Yes") else: print("No") 8. You can use the del keyword to remove a specific item, or delete a dictionary. To remove all the dictionary's items, you need to use the clear() method: polEngDict = { "zamek" : "castle", "woda" : "water", "gleba" : "soil" } print(len(polEngDict)) # outputs: 3 del polEngDict["zamek"] # remove an item print(len(polEngDict)) # outputs: 2 polEngDict.clear() # removes all the items print(len(polEngDict)) # outputs: 0 del polEngDict # removes the dictionary 9. To copy a dictionary, use the copy() method: polEngDict = { "zamek" : "castle", "woda" : "water", "gleba" : "soil" } copyDict = polEngDict.copy() _______________________________________________________________________________________________________________ project tic-tac-toe Estimate time 30=60 minutes Level of difficulty Medium/Hard Objectives perfecting the student's skills in using Python for solving complex problems, integration of programming techniques in one program consisting of many various parts. Scenario Your task is to write a simple program which pretends to play tic-tac-toe with the user. To make it all easier for you, we've decided to simplify the game. Here are our assumptions: the computer (i.e., your program) should play the game using 'X's; the user (e.g., you) should play the game using 'O's; the first move belongs to the computer - it always puts its first 'X' in the middle of the board; all the squares are numbered row by row starting with 1 (see the example session below for reference) the user inputs their move by entering the number of the square they choose - the number must be valid, i.e., it must be an integer, it must be greater than 0 and less than 10, and it cannot point to a field which is already occupied; the program checks if the game is over - there are four possible verdicts: the game should continue, or the game ends with a tie, your win, or the computer's win; the computer responds with its move and the check is repeated; don't implement any form of artificial intelligence - a random field choice made by the computer is good enough for the game. The example session with the program may look as follows: +-------+-------+-------+ | | | | | 1 | 2 | 3 | | | | | +-------+-------+-------+ | | | | | 4 | X | 6 | | | | | +-------+-------+-------+ | | | | | 7 | 8 | 9 | | | | | +-------+-------+-------+ Enter your move: 1 +-------+-------+-------+ | | | | | O | 2 | 3 | | | | | +-------+-------+-------+ | | | | | 4 | X | 6 | | | | | +-------+-------+-------+ | | | | | 7 | 8 | 9 | | | | | +-------+-------+-------+ +-------+-------+-------+ | | | | | O | X | 3 | | | | | +-------+-------+-------+ | | | | | 4 | X | 6 | | | | | +-------+-------+-------+ | | | | | 7 | 8 | 9 | | | | | +-------+-------+-------+ Enter your move: 8 +-------+-------+-------+ | | | | | O | X | 3 | | | | | +-------+-------+-------+ | | | | | 4 | X | 6 | | | | | +-------+-------+-------+ | | | | | 7 | O | 9 | | | | | +-------+-------+-------+ +-------+-------+-------+ | | | | | O | X | 3 | | | | | +-------+-------+-------+ | | | | | 4 | X | X | | | | | +-------+-------+-------+ | | | | | 7 | O | 9 | | | | | +-------+-------+-------+ Enter your move: 4 +-------+-------+-------+ | | | | | O | X | 3 | | | | | +-------+-------+-------+ | | | | | O | X | X | | | | | +-------+-------+-------+ | | | | | 7 | O | 9 | | | | | +-------+-------+-------+ +-------+-------+-------+ | | | | | O | X | X | | | | | +-------+-------+-------+ | | | | | O | X | X | | | | | +-------+-------+-------+ | | | | | 7 | O | 9 | | | | | +-------+-------+-------+ Enter your move: 7 +-------+-------+-------+ | | | | | O | X | X | | | | | +-------+-------+-------+ | | | | | O | X | X | | | | | +-------+-------+-------+ | | | | | O | O | 9 | | | | | +-------+-------+-------+ You won! Requirements Implement the following features: the board should be stored as a three-element list, while each element is another three-element list (the inner lists represent rows) so that all of the squares may be accessed using the following syntax: board[row][column] each of the inner list's elements can contain 'O', 'X', or a digit representing the square's number (such a square is considered free) the board's appearance should be exactly the same as the one presented in the example. implement the functions defined for you in the editor. Drawing a random integer number can be done by utilizing a Python function called randrange(). The example program below shows how to use it (the program prints ten random numbers from 0 to 8). Note: the from-import instruction provides an access to the randrange function defined within an external Python module callled random. from random import randrange for i in range(10): print(randrange(8)) guide: def DisplayBoard(board): # # the function accepts one parameter containing the board's current status # and prints it out to the console # def EnterMove(board): # # the function accepts the board current status, asks the user about their move, # checks the input and updates the board according to the user's decision # def MakeListOfFreeFields(board): # # the function browses the board and builds a list of all the free squares; # the list consists of tuples, while each tuple is a pair of row and column numbers # def VictoryFor(board, sign): # # the function analyzes the board status in order to check if # the player using 'O's or 'X's has won the game # def DrawMove(board): # # the function draws the computer's move and updates the board # _______________________________________________________________________________________________________________ Congratulations! You have completed Module 4. Well done! You've reached the end of Module 4 and completed a major milestone in your Python programming education. Here's a short summary of the objectives you've covered and got familiar with in Module 4: the defining and using of functions - their rationale, purpose, conventions, and traps; the concept of passing arguments in different ways and setting their default values, along with the mechanisms of returning the function's results; name scope issues; new data aggregates: tuples and dictionaries, and their role in data processing. _______________________________________________________________________________________________________________