# Python refresher — Part 2

# 1. Generators

- Function that returns an iterator — i.e. An item that we can iterate over one value at a time
- A generator can preserves all variables and states when a yield is encountered. Thereafter, when a next call is encountered the function carries on the execution from where it left off.

def days_of_the_week():

n = 1

yield n n += 1

yield n n += 1

yield n>>> print( days_of_the_week() ) # 1>>> print( days_of_the_week() ) # 2>>> print( days_of_the_week() ) # 3

# 2. List comprehensions

- List comprehensions are formed by creating a new list by applying some logic to all items in the original list.
- For example, to square all entries in a list:

`>>> a = [3, 4, 5]`

>>> b = [**x ** x** for x in a ]

# b = [9, 16, 25]

# 3. Optional arguments

- It is possible to write functions with an arbitrary number of arguments

def sum( a, b, *optional):

if len(optional) != 0:

return sum(a, b) + sum(optional)

else:

return (a + b)>>> sum(3,1) # 4

>>> sum(3,1,4,5) # 13

- Consequentially,
**a & b**are called**required arguments,**whereas optional is a list of optional arguments. - The optional arguments can also be represented as a hash-map if
****optional**is used instead of *optional.

# 4. Regular expressions

- Regular expressions can be leveraged to uncover complex patterns from strings.
- Python comes with a built in
**re**module that can be used for this purpose

import re>>> string = "hakuna matata"

>>> re.match( r'(.*)\n (.*?) .*', string, noflags)

# no match

# 5. Partial functions

- Partial functions are commonly used in tasks where you need to pre-populate some arguments in a function such as in the example below:

def_send_email(to, subject, body):

send_mail(subject,

body,

settings.EMAIL_HOST_USER, ) email_admin = partial(_send_email, to="admin@example.com") email_general_it = partial(_send_email, to="it@example.com") email_marketing = partial(_send_email, to="marketing@example.com") email_sales = partial(_send_email, to="sales@example.com")

- This way, you only need to provide the remaining two arguments when calling the generated higher order functions.
**Ref:**https://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions

# 6. Closures

- A closure is just a function that contains a nested function that uses one or more of the enclosing function’s variables.

def make_multiplier_of(n):

def multiplier(x):

return x * n

return multiplier# Multiplier of 3

times3 = make_multiplier_of(3)# Output: 27

print(times3(9))

# 7. Decorators

- A decorator simply modifies a function and return a new function
- This essentially means to augment a core function with additional functionality.