Monday, June 24, 2013

PyTutorial: Playing with functions

This is the 5th in a series of posts.  If you want to go the the start, go here.

Functions are a lot of fun! While in essence, all they do is help you organize your program, their key contribution to programming is that they empower the programmer to think more abstractly about what they want to achieve.  Instead of thinking strictly in terms of loops and ifs, functions let you think in terms of goals.  Not only that, they enable the programmer to "divide and conquer" by breaking up the main goal into smaller sub-goals, and then put them together to achieve the desired end.

Sounds like fun, doesn't it? :)

For example, say you wanted to add together the maximum values of two lists.
If one list had the values [3, 6, 12, 3, 6] and another [9, 5, 2, 5, 3], then you would want to add together 12 and 9 to get 21.

By now, you should already be able to do that.  In fact, I'm sure that loops and ifs are running through your mind at this very moment.  Great! :)  Because I want you to appreciate functions, I need you to suffer a little first:
Assignment 5.1: go head and write code that does just that: adds the maximum values of two lists.
Great!  I'm guessing that you either did some loop within a loop, or two separate loops, one after the other.  Either way, your solutions can be simplified, and thus made more elegant, with the use of functions.  Check this out:

Can you figure out what it does by just looking at it?  If you guessed that "find_max" is a function that calculates the maximum item in a list, then you guessed right! (Woohoo!)  We then use this function twice, once per list that we want to find its max item.  But let me go over the details with you:

Basically, the keyword def is Python's way of letting you define a function!  Defining a function doesn't actually do anything!  It just tells Python that there is a new function that can be used later on.  I'll go into it soon, but for now, let's go over how a function is structured.

After the def, you give the name of the function, which like a variable, can be just about anything you feel like.  To be useful, functions usually have inputs and outputs.  In tech speak, the inputs are called "input parameters" or just plain "parameters" or even "arguments", and the outputs are called "return values" or sometimes "output parameters".  Besides inputs and outputs, the function also does stuff.  The stuff it does, in tech speak, is called the function's "body".

So in logical order, first the function takes its inputs, or parameters.  Then, in its body, it does stuff (usually to the inputs), and finally, it returns whatever it wants as output to whomever used it.  In tech-speak, using a function is called "calling" or "invoking" it.   As I mentioned earlier, just defining a function is pretty useless, to actually get some work done, one needs to call the function.  Thus in our example, our find_max function is called twice: once, with list1 as its parameter and assigning the return value to max1, and the second time with list2 as its parameter, assigning the return value to max2.  Do you see how when calling a function, you place whatever input you want to give it in parenthesis?



Part of what makes functions so powerful is just this: while you write them only once, you can call them with different inputs and get different outputs!  Behind the scenes, the way it works is basically like this:
When Python encounters the line: max1 = find_max(list1), it:
  1. goes to the code where you defined find_max.  If it can't find it, you'll get an error message (duh!)  Most likely it won't be able to find it if you typed in its name incorrectly.  Congratulations! :)
  2. Python makes sure that you're calling it with the right amount of input parameters.  Again, if you called it with the wrong number of inputs, you'll get an error! 
  3. It then proceeds to assign each of the caller's input parameters to the input function's input variables.  This copies a reference of the caller's variables into the function's variables.  In our case, there is only one input parameter, so it only needs to do this:
    mylist = list1
  4. See how it assigned the caller's variable, in our case "list1", to "mylist"?  Neat, huh?
  5. Then it performs, or in tech-speak, "runs", the body.  This works exactly the same as any code you have already written.  Nothing new here!
  6. Nothing new except the "return" command, which tells the  function to exit with some value.  In our case, it would normally be max_item (but it can also be None, more on this below). 
  7. It then assigns a reference to the return values to whatever variable the caller wants to, in our case, it would do something like this:
    max1 = max_item
Got it? Good! :)  I gave you the details here just so you see that it's not really magic.  There is a little more to it than I mentioned, and I'll mention some if it a bit later, but that's pretty much it!  Nice, huh?

I want to tell you a bit more about the "return" statement.  A return is a way to end the function, and return to the caller whatever output you want.  Our function has two exit points: at one point it returns None as an output to the caller (which may cause problems for us later on, but I'll let you try to figure out why).  And at another, it returns whatever is the value of max_value at the time.

This would be a good time for you to try and type in the code example above and try to run it!  Pay attention to the tabs, because as we know by now, they're critical in Python.  Notice the colon at the end of the def line, and that the entire function body is indented.
Assignment 5.2: type in the code above and play with it, change the values of list1 and list2 and see what happens.
BTW, after running your code, type this in the Python Shell:
>>> type(find_max)

I find it interesting that functions can be thought of as information as well.  But different from numbers or lists because a function's information describes how to perform various actions!

In our example, the function max_one has one input parameter, and one output parameter.  But this doesn't need to be the case.  A function can have as many input and output parameters as you want.  It can have zero or more input parameters, and zero or more output parameters.  The way to have more than one parameter is to separate them  with commas, like this:  (try it!)


You can also have a function without any inputs or outputs: (try this too!)

And of course, you can mix and match however you like:  (and this too!)
See?!  That's a function with two input parameters, but only one return value.  Really, it's that simple.

Sometimes, you may not care for the return value of a function.  This may be the case if the body of the function is more important to you than its output.  If you don't assign the return values to anything, Python just throws them away: (guess what? try this too)
Here we call do_stuff twice.  The first time, we don't assign its return value (5) to anything, but we still get to see "amazing! 5" displayed on the screen.  The second time, we assign the return value to z, and then print it to the screen.  Try it!

Now it's your turn to write some code:
Assignment 5.3: write a function (call it "foo") that takes in two parameters, and returns the smaller of the two.
Were you able to figure it out?  There are plenty of ways to write it, and of course, you can call the input parameters whatever you like, but it should look something like this:
Here's another one:
Assignment 5.4: write a function that takes in three parameters, and returns the smaller of the three.
And yet another:
Assignment 5.5: write a function (call it "find_min") that takes in a list, and instead of returning its smallest item, just prints it to the screen.  That is, this function doesn't have any return values.
Assignment 5.6: now change the above function and have it actually return the smallest item, returning None if the list is empty.
Good.

Functions and references

Remember the tutorial on information, and how I went on and on about references?  I mentioned that when we get to functions (as well as objects, later on), this gets important.  Well, we're here! :)

Because in Python, input (as well as output) parameters are passed in by reference, it means that in a way, input parameters aren't strictly input parameters.  They can be used to modify their inputs, and thus, in a way, be output parameters as well!  Check this out:
What do you expect to see happen?  Try it!
Do you see how the value of a was modified from within the function?  This happens because listlist is referencing a.  If you modify the parameters this way, they become both input and output parameters.  Something to keep in mind.

One is the loneliest number...

Having one function is nice for simple things.  But if you have more complicated things that you want to achieve, you may want to break up the task into two or more functions.  Of course, each function can call any other function.  Check out this little example:

Can you follow what happens?  Here we defined two function,the first, find_min2, returns the minimum of two numbers.  We then define find_min3 which uses find_min2 in order to easily find the minimum of three numbers!  Try typing it in to see how this works!  The key point here is that within the body of a function, you can do whatever you want, including calling other function!
Assignment 5.7: write two functions, find_max2 and find_max3 in a similar manner to the above example.
When a function returns only one value, it's possible to nest them.  Nesting functions is tech-speak for directly using the output value of one function as the input value of another.  Check out how I can rewrite find_min3:
Can you see how now one function call is actually placed within the other?  That's nesting for ya!  First, Python runs find_min2(a, b).  The reason it runs this inner call first is because it needs to in order to find the value of the first input parameter for the outer call!  So basically, it figures out the minimum between a and b,  and then the minimum between that minimum and c.  To better see this, add some print statements to  find_min2 to see exactly how it is being called:

Of course, we could have written find_min3 slightly differently, and it would still work correctly:

Do you see that the difference here is that it first find the minimum between b and c, and then that minimum and a?  Play with it a bit until you get a good feel for it.  :)  It's a bit tricky, but very orderly all the same.
Assignment 5.8: modify the find_max3 that you previously wrote to work using a nesting.

Other people's functions

