{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Functions\n", "\n", "In this lesson we introduce functions as a way of making blocks of code for a specific task that are easy to use and re-use in your programs." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## What is a function?\n", "\n", "A *{term}`function`* is a block of organized, reusable code that can make your programs more effective, easier to read, and simple to manage. You can think functions as little self-contained programs that can perform a specific task that you can use repeatedly in your code. One of the basic principles in good programming is \"do not to repeat yourself\". In other words, you should avoid having duplicate lines of code in your scripts. Functions are a good way to avoid such situations and they can save you a lot of time and effort as you don't need to tell the computer repeatedly what to do every time it does a common task, such as converting temperatures from Fahrenheit to Celsius. During the course we have already used some functions such as the `print()` command which is actually a built-in function in Python." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Anatomy of a function\n", "\n", "Let's consider the task from the first lesson when we converted temperatures from Celsius to Fahrenheit.\n", "Such an operation is a fairly common task when dealing with temperature data.\n", "Thus we might need to repeat such calculations quite often when analysing or comparing weather or climate data between the US and Europe, for example." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating a first function\n", "\n", "Let's define our first function called `celsius_to_fahr`. Figure 2.4 explains the main elements of a function." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "def celsius_to_fahr(temp):\n", " return 9 / 5 * temp + 32" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![_**Figure 2.4** An example function with annotation of its important elements._](../img/Function_anatomy-400.png)\n", "\n", "_**Figure 2.4** An example function with annotation of its important elements._\n", "\n", "The function definition opens with the keyword `def` followed by the name of the function and a list of parameter names in parentheses.\n", "The body of the function — the statements that are executed when it runs — is indented below the definition line.\n", "\n", "When we call the function, the values we pass to it are assigned to the corresponding parameter variables so that we can use them inside the function (e.g., the variable `temp` in this function example).\n", "Inside the function, we use a `return` statement to define the value that should be given back when the function is used, or called." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Calling a function\n", "\n", "Now let's try using our function.\n", "Calling our self-defined function is no different from calling any other function such as `print()`.\n", "You need to call it with its name and provide your value(s) as the required parameter(s) inside the parentheses.\n", "Here, we can define a variable `freezing_point` that is the temperature in degrees Fahrenheit we get when using our function with the temperature 0°C (the temperature at which water freezes). We can then print that value to confirm. We should get a temperature of 32°F." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "freezing_point = celsius_to_fahr(0)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The freezing point of water in Fahrenheit is: 32.0\n" ] } ], "source": [ "print(\"The freezing point of water in Fahrenheit is:\", freezing_point)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can do the same thing with the boiling point of water in degrees Celsius (100°C). Just like with other functions, we can use our new function directly within something like the `print()` function to print out the boiling point of water in degrees Fahrenheit." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The boiling point of water in Fahrenheit is: 212.0\n" ] } ], "source": [ "print(\"The boiling point of water in Fahrenheit is:\", celsius_to_fahr(100))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating another function\n", "\n", "Now that we know how to create a function to convert Celsius to Fahrenheit, let’s create another function called `kelvins_to_celsius`. We can define this just like we did with our `celsius_to_fahr()` function, noting that the Celsius temperature is just the temperature in Kelvins minus 273.15. Just to avoid confusion this time, let's call the temperature variable used in the function `temp_kelvins`." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def kelvins_to_celsius(temp_kelvins):\n", " return temp_kelvins - 273.15" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's use it in the same way as the earlier one by defining a new variable `absolute_zero` that is the Celsius temperature of 0 Kelvins. Note that we can also use the parameter name `temp_kelvins` when calling the function to explicitly state which variable values is being used. Again, let's print the result to confirm everything works." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "absolute_zero = kelvins_to_celsius(temp_kelvins=0)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Absolute zero in Celsius is: -273.15\n" ] } ], "source": [ "print(\"Absolute zero in Celsius is:\", absolute_zero)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Question 2.9\n", "\n", "Isaac Newton developed [a scale for measuring temperatures](https://en.wikipedia.org/wiki/Newton_scale) that was a precursor to the modern-day Celsius scale. In his system, water would freeze at 0 °N and boil at 33 °N (°N here indicates degrees Newton, not degrees north :D). Although it is difficult to directly convert between the two scales, if we assume that the increments of temperature change are equal between 0 °N and 33 °N we can come up with a temperature conversion equation between degrees Celsius and degrees Newton:\n", "\n", "$$\n", "\\begin{equation}\n", " \\Large\n", " T_{\\mathrm{Newton}} = T_{\\mathrm{Celsius}} * 0.33\n", "\\end{equation}\n", "$$\n", "\n", "You task here is to:\n", "\n", "- Create a new function called `celsius_to_newton` that\n", " - Has one parameter that is the temperature in degrees Celsius to be converted to degrees Newton\n", " - Returns the temperature in degrees Newton" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "tags": [ "remove_cell" ] }, "outputs": [], "source": [ "# Use this cell to enter your solution." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "tags": [ "hide-cell", "remove_book_cell" ] }, "outputs": [], "source": [ "# Solution\n", "\n", "\n", "def celsius_to_newton(temp_celsius):\n", " return temp_celsius * 0.33" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Functions within a function\n", "\n", "What about converting Kelvins to Fahrenheit?\n", "We could write out a new formula for it, but we don’t need to.\n", "Instead, we can do the conversion using the two functions we have already created and calling those from the function we are now creating. Let's create a new function `kelvins_to_fahr` that takes the temperature in Kelvins as the parameter value `temp_kelvins` and uses our `kelvins_to_celsius` and `celsius_to_fahr` functions within the new function to convert temperatures from Kelvins to degrees Fahrenheit." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def kelvins_to_fahr(temp_kelvins):\n", " temp_celsius = kelvins_to_celsius(temp_kelvins)\n", " temp_fahr = celsius_to_fahr(temp_celsius)\n", " return temp_fahr" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's use the function to calculate the temperature of absolute zero in degrees Fahrenheit. We can then print that value to the screen again." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "absolute_zero_fahr = kelvins_to_fahr(temp_kelvins=0)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Absolute zero in Fahrenheit is: -459.66999999999996\n" ] } ], "source": [ "print(\"Absolute zero in Fahrenheit is:\", absolute_zero_fahr)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Functions and variable names\n", "\n", "A common point of confusion for new programmers is understanding how variable names in functions relate to those defined elsewhere in your notebooks.\n", "When defining a function, the variable names given in the function definition exist and will only be used when the function is called.\n", "That might seem confusing, but as it turns out, this is an excellent feature in Python that can save you from much suffering.\n", "Let's try to understand this by way of an example.\n", "\n", "Let us define modified version of our `kelvins_to_celsius` function where the parameter value is still called `temp_kelvins`, but we now store the converted temperature as temp_celsius first and return that value." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def kelvins_to_celsius(temp_kelvins):\n", " temp_celsius = temp_kelvins - 273.15\n", " return temp_celsius" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, we have defined our function to accept `temp_kelvins` as its only parameter, calculate `temp_celsius`, and return that value.\n", "As you will see below, the variables defined in the function exist only in its *namespace*.\n", "Let's confirm that." ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "NameError", "evalue": "name 'temp_kelvins' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[14], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtemp_kelvins\u001b[49m\n", "\u001b[0;31mNameError\u001b[0m: name 'temp_kelvins' is not defined" ] } ], "source": [ "temp_kelvins" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "NameError", "evalue": "name 'temp_celsius' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[15], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtemp_celsius\u001b[49m\n", "\u001b[0;31mNameError\u001b[0m: name 'temp_celsius' is not defined" ] } ], "source": [ "temp_celsius" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, in the *global namespace* we get a `NameError` when trying to access the variables `temp_kelvins` or `temp_celsius` because they have only been defined within the `kelvins_to_celsius()` function.\n", "\n", "Perhaps, however, you are thinking that we have not yet called the function, so that is why we get a `NameError`. Maybe if we use the function, then these variable values will be defined.\n", "Let's try that." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "20.0" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "kelvins_to_celsius(temp_kelvins=293.15)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "NameError", "evalue": "name 'temp_kelvins' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[17], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtemp_kelvins\u001b[49m\n", "\u001b[0;31mNameError\u001b[0m: name 'temp_kelvins' is not defined" ] } ], "source": [ "temp_kelvins" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see `temp_kelvins` is still not defined in the global namespace, where values such as `freezing_point` have been defined.\n", "\n", "Why does Python work this way?\n", "Well, as it turns out, the benefit of having a separate namespace for functions is that we can define a variable in the global namespace, such as `temp_kelvins` and not need to worry about its name within a function, or the use of a function changing its value.\n", "Inside the function, the value that is passed will be known as `temp_kelvins`, but modifying that value will not alter a variable of the same name in the global namespace.\n", "Let's have a look at another example using a modified `kelvins_to_celsius()` function we can call `kelvins_to_celsius2()`." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "def kelvins_to_celsius2(temperature):\n", " temperature = temperature - 273.15\n", " return temperature" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, we pass in a value as `temperature` and modify the value that is passed in before returning it.\n", "This is probably not a good idea in most cases because it could cause confusion, but it is perfectly valid code.\n", "\n", "Let's now define a variable `temperature` in the global namespace and use our function to modify it." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "temperature = 303.15" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "30.0" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "kelvins_to_celsius2(temperature=temperature)" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "303.15" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "temperature" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, the value of the variable `temperature` in the global namespace was set to 303.15 and remains 303.15 after using the `kelvins_to_celsius2()` function.\n", "Although there is a variable inside that function with the same name as the value in the global namespace, using the function assigns the value of `temperature` inside the function and manipulates that value only inside the function.\n", "\n", "It is important to be aware that it is possible to access variable values that have been defined in the global namespace from within functions, even if the value has not been passed to the function.\n", "This is because Python will search for variables defined with a given name first inside the function, and then outside the function (the search domain is known as the variable's *scope*).\n", "If such a value is found then it can be used by the function, which could be dangerous!\n", "\n", "Let's look at an example of behavior in a function that may be unexpected. Here we can define a third version of the `kelvins_to_celsius()` function that we can call `kelvins_to_celsius3()`." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "def kelvins_to_celsius3(temp):\n", " temp = temperature - 273.15\n", " return temp" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "30.0" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "kelvins_to_celsius3(273.15)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You were perhaps expecting to see a value of `0` returned by `kelvins_to_celsius3()`, but that does not occur because `temp` is assigned `temperature - 273.15` in the function.\n", "Although `temperature` was not passed to `kelvins_to_celsius3()` it is defined in the global namespace and thus can be used by our example function.\n", "Since `temperature = 303.15` we get a value of 30.0 returned when using `kelvins_to_celsius3()`.\n", "Conclusion: Be careful!\n", "\n", "For those who are interested, more information about namespaces and variables scopes can be found on the [Real Python website](https://realpython.com/python-namespaces-scope/)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Documenting functions with docstrings\n", "\n", "A documentation string, or a *{term}`docstring`* is a block of text that describes what a specific function, library, or script does and how to use it. Surprise surprise, PEP 8 contains [more guidance about documentation strings](https://www.python.org/dev/peps/pep-0008/#documentation-strings) [^pep8_docstring], and docstrings even have [their own guide page](https://www.python.org/dev/peps/pep-0257/) [^pep257]. Let's look an an example from our of our functions above.\n", "\n", "```python\n", "def kelvins_to_celsius(temp_kelvins):\n", " \"\"\"Converts temperature in Kelvins to degrees Celsius.\"\"\"\n", " return temp_kelvins - 273.15\n", "```\n", "\n", "Here you can see a short bit of text explaining in simple language what this function does. In this case our function is quite simple, but the docstring still helps remove uncertainty about what it can be used to do. So, what can we see in this example?\n", "\n", "- A docstring is always the first statement in a module or a function.\n", "- Docstrings are written using `\"\"\"triple double quotation marks\"\"\"`.\n", "- Short docstrings [can be written on a single line](https://www.python.org/dev/peps/pep-0257/#one-line-docstrings) [^pep257_one_line].\n", "\n", "Seems simple enough, right? We can also provide more detailed docstrings, which can be particularly helpful when using functions with multiple parameters. Let's expand the docstring above to provide more information about this function.\n", "\n", "```python\n", "def kelvins_to_celsius(temp_kelvins):\n", " \"\"\"\n", " Converts temperature in Kelvins to degrees Celsius.\n", "\n", " Parameters\n", " ----------\n", " temp_kelvins: \n", " Temperature in Kelvins\n", "\n", " Returns\n", " -------\n", " \n", " Converted temperature.\n", " \"\"\"\n", " return temp_kelvins - 273.15\n", "```\n", "\n", "Here you can now see more information about the expected values for the parameters and what will be returned when using the function. This level of documentation is not needed for every function, but clearly it can be useful, especially when you have multiple parameters. Note here that the suggested format is to have the quotation marks on their own separate lines." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Footnotes\n", "\n", "[^pep8_docstring]: \n", "[^pep257]: \n", "[^pep257_one_line]: " ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.9" } }, "nbformat": 4, "nbformat_minor": 4 }