# Python Fundamentals in a Nutshell

After some time off from Python, I decided to revisit some old concepts as well as learn a few new ones. Thought of summarizing them here:

* If you are performing the same calculation within a function for different input arguments, use nested functions.
    

This nested function can take one input argument and return the value after the calculation is performed on that argument. In the main (outer) function, return this nested function with the different input arguments passed to it.

So your initial function definition:

```python
def square_three_numbers(n1,n2,n3):
    sq_n1 = n1**2
    sq_n2 = n2**2
    sq_n3 = n3**2
    return (sq_n1,sq_n2,sq_n3)
```

Becomes:

```python
def square_three_numbers(n1,n2,n3):
    def square_number(number):
        return (number**2)
    return (square_number(n1),square_number(n2),square_number(n3))
```

The second snippet is a way more efficient way of modularizing your code.

* Use flexible arguments - '\*args' and '\*\*kwargs' when you don't know how many arguments you will be passing.
    

Use '\*args' when you are just plainly passing the arguments. This gets passed into the function body as a tuple and then you can iterate through it to get your arguments.

```python
def sum_numbers(*args):
    total=0
    for number in args:
        total=total+number
    return(total)
    
#Function call
print(sum_numbers(1,2,3))

#Output:
#6
```

Use '\*\*kwargs' when you pass an identifier before your argument value. This gets passed into the function body as a dictionary and then you can iterate through it to get your arguments (with keys being the identifiers and values being the argument value).

```python
#Function definition
def my_favourites(**kwargs):
    for key,value in kwargs.items():
        print("My favourite {0} is \'{1}\'!".format(key,value))



#Function call
my_favourites(series="Handmaid's Tale", movie="Rogue One: A Star Wars Story",song="Less I Know The Better")


#Output:
#My favourite series is 'Handmaid's Tale'!
#My favourite movie is 'Rogue One: A Star Wars Story'!
#My favourite song is 'Less I Know The Better'!
```

Is it weird that I took more time to decide my favourite movie than to write this code?

Notice the format() method for string. This is way easier than trying to concatenate strings using +.

* Lambda functions (anonymous function) - Used usually when you want to pass a function as an argument to higher-order functions.
    

```python
def exponent_function(exp):
    return lambda num : num ** exp

raised_to_10 = exponent_function(10)
raised_to_5 = exponent_function(5)

print(raised_to_10(2))
print(raised_to_5(2))

#Output:
#1024
#32
```

Lambda functions are used with:

> map(function,sequence) - Applies the lambda function to all the elements in the sequence. Returns a map object.

```python
numbers = [1,2,3,4]


#Use map() to apply the lambda function for all elements in your list
squares = map(lambda num:num**2, numbers)


#Remember, squares is a map object. So, convert it into a list to see what the values are!
print(list(squares))

#Output:
#[1,4,9,16]
```

> filter(function,sequence) - Applies the lambda function with a condition to the elements in the sequence. Returns a filter object.

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


#Use the lambda function in filter to print even numbers from the list
even_numbers = filter(lambda nums:nums%2==0,numbers)


#Remember, even_numbers is a filter object. So, convert it into a list to see what the values are!
print(list(even_numbers))

#Output
#[2, 4, 6]
```

> reduce(function,sequence) - Applies the lambda function to all the elements in the sequence. Returns a single value.

```python
numbers = [1,2,3,4]


# Use reduce() to apply a lambda function over numbers
product = reduce(lambda item1,item2:item1*item2, numbers)


# Print the result
print(product)

#Output:
#24
```

The function is first applied to the first two elements. The result of this computation and the third value are again passed into the function and so on.

* Iterators and Iterables - For efficient and flexible processing of data, especially when working with large datasets.
    

**Iterables**

```python
fruits = ["apple", "banana", "orange"]
for fruit in fruits:
    print(fruit)
```

Output:

```python
apple
banana
orange
```

In this example, the `fruits` list is an iterable object, and we can loop over its elements using a `for` loop.

**Iterators**

```python
fruits = ["apple", "banana", "orange"]
fruit_iterator = iter(fruits)
print(next(fruit_iterator))
print(next(fruit_iterator))
print(next(fruit_iterator))
```

Output:

```python
apple
banana
orange
```

In this example, we create an iterator object `fruit_iterator` using the `iter()` function. We then use the `next()` function to iterate over the elements of the `fruits` list. Each call to `next()` returns the next element in the list until there are no more elements to iterate over.

* List Comprehensions - **For loop reimagined.**
    

When this code:

```python
my_list=[]
for i in range(10):
    for j in range(5):
        my_list.append((j,i))
print(my_list)
```

becomes this:

```python
my_list_comp=[(j,i) for i in range(10) for j in range(5) ]
print(my_list_comp)
```

Your program will send you a pop-up saying 'Thank you!'

Seriously. Try it.

However, to ensure that you stay `pythonic`, it is advisable to have a clear understanding of when to use list comprehensions and when not to.
