# Chapter 4: Functions

Functions are reusable blocks of code designed to perform a specific task. They help organize code, reduce repetition, and improve readability and maintainability. In Python, functions are first-class objects, meaning they can be passed around as arguments, returned from other functions, and assigned to variables.

#### Defining and Calling Functions

A function must be defined before it can be called. Use the `def` keyword to define a function. Functions can take inputs, perform tasks, and optionally return outputs.

**Syntax:**

```python
def function_name(parameters):
    """Optional documentation string (docstring)"""
    # Code block
    return value  # Optional
```

**Example:**

```python
def greet(name):
    """This function returns a greeting message."""
    return f"Hello, {name}!"

# Calling the function
greeting = greet("Alice")
print(greeting)
```

**Key Points:**

* Function names should follow the same naming rules as variables.
* Parameters are optional; a function can take zero or more arguments.
* The `return` statement is optional but allows the function to send a result back to the caller.
* Include a docstring to describe the function’s purpose and behavior.

#### Parameters and Arguments

**Types of Parameters:**

1. **Positional Parameters:** Must be provided in the order they appear in the function definition.

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

   print(multiply(2, 3))  # Output: 6
   ```
2. **Default Parameters:** Provide default values for arguments, making them optional.

   ```python
   def greet(name="Guest"):
       print(f"Hello, {name}!")

   greet()  # Output: Hello, Guest!
   greet("Alice")  # Output: Hello, Alice!
   ```
3. **Keyword Arguments:** Allow specifying arguments by name, regardless of their order.

   ```python
   def divide(numerator, denominator):
       return numerator / denominator

   print(divide(denominator=4, numerator=8))  # Output: 2.0
   ```
4. **Arbitrary Arguments:** Use `*args` for non-keyword variable-length arguments and `**kwargs` for keyword variable-length arguments.

   ```python
   def print_all(*args, **kwargs):
       print("Positional:", args)
       print("Keyword:", kwargs)

   print_all(1, 2, 3, a="apple", b="banana")
   # Positional: (1, 2, 3)
   # Keyword: {'a': 'apple', 'b': 'banana'}
   ```

#### Return Values

A function can return a value to the caller using the `return` statement. If no `return` is provided, the function implicitly returns `None`.

**Examples:**

```python
def square(num):
    return num ** 2

print(square(4))  # Output: 16

# Multiple return values

def stats(numbers):
    return min(numbers), max(numbers), sum(numbers) / len(numbers)

print(stats([1, 2, 3, 4, 5]))  # Output: (1, 5, 3.0)
```

#### Scope and Lifetime of Variables

Scope determines where a variable can be accessed, while lifetime refers to how long it exists.

**Types of Scope:**

1. **Local Scope:** Variables declared inside a function are accessible only within that function.

   ```python
   def test():
       x = 10  # Local variable
       print(x)

   test()
   # print(x)  # Error: x is not defined
   ```
2. **Global Scope:** Variables declared outside all functions are accessible everywhere.

   ```python
   x = 10  # Global variable
   def test():
       print(x)

   test()
   print(x)
   ```
3. **Global Keyword:** Used to modify a global variable inside a function.

   ```python
   x = 10
   def modify():
       global x
       x = 20

   modify()
   print(x)  # Output: 20
   ```

**Lifetime:**

* Local variables are destroyed when the function execution ends.
* Global variables persist throughout the program’s execution.

#### Lambda Functions

Lambda functions are anonymous, single-line functions defined using the `lambda` keyword. They are useful for short, simple operations.

**Syntax:**

```python
lambda arguments: expression
```

**Examples:**

```python
# Single-argument lambda
square = lambda x: x ** 2
print(square(5))  # Output: 25

# Multi-argument lambda
add = lambda a, b: a + b
print(add(3, 7))  # Output: 10

# Sorting with lambda
names = ["Alice", "Bob", "Charlie"]
names.sort(key=lambda name: len(name))
print(names)  # Output: ['Bob', 'Alice', 'Charlie']
```

**Key Points:**

* Lambda functions can have multiple arguments but only one expression.
* They are often used with higher-order functions like `map()`, `filter()`, and `reduce()`.

#### Higher-Order Functions

Functions that accept other functions as arguments or return them as results are called higher-order functions.

**Examples:**

```python
# Using map()
numbers = [1, 2, 3, 4]
squares = map(lambda x: x ** 2, numbers)
print(list(squares))  # Output: [1, 4, 9, 16]

# Using filter()
numbers = [1, 2, 3, 4]
evens = filter(lambda x: x % 2 == 0, numbers)
print(list(evens))  # Output: [2, 4]

# Using reduce()
from functools import reduce
numbers = [1, 2, 3, 4]
product = reduce(lambda x, y: x * y, numbers)
print(product)  # Output: 24
```

#### Best Practices

1. Use meaningful names for functions and parameters.
2. Keep functions short and focused on a single task.
3. Document functions with docstrings.
4. Avoid using global variables; prefer passing arguments to functions.
5. Test functions independently.

#### Exercises

**Exercise 1:**

Write a function that calculates the greatest common divisor (GCD) of two numbers.

**Solution:**

```python
def gcd(a, b):
    while b:
        a, b = b, a % b
    return a

print(gcd(48, 18))  # Output: 6
```

**Exercise 2:**

Write a function to generate the Fibonacci sequence up to a given number `n`.

**Solution:**

```python
def fibonacci(n):
    sequence = []
    a, b = 0, 1
    while a < n:
        sequence.append(a)
        a, b = b, a + b
    return sequence

print(fibonacci(10))  # Output: [0, 1, 1, 2, 3, 5, 8]
```

**Exercise 3:**

Write a function that accepts another function as an argument and applies it to a list of numbers.

**Solution:**

```python
def apply_function(func, numbers):
    return [func(num) for num in numbers]

print(apply_function(lambda x: x ** 2, [1, 2, 3, 4]))  # Output: [1, 4, 9, 16]
```

In the next chapter, we will explore Python’s data structures, including lists, tuples, dictionaries, and sets.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://py.d19.in/chapter-4-functions.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
