Docs

README

05 - Functions

📌 What You'll Learn

  • Function definition and calling
  • Parameters and arguments
  • Return values
  • Default arguments
  • *args and **kwargs
  • Lambda functions
  • Recursion
  • Introduction to decorators

🔧 What is a Function?

A function is a reusable block of code that performs a specific task. Functions help you:

  • Avoid code repetition (DRY - Don't Repeat Yourself)
  • Organize code into logical units
  • Make code easier to read and maintain
  • Enable code reuse

📝 Function Definition and Calling

Basic Syntax

# Define a function
def greet():
    print("Hello, World!")

# Call the function
greet()  # Output: Hello, World!

Function with Parameters

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

greet("Alice")  # Output: Hello, Alice!
greet("Bob")    # Output: Hello, Bob!

Function with Return Value

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

result = add(3, 5)
print(result)  # Output: 8

# Multiple return values (returns a tuple)
def get_stats(numbers):
    return min(numbers), max(numbers), sum(numbers)

minimum, maximum, total = get_stats([1, 2, 3, 4, 5])

📥 Parameters and Arguments

Types of Arguments

# Positional arguments
def greet(first_name, last_name):
    print(f"Hello, {first_name} {last_name}")

greet("John", "Doe")  # Positional

# Keyword arguments
greet(last_name="Doe", first_name="John")  # Named

# Mixed (positional must come first!)
greet("John", last_name="Doe")  # OK
# greet(first_name="John", "Doe")  # ERROR!

Default Parameters

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

greet("Alice")              # Hello, Alice!
greet("Bob", "Hi")          # Hi, Bob!
greet("Charlie", greeting="Hey")  # Hey, Charlie!

# ⚠️ Default parameters must come after non-default ones
def example(a, b=10, c=20):  # OK
    pass

# def example(a=10, b):  # ERROR!

Mutable Default Arguments (Gotcha!)

# ❌ WRONG - mutable default is shared!
def add_item_bad(item, items=[]):
    items.append(item)
    return items

print(add_item_bad("a"))  # ['a']
print(add_item_bad("b"))  # ['a', 'b'] - Unexpected!

# ✅ CORRECT - use None as default
def add_item_good(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item_good("a"))  # ['a']
print(add_item_good("b"))  # ['b'] - Correct!

🌟 *args and **kwargs

*args (Variable Positional Arguments)

def sum_all(*args):
    print(f"args = {args}")  # It's a tuple!
    return sum(args)

print(sum_all(1, 2, 3))      # 6
print(sum_all(1, 2, 3, 4, 5))  # 15

# Combining with regular parameters
def greet(greeting, *names):
    for name in names:
        print(f"{greeting}, {name}!")

greet("Hello", "Alice", "Bob", "Charlie")

**kwargs (Variable Keyword Arguments)

def print_info(**kwargs):
    print(f"kwargs = {kwargs}")  # It's a dict!
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=25, city="NYC")

# Combining with regular parameters
def create_profile(name, **details):
    profile = {"name": name}
    profile.update(details)
    return profile

user = create_profile("Bob", age=30, job="Engineer")
print(user)  # {'name': 'Bob', 'age': 30, 'job': 'Engineer'}

Complete Parameter Order

# Order: regular -> *args -> keyword-only -> **kwargs
def example(a, b, *args, option=True, **kwargs):
    print(f"a={a}, b={b}")
    print(f"args={args}")
    print(f"option={option}")
    print(f"kwargs={kwargs}")

example(1, 2, 3, 4, 5, option=False, x=10, y=20)

Unpacking Arguments

# Unpack list/tuple with *
def add(a, b, c):
    return a + b + c

numbers = [1, 2, 3]
print(add(*numbers))  # Same as add(1, 2, 3)

# Unpack dict with **
def greet(name, greeting):
    print(f"{greeting}, {name}!")

params = {"name": "Alice", "greeting": "Hello"}
greet(**params)  # Same as greet(name="Alice", greeting="Hello")

⚡ Lambda Functions

Lambda functions are anonymous, one-line functions.

Basic Syntax

# Regular function
def add(a, b):
    return a + b

# Lambda equivalent
add = lambda a, b: a + b

print(add(3, 5))  # 8

Common Use Cases

# With sorted()
students = [("Alice", 85), ("Bob", 92), ("Charlie", 78)]
sorted_by_score = sorted(students, key=lambda x: x[1])
print(sorted_by_score)  # [('Charlie', 78), ('Alice', 85), ('Bob', 92)]

# With map()
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x**2, numbers))
print(squares)  # [1, 4, 9, 16, 25]

# With filter()
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # [2, 4]

# With reduce()
from functools import reduce
product = reduce(lambda x, y: x * y, numbers)
print(product)  # 120 (1*2*3*4*5)

When to Use Lambda

# ✅ Good - simple, one-time use
sorted(data, key=lambda x: x["name"])

# ❌ Avoid - complex logic, use regular function
# lambda x: (x.split()[0].upper() if x else None)

# ❌ Avoid - reused multiple times, give it a name!
# my_func = lambda x: complex_operation(x)

🔄 Recursion

A recursive function calls itself to solve smaller instances of the same problem.

Basic Example: Factorial

def factorial(n):
    # Base case
    if n <= 1:
        return 1
    # Recursive case
    return n * factorial(n - 1)

print(factorial(5))  # 120
# 5! = 5 * 4! = 5 * 4 * 3! = 5 * 4 * 3 * 2! = 5 * 4 * 3 * 2 * 1 = 120

Fibonacci

def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 0, 1, 1, 2, 3, 5, 8, 13, 21...
for i in range(10):
    print(fibonacci(i), end=" ")

Recursion Tips

# 1. Always have a base case (stopping condition)
# 2. Ensure progress toward base case
# 3. Python has a recursion limit (default ~1000)

import sys
print(sys.getrecursionlimit())  # 1000
# sys.setrecursionlimit(2000)  # Can increase if needed

# 4. Consider iterative solutions for deep recursion
# 5. Use memoization for overlapping subproblems

🎀 Decorators (Introduction)

Decorators modify or enhance functions without changing their code.

Basic Decorator

def my_decorator(func):
    def wrapper():
        print("Before the function")
        func()
        print("After the function")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# Output:
# Before the function
# Hello!
# After the function

Decorator with Arguments

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the function")
        result = func(*args, **kwargs)
        print("After the function")
        return result
    return wrapper

@my_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

Practical Decorators

import time

# Timer decorator
def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

slow_function()  # slow_function took 1.0012 seconds

# Debug decorator
def debug(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@debug
def add(a, b):
    return a + b

add(3, 5)

📋 Docstrings

Document your functions using docstrings.

def calculate_area(length, width):
    """
    Calculate the area of a rectangle.

    Args:
        length (float): The length of the rectangle.
        width (float): The width of the rectangle.

    Returns:
        float: The area of the rectangle.

    Raises:
        ValueError: If length or width is negative.

    Examples:
        >>> calculate_area(5, 3)
        15
        >>> calculate_area(10, 10)
        100
    """
    if length < 0 or width < 0:
        raise ValueError("Dimensions must be positive")
    return length * width

# Access docstring
print(calculate_area.__doc__)
help(calculate_area)

🔄 Higher-Order Functions

Functions that take other functions as arguments or return functions.

Built-in Higher-Order Functions

numbers = [1, 2, 3, 4, 5]

# map() - apply function to each element
squared = list(map(lambda x: x**2, numbers))
print(squared)  # [1, 4, 9, 16, 25]

# filter() - keep elements that match condition
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # [2, 4]

# sorted() with key function
words = ["banana", "apple", "cherry"]
by_length = sorted(words, key=len)
print(by_length)  # ['apple', 'banana', 'cherry']

# reduce() - accumulate values
from functools import reduce
total = reduce(lambda x, y: x + y, numbers)
print(total)  # 15

Creating Higher-Order Functions

def apply_operation(numbers, operation):
    return [operation(x) for x in numbers]

numbers = [1, 2, 3, 4, 5]
squared = apply_operation(numbers, lambda x: x**2)
doubled = apply_operation(numbers, lambda x: x*2)

📝 Summary

ConceptSyntaxDescription
Definedef name():Create a function
Returnreturn valueReturn a value
Defaultdef f(x=10):Default parameter value
*argsdef f(*args):Variable positional args
**kwargsdef f(**kwargs):Variable keyword args
Lambdalambda x: x*2Anonymous function
Decorator@decoratorModify function behavior

🎯 Next Steps

After mastering functions, proceed to 06_modules_packages to learn about organizing code into modules!

README - Python Tutorial | DeepML