Functions

๐Ÿ“–
What is a Function?

A function is a reusable block of code that performs a specific task. It's like a recipe you can follow multiple times without rewriting the steps.

alt text

The DRY Principle

๐Ÿ’ก
DRY = Don't Repeat Yourself

If you're copying and pasting code, you should probably write a function instead!

Without a function (repetitive):

# Calculating area three times - notice the pattern?
area1 = 10 * 5
print(f"Area 1: {area1}")

area2 = 8 * 6
print(f"Area 2: {area2}")

area3 = 12 * 4
print(f"Area 3: {area3}")

With a function (clean):

def calculate_area(length, width):
    return length * width

print(f"Area 1: {calculate_area(10, 5)}")
print(f"Area 2: {calculate_area(8, 6)}")
print(f"Area 3: {calculate_area(12, 4)}")

Basic Function Syntax

Declaring a Function

def greet():
    print("Hello, World!")

Anatomy:

  • def โ†’ keyword to start a function
  • greet โ†’ function name (use descriptive names!)
  • () โ†’ parentheses for parameters
  • : โ†’ colon to start the body
  • Indented code โ†’ what the function does

Calling a Function

โš ๏ธ
Important

Defining a function doesn't run it! You must call it.

def greet():
    print("Hello, World!")

greet()  # Now it runs!
greet()  # You can call it multiple times

Parameters and Arguments

๐Ÿ“
Terminology

Parameters are in the definition. Arguments are the actual values you pass.

def greet(name):      # 'name' is a parameter
    print(f"Hello, {name}!")

greet("Alice")        # "Alice" is an argument

Multiple parameters:

def add_numbers(a, b):
    result = a + b
    print(f"{a} + {b} = {result}")

add_numbers(5, 3)     # Output: 5 + 3 = 8

Return Values

Functions can give back results using return:

def multiply(a, b):
    return a * b

result = multiply(4, 5)
print(result)  # 20

# Use the result directly in calculations
total = multiply(3, 7) + multiply(2, 4)  # 21 + 8 = 29
โ„น๏ธ
print() vs return

print() shows output on screen. return sends a value back so you can use it later.


Default Arguments

Give parameters default values if no argument is provided:

def power(base, exponent=2):  # exponent defaults to 2
    return base ** exponent

print(power(5))      # 25 (5ยฒ)
print(power(5, 3))   # 125 (5ยณ)

Multiple defaults:

def create_profile(name, age=18, country="USA"):
    print(f"{name}, {age} years old, from {country}")

create_profile("Alice")                    # Uses both defaults
create_profile("Bob", 25)                  # Uses country default
create_profile("Charlie", 30, "Canada")    # No defaults used
โš ๏ธ
Rule

Parameters with defaults must come after parameters without defaults!

# โŒ Wrong
def bad(a=5, b):
    pass

# โœ… Correct
def good(b, a=5):
    pass

Variable Number of Arguments

*args (Positional Arguments)

Use when you don't know how many arguments will be passed:

def sum_all(*numbers):
    total = 0
    for num in numbers:
        total += num
    return total

print(sum_all(1, 2, 3))           # 6
print(sum_all(10, 20, 30, 40))    # 100

**kwargs (Keyword Arguments)

Use for named arguments as a dictionary:

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

print_info(name="Alice", age=25, city="New York")
# Output:
# name: Alice
# age: 25
# city: New York

Combining Everything

๐Ÿ’ก
Order Matters

When combining, use this order: regular params โ†’ *args โ†’ default params โ†’ **kwargs

def flexible(required, *args, default="default", **kwargs):
    print(f"Required: {required}")
    print(f"Args: {args}")
    print(f"Default: {default}")
    print(f"Kwargs: {kwargs}")

flexible("Must have", 1, 2, 3, default="Custom", extra="value")

Scope: Local vs Global

๐Ÿ“–
Scope

Scope determines where a variable can be accessed in your code.

Local scope: Variables inside functions only exist inside that function

def calculate():
    result = 10 * 5  # Local variable
    print(result)

calculate()        # 50
print(result)      # โŒ ERROR! result doesn't exist here

Global scope: Variables outside functions can be accessed anywhere

total = 0  # Global variable

def add_to_total(amount):
    global total  # Modify the global variable
    total += amount

add_to_total(10)
print(total)  # 10
๐Ÿ’ก
Best Practice

Avoid global variables! Pass values as arguments and return results instead.

Better approach:

def add_to_total(current, amount):
    return current + amount

total = 0
total = add_to_total(total, 10)  # 10
total = add_to_total(total, 5)   # 15

Decomposition

๐Ÿ“–
Decomposition

Breaking complex problems into smaller, manageable functions. Each function should do one thing well.

Bad (one giant function):

def process_order(items, customer):
    # Calculate, discount, tax, print - all in one!
    total = sum(item['price'] for item in items)
    if total > 100:
        total *= 0.9
    total *= 1.08
    print(f"Customer: {customer}")
    print(f"Total: ${total:.2f}")