One of the most useful things you can keep in mind as a programmer is that when it comes to many problems, you're not the first to have encountered them!  Besides unit-testing, knowing that you can use other people's code is what makes a programmer a professional!  Now, I'm not recommending stealing other's code.  There is a lot of code out there that is free and ready for everyone to use!  In fact, the very Python interpreter & the IDLE editor that you're using thus far has been free, and it was useful, wasn't it?  Same is true about other people's code.  
Using others' time-tested, proven code is usually better than writing a buggy version of the same yourself.
Now, the biggest problem of using other's code is that there is so much of it to chose from!  Really, it gets hard actually finding the code that you want to use, because it's buried in a heap of other code that you don't really want.  But it's a good habit to get familiar with the available code base of whatever language you're using.  As this tutorial develops, I'll slowly introduce you to what the Python community has to offer.  

So after much ado, let me introduce you to Python's built-in functions.  These are functions that were deemed soooooo useful for general programming that the language just comes ready with them!   One such function that you're already familiar is "len", which returns the number of items in a list (or even the length of a string).  But there are more, lots more.  You can find them all here!  Go ahead and take some time now to go  over them.  You don't need to memorize what all of them do, just get a feel for what's being offered.  Some of the ones that I find most useful are:
  • len(), enumerate(), id()
  • range(): returns a list of numbers from 0 to whatever you want.  In fact, it doesn't even need to start at 0.  It's very useful in a for loop, say, if, you know you want the loop to run a specific number of times. 
  • min() and max():  they can be used in various ways, such as: min(3, 5, 9), or with a list, such as min([3, 1, 3, 5, 3]).  Neat, huh?
  • int(), float(), str(): int returns an integer, you can call it like this: int(3.9), int("45").  float() and str() do similar things, but return different types.
  • round(): similar to int(), but rounds to the nearest value.  compare the results of int(3.9) and round(3.9)
One of the nicest things about Python is the interactive interpreter.  It lets you try out new code quickly and easily to see what it does...  You can do things like:
>>> range(10)
>>> range(5, 20)
>>> range(4, 20, 3)
>>> for a in range(20):
              print a, a*a

>>> min(3, 6, 7, 2, 4, 7, 2, 4)
>>> round(2.9)
>>> int("37")
>>> str(38)
>>> float("28.9")
>>> float(23)

Neat, huh?  You get all those goodies, and for absolutely free!  So scan though those built-in functions, and play around with whichever seem interesting to you! :)

Back to databases...

Remember our little database example?  We had something like this:
>>> people = [
    {"name": "Sally", "gender": "F", "birth_year": 1972, "height": 175},
    {"name": "Ido", "gender": "M", "birth_year": 1923, "height": 143},
    {"name": "Fred", "gender": "M", "birth_year": 2010, "height": 63},
    {"name": "Molly", "gender": "F", "birth_year": 1948, "height": 163},
    ]

Functions can be very useful when dealing with such structured data.  For instance, one can imagine a function that you can call, giving the people information as input, and it returning the first record it finds with a given name:

>>> rec = find_first_with_name(people, "Fred")
>>> print rec["height"]
63

Can you figure out how to write such a function?

You can see that it takes in two input parameters: the people database, as well as a name that it uses as comparison.  It also has one return value: the record found in the database (probably None if none found)...
Assignment 5.9: try and figure out how to write such a function! :)
If you couldn't figure out, no worries, here's one way to do this:
Do you see how that works?  the names of the input parameters are completely arbitrary...  You can change them from "ppl" and "name" to "a" and "b" and it would exactly the same (assuming you change the references to them in the function's body as well...
Assignment 5.10: rewrite the above function to work with input parameters "a" and "b", just because you can.
Now something similar:
Assignment 5.11: write a function, call it foofoo that takes in the people database as well as an age, and returns the first person that is at least that old... You can assume that today is 2013, or whatever year it is today... :)
But I can think of something even more useful than that!  How about instead of returning the first person to match some criteria, make some function that returns a list of people that match the criteria!  Can you think of how to do that?  Remember from the previous lesson that you can append things to lists.........
Assignment 5.12: write a function, call it find_gender, that takes in the people database as well as a gender ('M' or 'F'), and returns all the people that match.
Were you able to figure it out?  If not, I'll give you a little hint, ready? One way to do this is to start out with an empty list, then go through the input parameter that contains the people database, appending to that list all the items that match.  Good luck!

