#!/usr/bin/env python
# coding: utf-8

# # 8.2 List Comprehensions & Higher order functions
# <a href="https://colab.research.google.com/github/rambasnet/FDSPython-Notebooks/blob/master/Ch08-2-Lists-Advanced.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a>
# 
# ## Topics
# - list shortcuts
# - lambda functions applications
# - built-in higher order functions
# 
# ## 8.2.1 List comprehension
# - list is a very powerful and commonly used container
# - list shortcuts can make you an efficient programmer
# - E.g., an arithmetic set $S = \{x^2 : x \in \{0 ... 9\}\}$
#     - is equivalent to: 
#     ```python
#     S = [x**2 for x in range(10)]
#     ```
# - consists of brackets containing an expression followed by a for clause, then zero or more for or if clauses
#     - the expressions can be anything
#     - always results a new list from evaluating expression
# - syntax:
# ```python
# somelist = [expression for item in list if conditional]
# ```

# In[2]:


# Typical way to create a list of squared values of list 0 to 9?
sq = []
for i in range(10):
    sq.append(i**2)


# In[3]:


print(sq)


# In[4]:


# List comprehension -- handy technique:
S = [x**2 for x in range(10)]


# In[5]:


S


# In maths: V = (1, 2, 4, 8, ... 2 <sup>12</sup>)

# In[6]:


# In python ?:
V = [2**x for x in range(13)]
print(V)


# In mathematics: $M = \{x | x \in S \ and \ x \ even\}$

# In[10]:


# Simple approach
M = []
for x in S:
    if x%2 == 0:
        M.append(x)


# In[11]:


print(M)


# In[12]:


# List comprehension
M1 = [x for x in S if x%2==0]


# In[14]:


assert M == M1, 'M and M1 are not equal!'


# In[12]:


#sentence = "The quick brown fox jumps over the lazy dog"
#words = sentence.split()
# can make a list of tuples or list of lists
wlist = [(w.upper(), w.lower(), len(w)) for w in "The quick brown fox jumps over the lazy dog".split()]


# In[13]:


wlist


# ## 8.2.2 higher order functions and lambda applications
# - map, reduce, filter, sorted functions take function and iterable such as list as arguments
# - lambda expression can be used as a parameter for higher order functions

# ### sorted( )

# In[6]:


list1 = ['Apple', 'apple', 'ball', 'Ball', 'cat']
list2 = sorted(list1, key=lambda x: x.lower())


# In[7]:


print(list2) 


# In[8]:


list3 = [('cat', 10), ('ball', 20), ('apple', 3)] 
from operator import itemgetter
list5 = sorted(list3, key=itemgetter(1), reverse=True)


# In[9]:


print(list5)


# In[10]:


list6 = sorted(list3, key=lambda x: x[1], reverse=True)


# In[11]:


print(list6)


# ### filter( )
# - filter elemets in the list by returning a new list for each element the function returns True

# In[13]:


help(filter)


# In[14]:


list7 = [2, 18, 9, 22, 17, 24, 8, 12, 27]
list8 = list(filter(lambda x: x%3==0, list7))


# In[15]:


print(list8)


# ### map( )

# In[16]:


help(map)


# In[17]:


items = list(range(1, 11))
squared = list(map(lambda x: x**2, items))


# In[18]:


print(squared)


# In[1]:


# map each words with its length
sentence = "The quick brown fox jumps over the lazy dog"
words = [word.lower() for word in sentence.split()]


# In[2]:


print(words)


# In[21]:


w_len = list(map(lambda w: (w, w.upper(), len(w)), words))


# In[22]:


print(w_len)


# ### reduce( )
# - reduce() is found in functools module
# - used to reduce a list of values to a single output

# In[23]:


import functools
help(functools)


# ## 8.2.3 higher order function applications
# 
# ### find sum of first n values

# In[24]:


s = functools.reduce(lambda x,y:x+y, range(1, 11))


# In[25]:


assert sum(range(1, 11)) == s


# ### find factorial (or product of) first n values

# In[26]:


fact = functools.reduce(lambda x,y:x*y, range(1, 11))


# In[27]:


fact


# In[28]:


import math
assert math.factorial(10) == fact


# In[ ]: