# 2.5. Function#

## 2.5.1. Omit Else Clauses in a Python Function to Improve Code Readability#

If you are using if statements to return different values, adding an else clause may introduce unnecessary complexity. Omitting the else clause for the last condition will make the code simpler and easier to read.

```def get_discount(price):
if price >= 100:
return 20
if price >= 50:
return 10
else: # not necessary
return 5
```
```def get_discount(price):
if price >= 100:
return 20
if price >= 50:
return 10
return 5 # omit else
```

## 2.5.2. When to Use and Not to Use Lambda Functions#

Lambda functions are helpful when defining a function that is used only once and does not require a name.

```numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]

# use lambda function because it is used only once
even_numbers = filter(lambda num: num % 2 == 0, numbers)
```

However, if you need to reuse a function in various parts of your code, use a named function to avoid repeating the same code.

```# use named function because it is used multiple times
def is_even(num: int):
return num % 2 == 0

even_numbers = filter(is_even, numbers)
any(is_even(num) for num in numbers)
```
```True
```

## 2.5.3. How to Pass an Arbitrary Number of Arguments to a Python Function#

If you want to create a function that takes an arbitrary number of arguments, use `*args` or `**kwargs`.

`*args` allows variable arguments as a set, while `**kwargs` allows variable keyword arguments as a dictionary.

```def multiply(*nums):
print(f"nums: {nums}")
res = 1
for num in nums:
res *= num
return res

multiply(1, 2, 3)
```
```nums: (1, 2, 3)
```
```6
```
```def add_to_order(**new_order):
print(f"new_order: {new_order}")
cart = {'apple': 2, 'orange': 3}
cart.update(new_order)
return cart

```
```new_order: {'kiwi': 2, 'apple': 1}
```
```{'apple': 1, 'orange': 3, 'kiwi': 2}
```

## 2.5.4. Decorator in Python#

If you want to apply a common piece of functionality to multiple functions while keeping the code clean, use decorator. Decorator modifies the behavior of your Python functions without altering the code directly.

In the code below, `time_func` is a decorator that can be used to track the execution time of any function.

```import time

def time_func(func):
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
print(f'Elapsed time: {(end - start) * 1000:.3f}ms')
return wrapper
```
```@time_func
return num1 + num2

@time_func
def multiply(num1: int, num2: int):
print(f"Multiply {num1} and {num2}")
return num1 * num2

multiply(1, 2)
```
```Add 1 and 2
Elapsed time: 1.006ms
Multiply 1 and 2
Elapsed time: 0.027ms
```

## 2.5.5. Keyword-Only Arguments in Python#

In Python, you can define functions that accept arguments either by position or by keyword. However, passing arguments by position can lead to errors if the arguments are provided in the wrong order.

```import pandas as pd

```
```---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[5], line 8
4 def add_number(number: float, df: pd.DataFrame):
----> 8 add_number(pd.DataFrame({"a": [1, 2, 3]}), 2)

Cell In[5], line 5, in add_number(number, df)
4 def add_number(number: float, df: pd.DataFrame):

AttributeError: 'int' object has no attribute 'add'
```

To avoid this issue, you can define keyword-only arguments in a function using the * symbol. This requires the caller to specify the arguments using their corresponding keywords.

```def add_number(*, number: float, df: pd.DataFrame):

```
```---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], line 5
1 def add_number(*, number: float, df: pd.DataFrame):
----> 5 add_number(pd.DataFrame({"a": [1, 2, 3]}), 2)

TypeError: add_number() takes 0 positional arguments but 2 were given
```
```add_number(df=pd.DataFrame({"a": [1, 2, 3]}), number=2)
```
a
0 3
1 4
2 5

## 2.5.6. Enhance Code Readability with Single Point of Return#

Consider using a single point of return in a Python function instead of multiple points of return to enhance code readability. When there is only one return statement, it becomes simpler to follow the logic and understand the purpose of the function.

```def calculate_grade(score: float):
if score < 0 or score > 100:
print("Invalid score!")
return None
elif score >= 90:
print("Excellent!")
return "A"
elif score >= 80:
print("Good job!")
return "B"
elif score >= 70:
print("Average.")
return "C"
else:
print("You failed.")
return "F"
```
```def calculate_grade(score: float):
if score < 0 or score > 100:
print("Invalid score!")
elif score >= 90: