Skip to content

Latest commit

 

History

History
882 lines (572 loc) · 23.3 KB

10.Functions.md

File metadata and controls

882 lines (572 loc) · 23.3 KB

Lesson 10: Functions

"Functionality is the heart of programming, functions are the veins."

Content

  1. Introduction to Functions
  2. Parameters and Arguments
  3. Positional vs Key Arguments
  4. Scopes
  5. Return
  6. Optional Parameters
  7. Args and Kwargs
  8. Argument Ordering
  9. Quiz
  10. Homework

1. Introduction to Functions

1.1 What is a function?

A function in programming is a self-contained, reusable block of code which acts as a mini-program within a larger program.

Functions allow you to segment your code into modular, manageable pieces, thereby enhancing readability, simplifying debugging and improving coding experience overall.

1.2 Real world examples

Let's say we have a complex repetitive task, baking cakes. And let's say that in order to bake a singular cake we have to run this code:

print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")

So if we had to bake 4 cakes in different times, and we had to do it without using functions, our code would look like this:

print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")

print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")

print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")

print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")

Let's write the same code, but with functions this time.

1.3 Syntax

In Python functions are defined by def keyword followed by the name of the function , then curly brackets () and semicolon :.

NOTE: Take a look at indentation, in case it's wrong the Pyhton interpreter will not be able to compile the code.

Example

def print_cake_recipe():
    print("1. Preheat the oven to 350°")
    print("2. Mix flour, sugar, and eggs.")
    print("3. Bake for 30 minutes.")
    print("4. Let the cake cool and serve.")
    print("\n")


print_cake_recipe() # we call the function in order to execute code inside it
print_cake_recipe()
print_cake_recipe()
print_cake_recipe()

Explanation

We created a function print_cake_recipe() and then called it 4 times, lets see what happens when we run it.

Output

1. Preheat the oven to 350°
2. Mix flour, sugar, and eggs.
3. Bake for 30 minutes.
4. Let the cake cool and serve.

1. Preheat the oven to 350°
2. Mix flour, sugar, and eggs.
3. Bake for 30 minutes.
4. Let the cake cool and serve.

1. Preheat the oven to 350°
2. Mix flour, sugar, and eggs.
3. Bake for 30 minutes.
4. Let the cake cool and serve.

1. Preheat the oven to 350°
2. Mix flour, sugar, and eggs.
3. Bake for 30 minutes.
4. Let the cake cool and serve.

Now you can see that it is a very convenient way to write the programs, as you can collect chunks of your code into functions and call them every time you need.

NOTE: If we try to assign function value to a variable, it will be None, more on this in "10.5 Return".

2. Parameters and Arguments

Often you need to work with dynamic values within your function and the optimal approach to this challenge would be implementing parameters.

2.1 Syntax

Objective: Write an addition function that would take two values and print the sum of these two values. Here is an example of how this code would look like:

Example

def addition(a, b):
    print(a + b)

addition(1, 3)

Output

4

Explanation

Define function addition and in curly brackets we declared the parameters: a and b.

Then we called the function and passed arguments 1 and 3, as a and b accordingly

2.2 Parameters vs Arguments

As irrelevant as it might seem, there is a difference between these two key terms. The function parameters are the names listed in the function's definition and the function arguments are the real values passed to the function.

You just need to understand that once you pass value 11 to the parameter a into the function, it becomes and argument.

3. Positional vs Key Arguments

When calling functions in Python, the arguments you pass can be either positional or keyword arguments. Understanding the difference and the proper usage of each type is crucial for writing clear and error-free code.

3.1 Positional Arguments

Positional arguments are arguments that need to be included in the correct order. The order in which you pass the values when calling the function should match the order in which the parameters were defined in the function.

Example

def create_profile(name, age, profession):
    print(f"Name: {name}, Age: {age}, Profession: {profession}")

create_profile("Alice", 30, "Engineer")

Output

Name: Alice, Age: 30, Profession: Engineer

Explanation

In this example, "Alice" is passed as the name, 30 as the age, and "Engineer" as the profession, following the order they were defined in the function.

We did it sequentially, so that params are in the same order. Never mix the order of your arguments, it can blow out your code!

3.2 Keyword Arguments

Keyword arguments, on the other hand, are arguments passed to a function, accompanied by an identifier.