BTW, when you call this find_gender() of yours, what do you get as a result?  Interestingly enough, you get another people database, but smaller!  Ones that contain only the people that match.  For instance you can do something like this:

>>> males = find_gender(people, 'M')
>>> print males

You should see only guys here.  Sad, but such is life.  Then you can do something like this:
>>> blue = find_gender(males, 'F')
>>> print blue

What do you expect blue to contain?  Notice that when you call it, you're passing to it not the people database, but the males database.  Try it!  Did what you get make sense?

While this example wasn't very fascinating, you can make things more complicated:
Assignment 5.13: write a function, call it find_older_than, that takes in the people database as well as an age, as returns a list that contains all the matching people.
Were you able to do it?  Not hugely different than find_gender(), wasn't it?  But now, you can mix and match them together!  Say you wanted to find all the females that are older than 50.  Can you figure out how to do that?
Assignment 5.14: take advantage of both find_gender() as well as find_older_than() that you wrote in order to find all females older than 50.
Pretty neat, huh?

Do you see where I'm going with this?  You can make functions that solve a particular problem, and then put them together in order to solve more complicated problems.  What makes this concept even more powerful is that this also enables you to unit test each individual function separately, make sure that it's working well, and then unit test the combination of the functions.  But at that point, you already built up some confidence that at the very least the building blocks are solid, and you can concentrate on testing that their combination, or in tech speak, "integration", is correct.

Ok, let's give you a little more practice... Remember the cars database?
>>> cars = [
    {"make": "toyota", "year": 1997, "color": "blue", "miles": 129732},
    {"make": "ford", "year": 2003, "color": "green", "miles": 83832},
    {"make": "toyota", "year": 2007, "color": "green", "miles": 27212}
    ]

Now you do! :)
Assignment 5.15: write a function that given a list of cars, returns the average number of miles on them.
Assignment 5.16: write a function that given a list of cars and a year, returns a list of all the cars that are that year or newer.
Of course, as you write these functions, you can change the cars list, that is, go ahead and add more cars to it!  Here's a little challenge:
Assignment 5.17: write a function that given a list of cars, returns a list of all the available colors.  Make sure that each color only appears once!
Assignment 5.18: write a function that given a list of cars, returns a list of all the available makes?  Again, make sure that each make only appears once!
Did you notice that the above two assignments are rather similar?  Can you think of some sort of "helper" function that you can write, that would simplify the above two solutions?  Is there some sort of mundane, repetitive task that you're doing in both functions that you can sort of "take out" and put into a third?  Then call it from the two assignments?

Well, did you come up with any ideas?  Guess what?! I have one!! How about writing a function that appends an item into a list only if it's not in there already? Keep references in mind, and this becomes a lot simpler.
Assignment 5.19: write a function, call it append_unique, that appends an item to a given list only if it's not there already.
Assignment 5.20: modify the functions that you wrote in assignments 5.17 and 5.18 above to use the append_unique function that you just wrote!  Neat, huh?  That's breaking down a problem for ya! 
 Now, if you haven't already done so (to satisfy your curiosity, of course), put some of these functions together:
Assignment 5.21: write a function that given a list of cars, returns all the colors that are available for cars made on or after the year 2000.
Very nice!  :)

A little Python treat ...

Python has an elegant way to check to see if a list contains a particular item:
>>> x = [2, 5, 3]
>>> a = 7 in x
>>> b = 5 in x
>>> print a, b

Thus you can do something like this:


You like? You can use the same approach to check to see if a map contains a particular key:
>>> y = {"hi": 3, "there": 5}
>>> a= "hi" in y
>>> b = "what" in y
>>> c = 3 in y
>>> print a, b, c

Notice that c is False, because it's in there as a value, and not as a key.
Assignment 5.22: modify append_unique() to use the "in" keyword instead of the for loop that you probably wrote.  :)

Functions as tools for unit testing

Functions are great for unit testing your code.  One can write a simple function to help with testing like so:

>>> def TestSomething(someInput, expectedOutput): ...

You can then run it with many different inputs and expected outputs and make sure that what you want works well.  A slightly better (more generic) way to do this is as follows:

Do you see how this lets you test a function (in this case, Max?) This is just one simple example, and you can definitely create more sophisticated testing tools, it all depends on your needs. :)

I bet you couldn't wait for some more Minesweeper! :)

Functions let us take our Minesweeper code to a whole new level! Let's start with the more straightforward things that you can do:

  • Can you write a function, call it IsCellLost(), that takes in a Minesweeper cell as an input parameter, and returns True if a player had clicked on this cell, and this cell has a mine?
  • How about a function, IsCellTowardsWin(), that returns True if the player had clicked on this cell, and it has no mine?
  • Of course, how about the function IsGameLost(), that takes a Minesweeper grid, and returns True if any of the cells containing a mine were clicked.  Of course, you can use the function IsCellLost()  to help you out.
  • And you saw this coming, didn't you: write a function, IsGameWon(), that takes a Minesweeper grid, and returns True if the game is won.  That is, all the cells that don't have a mine have been clicked, and none of the ones that do have a mine have been clicked.  If course, you can use the function IsCellTowardsWin() to help you out!
  • To help you out / debug your code, you can write helper functions, such as PrintCell() and PrintGrid() which prints a given cell / grid to the screen.
Of course, remember to unit test your code!!! :)

Now, for a little challenge: I want you to create a function, call it GenerateEmptyGrid(), that takes in the number of rows and columns and returns a new grid with no mines, and no cells have been flagged or clicked.  For instance, calling:
>>> myGrid = GenerateEmptyGrid(4, 5)

Should return a new Minesweeper grid that has 4 rows and 5 columns.  That, and nothing else. :)  Remember to watch out for references here—you want to make sure that every cell in your grid is different from one another (that is, has a different ID from each other.)  A little hint here: you will probably want to use Python's built-in function, range(), that I briefly mentioned a little earlier.

Now the fun part.  Write another function, call it AddMinesToGrid(), that takes in a previously created grid, and a probability that any given cell inside it should contain a mine.  It should then modify that grid by adding mines accordingly.  Remember that variables are passed into functions by reference, it should help you out here.  For instance, calling:
>>> AddMinesToGrid(myGrid, 0.3)

Should place a few mines randomly inside it, with a 30% chance of any given cell containing a mine.  Thus, the closer the prob is to 0, one should expect fewer mines, and the closer the prob is to 1, one should expect to see more mines.  BUT! You ask: how the heck do I do random stuff in Python?  Ah-Ha!  Don't worry, Python has a neat little library (more about libraries later) that let you do random stuff.  And, it's really easy to use.  All you have to do, is at the very start of your program file (before you define functions, and do stuff), have this line of code:
>>> import random

I'll go into what it does later, but basically, it tells Python that you want to use the "random" library.
After you do the import, you can generate random numbers anywhere in the code (including inside loop or functions or wherever) simply by calling this strange-looking function:

>>> x = random.random()
>>> print x

Basically, this tells Python that you want to call the "random" function inside the "random" library.  Pretty straight forward, I would say.

Now, what does random.random() return?  Incredibly enough, it returns a random number (sort of random, actually, but more on this later, but good enough for our purposes here).  And amazingly enough, this number is between 0 and 1!  Wow!  Can you figure out how to use it in order to determine whether any cell in the given grid should be given a mine?  Think about it.  :) 

After implementing AddMinesToGrid(), there are a couple of other functions that you may want to write:

  • FlagACell(): given a grid, and the coordinates of a particular cell within it, mark it as "flagged"
  • ClickOnACell(): mark a particular cell as clicked...
Whew!  I hope you enjoyed this as much as I have.... :)

Have you noticed, btw, that now you have a lot of the tools needed to play a fake game of Minesweeper?  That is, you can create an empty grid, populate it with random mines, and start clicking around, checking to see whether you won (or lost) the game?  A few key functions are still missing, can you think of what they are?  Don't write them just yet, just think about what additional functionality is missing....  Unless you really want to write them, of course... :)

We're done for now!

I hope you enjoyed this little introduction to functions.  In the next lesson, I'll give you more practice with functions, but I'll also take you into the realm of the infinite....  

Come prepared.
:)

No comments:

Post a Comment