Python Functions
## Introduction to Python Functions
In Python, a **function** is a structured, reusable block of code designed to perform a single, focused, or related action.
Functions are fundamental to writing clean, maintainable code. They improve your application's modularity and enable high levels of code reuse. While Python comes with many built-in functions (such as `print()`, `len()`, and `range()`), you can also define your own functions. These are referred to as **user-defined functions**.
---
## Defining a Function
You can define custom functions to implement any logic you require. When defining a function in Python, keep the following rules in mind:
* **The `def` Keyword:** Every function definition begins with the `def` keyword, followed by the function name and parentheses `()`.
* **Parameters:** Any input parameters or arguments must be placed within these parentheses. You can also define default values for these parameters inside the parentheses.
* **Docstrings:** The first statement of a function body can optionally be a string literal. This is the function's documentation string (docstring), used to describe what the function does.
* **Colon and Indentation:** The function block starts after a colon (`:`) and must be indented.
* **The `return` Statement:** The `return ` statement exits a function, optionally passing back an expression to the caller. A `return` statement with no arguments (or omitting the `return` statement entirely) implicitly returns `None`.
### Syntax
```python
def function_name(parameters):
"""Optional function docstring."""
function_suite
return
```
By default, arguments are matched to parameters sequentially based on the order in which they are declared.
### Basic Example
The following is a simple Python function. It takes a string as an input parameter and prints it to the standard output:
```python
def print_me(input_string):
"""Prints the passed string to the standard output."""
print(input_string)
return
```
---
## Calling a Function
Defining a function only specifies its name, parameters, and internal code block structure. To execute the code inside the function, you must **call** it from another block of code or directly from the Python interpreter.
The following example demonstrates how to call the `print_me()` function:
```python
# Function definition
def print_me(input_string):
"""Prints any passed string."""
print(input_string)
return
# Calling the function
print_me("Calling the user-defined function for the first time!")
print_me("Calling the same function again.")
```
### Output
```text
Calling the user-defined function for the first time!
Calling the same function again.
```
---
## Parameter Passing: Mutable vs. Immutable Objects
In Python, types are associated with **objects**, not variables. A variable is simply a reference (or pointer) to an object in memory.
For example:
```python
a = [1, 2, 3]
a = "YouTip"
```
In this snippet, `[1, 2, 3]` is a `list` object, and `"YouTip"` is a `string` object. The variable `a` has no fixed type; it is merely a reference that first points to a list object and is later reassigned to point to a string object.
### Mutable vs. Immutable Objects
Python handles data types based on whether they are mutable (changeable) or immutable (unchangeable):
* **Immutable Types:** Objects of types like integers, floats, strings, and tuples cannot be modified after they are created. If you assign `a = 5` and then `a = 10`, you are not changing the value of the original integer object `5`. Instead, you are creating a new integer object `10` and pointing `a` to it.
* **Mutable Types:** Objects of types like lists, dictionaries, and sets can be modified in place. If you assign `la = [1, 2, 3, 4]` and then modify an element with `la = 5`, the list object itself is modified. Its memory address remains the same.
### How Arguments are Passed to Functions
Because everything in Python is an object, argument passing is best understood through the lens of object mutability rather than traditional "pass-by-value" or "pass-by-reference" terminology:
* **Passing Immutable Objects:** Similar to pass-by-value. When you pass integers, strings, or tuples to a function, modifying the parameter inside the function does not affect the original object outside the function. Instead, the local variable is reassigned to a new object.
* **Passing Mutable Objects:** Similar to pass-by-reference. When you pass lists, dictionaries, or sets to a function, any in-place modifications made to the parameter inside the function will reflect on the original object outside the function.
#### Example: Passing an Immutable Object (Integer)
```python
def change_int(num):
num = 10 # Reassigns local variable 'num' to a new integer object
val = 2
change_int(val)
print(val) # Output is still 2
```
**Explanation:** The variable `val` points to the integer object `2`. When passed to `change_int()`, the local variable `num` also points to `2`. When `num = 10` is executed, a new integer object `10` is created, and `num` is reassigned to point to it. The original variable `val` still points to `2`.
#### Example: Passing a Mutable Object (List)
```python
def modify_list(my_list):
"""Appends a list to the passed list."""
my_list.append([1, 2, 3, 4])
print("Inside the function:", my_list)
return
# Calling the function
sample_list = [10, 20, 30]
modify_list(sample_list)
print("Outside the function:", sample_list)
```
### Output
```text
Inside the function: [10, 20, 30, [1, 2, 3, 4]]
Outside the function: [10, 20, 30, [1, 2, 3, 4]]
```
---
## Function Arguments
When calling a function, you can pass arguments using four different mechanisms:
1. Required arguments (Positional arguments)
2. Keyword arguments
3. Default arguments
4. Variable-length arguments (`*args` and `**kwargs`)
### 1. Required Arguments
Required arguments must be passed to the function in the correct positional order. The number of arguments in the function call must match the number of parameters defined in the function signature.
If you call a function without its required arguments, Python will raise a `TypeError`:
```python
def print_me(input_string):
"""Prints the passed string."""
print(input_string)
return
# Calling the function without arguments raises an error
print_me()
```
### Output
```text
Traceback (most recent call last):
File "test.py", line 7, in
print_me()
TypeError: print_me() missing 1 required positional argument: 'input_string'
```
### 2. Keyword Arguments
Keyword arguments allow you to pass arguments to a function using the parameter names as identifiers (`parameter_name = value`).
Using keyword arguments allows you to pass arguments in any order, as the Python interpreter uses the names to match values to parameters.
```python
def print_info(name, age):
"""Prints the passed user information."""
print("Name:", name)
print("Age:", age)
return
# Calling print_info with keyword arguments in a different order
print_info(age=50, name="Miki")
```
### Output
```text
Name: Miki
Age: 50
```
### 3. Default Arguments
A default argument is an argument that assumes a default value if a value is not provided in the function call. You can define default values in the function signature using the assignment operator (`=`).
```python
def print_info(name, age=35):
"""Prints user information. Defaults age to 35 if not provided."""
print("Name:", name)
print("Age:", age)
return
# Call with both arguments
print_info(age=50, name="Miki")
# Call with only the required argument
print_info(name="Miki")
```
### Output
```text
Name: Miki
Age: 50
Name: Miki
Age: 35
```
### 4. Variable-Length Arguments
Sometimes you need to process a function with more arguments than you specified when defining the function. These are called variable-length arguments.
#### Positional Variable-Length Arguments (`*args`)
An asterisk (`*`) before a variable name in the function definition holds all non-keyword variable arguments as a **tuple**.
```python
def print_non_keyword_args(first_arg, *var_args):
"""Prints variable positional arguments."""
print("First argument:", first_arg)
print("Variable arguments:")
for arg in var_args:
print(arg)
return
# Call with multiple arguments
print_non_keyword_args(10)
print_non_keyword_args(70, 60, 50, 40)
```
### Output
```text
First argument: 10
Variable arguments:
First argument: 70
Variable arguments:
60
50
40
```
#### Keyword Variable-Length Arguments (`**kwargs`)
Two asterisks (`**`) before a variable name allow you to pass a variable number of keyword arguments. Inside the function, these arguments are received as a **dictionary**.
```python
def print_keyword_args(first_arg, **var_kwargs):
"""Prints variable keyword arguments."""
print("First argument:", first_arg)
print("Variable keyword arguments:", var_kwargs)
return
# Call with keyword arguments
print_keyword_args(100, user="Alice", role="Admin")
```
### Output
```text
First argument: 100
Variable keyword arguments: {'user': 'Alice', 'role': 'Admin'}
```
---
## Best Practices and Considerations
* **Keep Functions Focused:** A function should adhere to the Single Responsibility Principle (SRP). It should do one thing and do it well.
* **Use Clear Naming Conventions:** Use lowercase words separated by underscores (`snake_case`) for function names to comply with PEP 8 guidelines.
* **Avoid Mutable Default Arguments:** Never use mutable objects (like empty lists `[]` or dictionaries `{}`) as default arguments. Because default arguments are evaluated once when the function is defined, subsequent calls will share the same mutable object, leading to unexpected side effects.
*Incorrect:*
```python
def append_to(element, target_list=[]):
target_list.append(element)
return target_list
```
*Correct:*
```python
def append_to(element, target_list=None):
if target_list is None:
target_list = []
target_list.append(element)
return target_list
```
YouTip