Good (decomposed):

def calculate_subtotal(items):
    return sum(item['price'] for item in items)

def apply_discount(amount):
    return amount * 0.9 if amount > 100 else amount

def add_tax(amount):
    return amount * 1.08

def print_receipt(customer, total):
    print(f"Customer: {customer}")
    print(f"Total: ${total:.2f}")

def process_order(items, customer):
    subtotal = calculate_subtotal(items)
    discounted = apply_discount(subtotal)
    final = add_tax(discounted)
    print_receipt(customer, final)

Benefits: โœ… Easier to understand โœ… Easier to test โœ… Reusable components โœ… Easier to debug


Recursion

๐Ÿ“–
Recursion

When a function calls itself to solve smaller versions of the same problem.

Classic example: Factorial (5! = 5 ร— 4 ร— 3 ร— 2 ร— 1)

def factorial(n):
    # Base case: stop condition
    if n == 0 or n == 1:
        return 1
    
    # Recursive case: call itself
    return n * factorial(n - 1)

print(factorial(5))  # 120

How it works:

factorial(5) = 5 ร— factorial(4)
             = 5 ร— (4 ร— factorial(3))
             = 5 ร— (4 ร— (3 ร— factorial(2)))
             = 5 ร— (4 ร— (3 ร— (2 ร— factorial(1))))
             = 5 ร— (4 ร— (3 ร— (2 ร— 1)))
             = 120

Key parts of recursion:

๐Ÿ“
Recursion Checklist

1. Base case: When to stop
2. Recursive case: Call itself with simpler input
3. Progress: Each call must get closer to the base case

Another example: Countdown

def countdown(n):
    if n == 0:
        print("Blast off!")
        return
    print(n)
    countdown(n - 1)

countdown(3)
# Output: 3, 2, 1, Blast off!
โš ๏ธ
Watch Out

Deep recursion can cause memory issues. Python has a default recursion limit.


Practice Exercises

๐Ÿ’ป
Exercise 1: Rectangle Printer

Write a function rectangle(m, n) that prints an m ร— n box of asterisks.

rectangle(2, 4)
# Output:
# ****
# ****
๐Ÿ’ป
Exercise 2: Add Excitement

Write add_excitement(words) that adds "!" to each string in a list.

  • Version A: Modify the original list
  • Version B: Return a new list without modifying the original
words = ["hello", "world"]
add_excitement(words)
# words is now ["hello!", "world!"]
๐Ÿ’ป
Exercise 3: Sum Digits

Write sum_digits(num) that returns the sum of all digits in a number.

sum_digits(123)   # Returns: 6 (1 + 2 + 3)
sum_digits(4567)  # Returns: 22 (4 + 5 + 6 + 7)
๐Ÿ’ป
Exercise 4: First Difference

Write first_diff(str1, str2) that returns the first position where strings differ, or -1 if identical.

first_diff("hello", "world")  # Returns: 0
first_diff("test", "tent")    # Returns: 2
first_diff("same", "same")    # Returns: -1
๐Ÿ’ป
Exercise 5: Tic-Tac-Toe

A 3ร—3 board uses: 0 = empty, 1 = X, 2 = O

  • Part A: Write a function that randomly places a 2 in an empty spot
  • Part B: Write a function that checks if someone has won (returns True/False)
๐Ÿ’ป
Exercise 6: String Matching

Write matches(str1, str2) that counts how many positions have the same character.

matches("python", "path")  # Returns: 3 (positions 0, 2, 3)
๐Ÿ’ป
Exercise 7: Find All Occurrences

Write findall(string, char) that returns a list of all positions where a character appears.

findall("hello", "l")  # Returns: [2, 3]
findall("test", "x")   # Returns: []
๐Ÿ’ป
Exercise 8: Case Swap

Write change_case(string) that swaps uppercase โ†” lowercase.

change_case("Hello World")  # Returns: "hELLO wORLD"

Challenge Exercises

โ“
Challenge 1: Merge Sorted Lists

Write merge(list1, list2) that combines two sorted lists into one sorted list.

  • Try it with .sort() method
  • Try it without using .sort()
merge([1, 3, 5], [2, 4, 6])  # Returns: [1, 2, 3, 4, 5, 6]
โ“
Challenge 2: Number to English

Write verbose(num) that converts numbers to English words (up to 10ยนโต).

verbose(123456)  
# Returns: "one hundred twenty-three thousand, four hundred fifty-six"
โ“
Challenge 3: Base 20 Conversion

Convert base 10 numbers to base 20 using letters A-T (A=0, B=1, ..., T=19).

base20(0)    # Returns: "A"
base20(20)   # Returns: "BA"
base20(39)   # Returns: "BT"
base20(400)  # Returns: "BAA"
โ“
Challenge 4: Closest Value

Write closest(L, n) that returns the largest element in L that doesn't exceed n.

closest([1, 6, 3, 9, 11], 8)  # Returns: 6
closest([5, 10, 15, 20], 12)  # Returns: 10