You explicitly state which parameter you're passing the argument to by using the name of the parameter. This means the order of the arguments does not matter, as long as all required parameters are provided.

Example

def create_profile(name, age, profession):
    print(f"Name: {name}, Age: {age}, Profession: {profession}")

create_profile(age=30, profession="Engineer", name="Alice")

Output

Name: Alice, Age: 30, Profession: Engineer

Explanation

Here, even though the order of arguments is different from the order of parameters in the function definition, Python knows which argument corresponds to which parameter, thanks to the keyword parameters.

Personally I prefer the following approach, following it, your codebase becomes much cleaner.

4. Scopes

The scope of a variable refers to the context in which it is visible or accessible in the code. In Python, scopes help manage and isolate variables in different parts of the program, ensuring that variable names don't clash and create unexpected behaviors.

The two main types of scopes are local and global.

4.1 Local scope

Variables declared within a function have local scope, they are created when function is called and destroyed when it finishes its execution. Local variables declared in a functions can only be accessed from within this function.

Example

def addition():
    suma = 10 + 15
    print(suma)

addition()

Output

25

Example

def addition():
    summ = 10 + 15
    print(summ)

print(summ)

Output

NameError: name 'summ' is not defined.

Explanation

In this case we declare variable suma and since we created it inside the function it has local scope, therefore we wouldn't be able to call it from outside:

4.2 Global scope

Variables declared outside all the functions have a global scope. They can be accessed from any part of the code, including inside functions, unless overshadowed by a local variable with the same name.

Example

total = 50

def print_added_total():
    print(total + 50)

print_added_total()

Output

100

Explanation

Here, we declared variable outside the function, therefore it has global scope and can be accessed and modified from wherever one might need.

However, if we attempt to reassign a different value to the global variable from within the function the program will not work as intended.

Example

total = 50

def update_total():
    total += 50

update_total()

Output

UnboundLocalError: local variable 'total' referenced before assignment

This happens because you can't reassign value to a variable, unless you use keyword global, however it is strongly advised not to use it, as this will introduce redundant complexity to your code, making it less clear and more bug prone.

Here it is important to understand the difference between reassigning a value and modifying an object as if we attempt to append an element to a list declared with global scope it won't cause any problems.

Example

list1 = ["a", "b"]

def update_list():
    list1.append("c")

    
update_list()
print(list1)

Output

['a', 'b', 'c']

4.3 Best practices

  1. Use Few Global Variables: Try to use global variables (those outside functions) as little as possible to avoid confusion.

  2. Keep Variables Local: Use variables inside functions for things that only matter in that function. This helps keep your code clean and easy to understand.

  3. Be Careful with Same Names: If you use the same name for a variable inside and outside a function, the function will only know about the inside one.

5. Return

The return statement in a function sends a value from within the function's local scope to where the function has been called. It's a powerful way to pass data out of a function and can be used to send any type of object back to the caller.

5.1 Syntax

The return statement is followed by the value or expression you want to return. If no value or expression is specified, the function will return None.

Example

def addition(a, b):
    return a + b

sum_of_two_numbers = addition(1, 3)
print(sum_of_two_numbers)

Output

4

In this example, the addition function takes two positional arguments a and b and returns their sum returning the value to sum_of_two_numbers variable.

Note: Any operation after the return statement will not be executed.

Example

def addition(a, b):
    return a + b
    print("this message will never be output")


sum_of_two_numbers = addition(1, 3)
print(sum_of_two_numbers)

Output

4

6. Optional Parameters

In Python functions can be called with a varying number of parameters. This feature enhances flexibility and usability of the code depending on the scenario. Optional parameters have default values, which are used if no argument is passed during the function call.

6.1 Benefits of Optional Parameters

Optional parameters make your functions more flexible. They allow you to create more generalized functions that can handle a wider range of inputs.

Thanks to optional parameters you will be able to use the same function for slightly different purposes without overloading it with arguments or creating multiple, nearly identical functions.

6.2 Syntax

To define an optional parameter, you assign it a default value in the function's definition using operator =. This default value is used if the caller does not provide a value for that parameter.

Example

def greet(name, message="Hello"):
    print(f"{message}, {name}!")


greet("Alice")
greet("Bob", "Good morning")

Output

Hello, Alice!
Good morning, Bob!

Explanation

In the greet function above, name is a mandatory parameter, while message is optional with a default value of "Hello". If no message is provided when the function is called, it uses the default value.

Important: You can only assign default values to parameters After you have declared your positional arguments, more on this in args/kwargs section

7. Args and Kwargs

In Python, *args and **kwargs are special operators used in function definitions. They allow a function to accept a variable number of arguments, making your functions more flexible. *args is used for positional arguments, while **kwargs is used for keyword arguments.

7.1 *args

The *args parameter allows a function to take any number of positional arguments without having to define each one individually.

Note The arguments passed to *args are accessible as a tuple. Therefore, contents of args can not and should not be altered in traditional ways.

Example

def calculate_sum(*numbers):
    total = sum(numbers)
    return total


print(calculate_sum(10, 20, 30))  # Output: 60

Explanation

In this example, calculate_sum() can take any number of numerical arguments, sum them up, and return the total. The *numbers parameter collects all the positional arguments into a tuple called numbers.

7.1.1 Unpacking parameters

As we learned previously in data types, you can use * to unpack the elements of an iterable, it is relevant as if you will try to pass an iterable to a function without unpacking it, it will be treated as a whole object.

NOTE: Same applies for **kwargs

Example

list1 = ["a", "b", "c"]


def example(*args):
    for argument in args:
        print(argument)

        
example(list1)

Output

['a', 'b', 'c']

Therefore, if you want to pass all the elements of list1 we will need to unpack them first.

Example

list1 = ["a", "b", "c"]


def example(*args):
    for argument in args:
        print(argument)

        
example(*list1)

Output

a
b
c

7.2 **kwargs

The **kwargs parameter allows a function to accept any number of keyword arguments. This is useful when you want to handle named arguments in your function. The keyword arguments passed to **kwargs are stored in a dictionary.

Example

def student_info(**details):
    for key, value in details.items():
        print(f"{key}: {value}")


student_info(name="John", grade="A", subject="Mathematics")

Output:

name: John
grade: A
subject: Mathematics

Explanation

In this example, student_info() can accept any number of keyword arguments. The **details parameter collects all the keyword arguments into a dictionary called details.

8. Argument Ordering

When defining a function, it's important to follow the correct order of parameters to avoid syntax errors. The order should be:

  1. Standard arguments
  2. *args
  3. **kwargs

This order ensures that your function can handle a mix of standard, positional, and keyword arguments effectively.

Example of Correct Syntax

def mix_and_match(a, b, *args, **kwargs):
    pass  # Function implementation

Example

Incorrect Syntax - Will Cause an Error!

def mix_and_match(a, b, **kwargs, *args):
    pass

Output:

SyntaxError: invalid syntax

9. Quiz

Question 1:

What will the output be for the following function call?

def greet(name):
    return "Hello, " + name

print(greet("Alice"))

A) Hello, Alice
B) Hello,
C) Alice
D) It will raise an error.


Question 2:

Given this coffee-making function, what will the following function call output?

def make_coffee(size="Medium", type="Cappuccino"):
    return f"Making a {size} {type} coffee."

print(make_coffee("Large"))

A) Making a Large Cappuccino coffee.
B) Making a Large coffee.
C) Making a Medium Cappuccino coffee.
D) It will raise an error.


Question 3:

What does the *args parameter in a function allow you to do?

A) It allows the function to accept any number of keyword arguments.
B) It allows the function to accept a list of arguments.
C) It allows the function to accept any number of positional arguments.
D) It unpacks the arguments passed to the function.


Question 4:

What will be the output of the following code?

def user_profile(**details):
    return details.get("name", "Anonymous") + " - " + details.get("role", "Guest")

print(user_profile(name="John", age=30, role="Admin"))

A) John - 30
B) John - Admin
C) Anonymous - Guest
D) It will raise an error.


Question 5:

Consider this function. What is true about the return statement in this function?

def add_numbers(a, b):
    result = a + b
    return result
    print("Calculation has been completed")

A) It outputs the result of the function.
B) It stops the function's execution and returns the result.
C) It prints the result before ending the function.
D) It is optional and can be omitted.


Question 6:

Given this code, what will the output be?

def calculate_difference(a, b):
    result = a - b
    return result

calculate_difference(10, 5)
print(result)

A) 5
B) 10
C) result
D) It will raise a NameError.


Question 7:

Consider the function and its call below. What will the output be?

def display_info(name, age):
    return f"Name: {name}, Age: {age}"

print(display_info(age=25, name="Emma"))

A) Name: Emma, Age: 25
B) Name: 25, Age: Emma
C) Name: name, Age: age
D) It will raise an error.


Question 8:

In the function definition below, which parameters are considered optional?

def func(a, b=5, c=10):
    return

A) Only a
B) Both b and c
C) Only b
D) All a, b, and c


Question 9:

Which of the following is the correct way to define a function with all types of arguments?

A) def func(*args, a, b, **kwargs):
B) def func(a, *args, b, **kwargs):
C) def func(a, b, *args, **kwargs):
D) def func(**kwargs, *args, a, b):


Question 10:

Given the function and call below, what will the function return?

def multiply_numbers(*args):
    result = 1
    for number in args:
        result *= number
    return result

print(multiply_numbers(2, 3, 4))

A) 24
B) 9
C) 6
D) It will raise an error.


Question 11:

What will be the output of the following code?

x = 10

def print_number():
    x = 5
    print("Inside function:", x)

print_number()
print("Outside function:", x)

A) Inside function: 5, Outside function: 5
B) Inside function: 10, Outside function: 10
C) Inside function: 5, Outside function: 10
D) It will raise a NameError.

10. Homework

Task 1: Star Rectangle

Objective: Implement a function named draw_rectangle that outputs a rectangle made of asterisks (*). The rectangle should have a width of 7 characters and a height of 6 lines.

Requirements:

  • Use nested for loops to generate the rectangle.
  • The outer loop should iterate through the lines (height), and the inner loop should iterate through the characters (width) on each line.
  • Only the border of the rectangle should be drawn with asterisks, while spaces ( ) should fill the interior.
  • The function does not need to return anything; it should directly print the rectangle to the console.
*******
*     *
*     *
*     *
*     *
*******

Task 2: Sum of Digits

Objective: Create a function print_digit_sum that calculates and prints the sum of all digits in a given integer.

Requirements:

  • The function should accept a single integer argument, possibly negative.
  • Convert the integer to its absolute value to handle negative numbers.
  • Iterate over each digit in the number and calculate the total sum.
  • Print the result to the console. The function returns None.
print_digit_sum(1234)  # Output: 10
print_digit_sum(-567)  # Output: 18

Task 3: Find All Factors

Objective: Develop a function get_factors that returns a list of all the divisors of a given natural number.

Requirements:

  • The function should accept a single integer argument, num.
  • If num is less than 1, return an empty list to reflect the definition of natural numbers.
  • Efficiently find and return a list of all divisors of num.
  • Ensure correct functionality for both small and large values of num.
print(get_factors(28))  # Output: [1, 2, 4, 7, 14, 28]
print(get_factors(13))  # Output: [1, 13]
print(get_factors(0))   # Output: []

Task 4: Temperature Converter

Objective: Enhance the convert_temperature function to support optional parameters for conversion direction.

Requirements:

  • The function should accept one mandatory parameter for the temperature value and one optional parameter for the direction of conversion ('C' for Celsius to Fahrenheit, 'F' for Fahrenheit to Celsius).
  • Use default arguments to assume conversion from Celsius to Fahrenheit if the direction is not specified.
  • Calculate and return the converted temperature value.
print(convert_temperature(100))  # Assumes Celsius to Fahrenheit, Output: 212
print(convert_temperature(212, convert_to='C'))  # Fahrenheit to Celsius, Output: 100

Task 5: Statistics Calculator

Objective: Implement a function calculate_statistics that computes various statistical measures (mean, median, mode, range) for a dataset, based on specified options.

Requirements:

  • The function should accept an arbitrary number of positional arguments (*data) representing the dataset.
  • Accept keyword arguments (**options) to specify which statistics to calculate: mean, median, mode, range. If an option is True, calculate that statistic.
  • Return a dictionary with keys as the names of the statistics calculated and their corresponding results as values.
  • If no options are specified, calculate and return all statistics.
  • Handle edge cases such as empty datasets or datasets without a mode.
data_points = [4, 1, 2, 2, 3, 5]
print(calculate_statistics(*data_points, mean=True, range=True))
# Output: {'mean': 2.8333333333333335, 'range': 4}

print(calculate_statistics(*data_points))
# Output: {'mean': ..., 'median': ..., 'mode': ..., 'range': ...}