<jupyter_start><jupyter_text>Chapter 3: Functions***__Contents__- [Function calls](Function-calls)- [Type conversion functions](Type-conversion-functions)- [Math functions](Math-functions)- [Composition](Composition)- [Adding new functions](Adding-new-functions)- [Definitions and uses](Definitions-and-uses)- [Flow of execution](Flow-of-execution)- [Parameters and arguments](Parameters-and-arguments)- [Variables and parameters are local](Variables-and-parameters-are-local)- [Stack diagrams](Stack-diagrams)- [Fruitful functions and void functions](Fruitful-functions-and-void-functions)- [Why functions](Why-functions)- [Debugging](Debugging)- [Exercises](Exercises)***_This notebook is based on "Think Python, 2Ed" by <NAME>_ [https://greenteapress.com/wp/think-python-2e/](https://greenteapress.com/wp/think-python-2e/)***  Function calls- A __function__ is a named sequence of statements that performs a computation- To use a function, write a statement including a __function call__<jupyter_code>type( 32 )<jupyter_output><empty_output><jupyter_text>- The name of this function is `type`- It is built in to the Python language- The expression in the parenthesis is called the function's __argument__- The function returns the __datatype__ of the argument, in this case an `int`, which is short for integer  Type conversion functions- Datatypes are very important in programming languages- A value's type dictates how it can be used- Python has functions that convert a value from one datatype to another- The two basic numeric datatypes are:    - `int` For integer data    - `float` For data with fractional parts expressed with a decimal point<jupyter_code># Convert an int to a float
float( 42 )
# Convert a float to an int
int( 3.14159 )<jupyter_output><empty_output><jupyter_text>- Note that converting from a `float` to an `int` can result in the loss of data- Numeric data is __atomic__- This means you can't split it up into different parts- However, `string` data is different<jupyter_code># Convert an int to a string
str( 42 )
# Convert a float to a string
str( 3.14159 )<jupyter_output><empty_output><jupyter_text>- In these examples, numeric data has been converted to strings- The string `'42'` consists of two characters: `4` and `2`- We read these characters as representing numbers, but the computer sees them as different- Note that the strings are indicated using single quotes `''`- To use a `string` in a mathematical expression, it must be converted to a numeric datatype- If you don't, Python will halt execution and generate an error<jupyter_code>int( '42' )<jupyter_output><empty_output><jupyter_text>Math functions- Beyond builtin functions, Python has a number of __modules__ that provide additional functionality- Think of modules like a library of related functions and variables- Most of the important math functions can be found in the `math` module- Before you can use it, you need to `import` it into your code<jupyter_code>import math<jupyter_output><empty_output><jupyter_text>- To access a function or variable defined in the module, you need to specify the name of the module and function using __dot notation__- Think of this as using an area code to dial a phone number- There could be multiple functions with the same name, but the module name tells Python where to find the one you want<jupyter_code>radians = math.pi / 2
height = math.sin( radians )<jupyter_output><empty_output><jupyter_text>- You can find all the functions and variables in the module in the Python documentation - As an example, let's write code to calculate the circumference of a circle $c = 2 r \pi$<jupyter_code># Define the radius
radius = 3
# Calculate the circumference
circumference = 2 * radius * math.pi
print( circumference )<jupyter_output>18.84955592153876<jupyter_text>Composition- Building solutions to large problems using solutions to small problems as a building block is a fundamental skill in thinking like a computer science- The process of combining building blocks is called __composition__- Almost anywhere you can put a _value_ in Python, you can put an _expression_- The exception is that only a variable can go on the left of an assignment statement  Adding new functions- You can create your own functions using a __function definition__<jupyter_code>def print_star_wars_greeting():
    print( 'May the force be with you' )
    print( 'And also with you' )<jupyter_output><empty_output><jupyter_text>- `def` is a keyword that tells Python you are defininig a function- The name of the function immediately follows `def`- The naming rules for functions are the same as for variables- A verb is usually in a function name since it _does_ something- For the moment, leave the parentheses empty- We will add __parameters__ later- The __header__ is the first line of the definition- The __body__ is the rest of the function and is indicated by the `:`- Python uses the indentation to know where the body of the function starts and stops- The indentation is 4 spaces, not a `TAB`- Defining functions actually creates a variable with the name of the function and a value referred to as a __function object__- The function isn't executed until you call it in a __function call_<jupyter_code>print_star_wars_greeting()<jupyter_output>May the force be with you
And also with you<jupyter_text>- Once you define a function, you can use it in later functions<jupyter_code>def print_we_will():
    print( 'We will' )

def print_lyrics():
    print_we_will()
    print_we_will()
    print( 'Rock you' )

print_lyrics()<jupyter_output>We will
We will
Rock you<jupyter_text>Definitions and uses- Function definitions are executed like other statements, but they just create function objects- These store the statements in the body so they can be executed when the function is called- What happens if we try to call a function before it is defined?  - Look back at the previous example  - Move `print_lyrics()` to the top  - Now, move `print_we_will()` in between the other two functions- What is it about Python that might cause this behavior?- You have to create a function before you can execute it  Flow of execution- The order in which statements are executed is called __flow of execution__- Execution starts at the top- Statements are executed one at a time- Think of a function as a detour to another part of code- When it is done, it comes right back to where it left- Don’t forget that functions can call other functions though  Parameters and arguments- Sometimes functions need additional information in order to execute properly- We have seen builtin functions using __arguments__- A good example was `math.sin( radians )`- You can do it with user-defined functions as well- Inside the function, arguments are referred to as __parameters__<jupyter_code>def greet( name ):
    print( 'Hello', name )
    print( 'How are you doing?' )
greet( '<NAME>' )
robot = 'Vincent'
greet( robot )<jupyter_output>Hello <NAME>
How are you doing?
Hello Vincent
How are you doing?<jupyter_text>- In this example, `name` is the function's parameter- When the function is called the value of the parameter is set to the value of the argument- The name of the argument variable has nothing to do with the name of the parameter- In fact, you can call the function using an expression<jupyter_code>greet( 2 * 'Yo ' + 'Ma' )<jupyter_output>Hello Yo Yo Ma
How are you doing?<jupyter_text>Variables and parameters are local- Variables created in a function are __local__ to that function- This means that they only exist while that function is executing- They can't be accessed from outside the function and when the function ends, they are thrown away- For example, if we try to use the variable `z` outside of the function, Python will fail to compile the code and generate an error<jupyter_code>def do_something( x, y ):
    z = x * y
    print( 'z=[' + str(z) + ']' )

do_something( 3, 4 )
# print( z ) # Fails since z doesn't exist<jupyter_output>z=[12]<jupyter_text>- Note that parameters are local as well- `x` and `y` don't exist outside of the function<jupyter_code>def do_something_else( x, y ):
    x = x + 2
    y = y - 1
    z = x * y
    print( 'Inside: x=[' + str( x ) + ']' )
    print( 'Inside: y=[' + str( y ) + ']' )
    print( 'Inside: z=[' + str(z) + ']' )

x = 3
y = 4
do_something_else( x, y )
print( 'Outside: x=[' + str( x ) + ']' )
print( 'Outside: y=[' + str( y ) + ']' )<jupyter_output>Inside: x=[5]
Inside: y=[3]
Inside: z=[15]
Outside: x=[3]
Outside: y=[4]<jupyter_text>- Even if the variables share the same name, they are different variables- Think of this like telephone numbers and area codes.  The same number can exist as a local number in different area codes.  Stack diagrams- __TODO__ _Insert an image_- Sometimes it is useful to visualize the execution of a program using a __stack diagram__- Each function is represented by a __frame__- The frame holds all the important information about a function's execution, like the variables and the current line of execution- The currently executing function is placed on the bottom- Whenever you encounter a runtime error, Python will display an error detailing the current line of code and the function in which it resides- It will also list the line of code that called that function and the function in which it resides- This continues until it gets the main entry point of the program `__main__`- This is called a __stack trace__ (or sometimes even a __trace back__)- To visualize the execution of the previous code that prints song lyrics, use this website   Fruitful functions and void functions- We have already used functions that return a value- The book refers to these as __fruitful functions__- Other functions that don't return anything are called __void functions__- When a function returns something, it is rare that you want to ignore it- If you don't use the value, it is lost<jupyter_code>def calculate_area_circle( radius ):
    return math.pi * radius ** 2

def calculate_area_donut( radius_outer, radius_inner ):
    area_outer = calculate_area_circle( radius_outer )
    area_inner = calculate_area_circle( radius_inner )
    return area_outer - area_inner


print( calculate_area_donut( 3, 2 ) )<jupyter_output>15.707963267948966<jupyter_text>- If you try to save the return value of a void function, the variable will have a value of `None`- This isn't the same as the string `'None'`  Why functionsThere are a number of benefits of functions:- Creating a new function gives you an opportunity to name a group of statements, which makes your program easier to read and debug.- Functions can make a program smaller by eliminating repetitive code. Later, if you make a change, you only have to make it in one place.- Dividing a long program into functions allows you to debug the parts one at a time and then assemble them into a working whole.- Well-designed functions are often useful for many programs. Once you write and debug one, you can reuse it.  Debugging- Don’t use both tabs and spaces in your scripts/programs- Configure your editor to insert spaces when you hit the tab key- Don’t forget to save your script/program before you try to run it- It is frequently helpful to insert debugging statements in your code- Mine frequently look like the following<jupyter_code>x = 55
print( 'x=[' + str( x ) + ']' )<jupyter_output>x=[55]<jupyter_text>*** Exercises- Write a function that takes a radius of a circle as an argument and returns (not prints) the circumference of the circle.  Write a second function that also takes a raidus of a circle as an argument and returns the area of the circle.  Calculate, by hand, the correct answers for circles with radii of 2, 3, and 4.  Test the functions with these values and check for correctness.<jupyter_code>def calc_circle_circumference( radius ):
    # INSERT YOUR CODE HERE
    return 0

def calc_circle_area( radius ):
    # INSERT YOUR CODE HERE
    return 0<jupyter_output><empty_output><jupyter_text>- Write a Python function that converts three (3) different Fahrenheit temperatures to Celsius temperatures.  Create two versions of the function:  1. Create a version that prints the converted temperature.  1. Create a version that returns the converted temperature.<jupyter_code>def convert_fahrenheit_to_celsius_print( f_temp ):
    # INSERT YOUR CODE HERE
    print( '0' )

def convert_fahrenheit_to_celsius_return( f_temp ):
    # INSERT YOUR CODE HERE
    return 0<jupyter_output><empty_output><jupyter_text>- Bob wants to know how much he should charge for his app on the App store.  He ran two price trials (results below) to gauge interest.  He also has some costs associated with selling his app (listed below).  Write a function called ``calculate_profit`` that takes a proposed price for his app and returns the predicted profit earned.  You may assume a simple linear relationship between the purchase price and the number of purchases.| Purchase price | Number of purchases ||---------------:|--------------------:|| \$2.00 | 1,000 || \$3.00 | 700 || Known costs |  ||:---|---:|| Refund rate | 5\% || Transaction cost | \$0.50 |<jupyter_code># Function to calculate the profit
def calculate_profit( app_price ):
    # ????

    # Profit!
    return 0<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07148775894538606, 'fraction_numerical': 0.008738229755178907, 'mean_word_length': 4.571128829206882, 'pattern_counts': {'":': 0, '<': 71, '<?xml version=': 0, '>': 71, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '382902', 'license': 'MIT', 'n_tokens_mistral': 3708, 'n_tokens_neox': 3503, 'n_words': 1854, 'path': 'CSNE2444-Intro-to-CS-I/jupyter-notebooks/ch03-functions.ipynb', 'repo_name': 'snucsne/CSNE-Course-Source-Code', 'size': '13290'}