<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Lecture 4
Topics to be covered today 
Step1:  The new iterable needn't be a list
Step2:  To be technically correct, the above two are NOT comprehensions but something called generator expressions. But for all practical use you can think of them as unofficial tuple and set comprehension.
Using conditional in list comprehension
Step4:  So other than it's elegance why do we need list comprehensions ? It's cause they are faster than their for loop variants. Run the next cell and look at the time difference. You'll most probably see that the comprehension is nearly twice as fast as the doubly nested loop.
Step5:  List Comprehension for Nested Loops
Step6:  Rookie Mistake
Let us consider the list comprehension above. A common mistake is to write the inner loops before it's encapsulating loop. Like so 
Step7:  So, remember --> Left to right, outer to inner..........
Step8:  I would strongly encourage you to think directly in terms of list comprehension rather than constructing the nested loop structure first and then convert to comprehension.
Also, the new_value can be anything Python type. It need not maintain the type of the list from which the new one is being constructed. Let us look at how the star program can be done via comprehension.
Step9:  So, why keep loops at all ?
It's cause when a bunch of statements are to be executed in an iterative construct then it maybe impossible to do in a list comprehension. List comprehension/Generator Expression is for creating a new iterable from an existing one. What if you need to print stuff, do something with list elements that doesn't involve making a new iterable, etc ?
Then list comprehension doesn't make sense cause that's not what it was designed for.
Readability
 It's important to make sure that your code is readable and able to be understood by other developers. We will elaborate on this later on whilst talking about PEP8 and good development practices.
Step10:  Dictionaries
Dictionaries in Python, are like Hash Maps or Associative Arrays. It is an iterable of key-value pairs. Obviously it is a mutable type.
So, why can't we instead have a list of tuples where every tuple is (key, value) ?
That's because the search/indexing operation will be really slow, that is linear time. Whereas for dictionary it is ideally constant time.
Construction
Step11:  You can see that dictionaries aren't sorted by key or value.
Searching in Dictionaries
Step12:  Various methods exist for dictionaries 
<ASSISTANT_TASK:>
Python Code:
# For loop
a = [1, 2, 3, 4, 5]
b = []
for i in a:
    b.append(i**2)
print(b)
###############################################
# Using list comprehension
c = [value ** 2 for value in a]
print(c)
Explanation: Lecture 4
Topics to be covered today :
1. List Comprehensions
2. Dictionaries
List Comprehensions
List comprehensions in Python is a efficient and concise technique to transform one iterable to
another.
From for loops to list comprehension
Structure of a basic list comprehension : [new_value for value in list]
Examples :
End of explanation
c = tuple(value ** 2 for value in a)
d = set(value **2 for value in a)
print(c)
print(d)
Explanation: The new iterable needn't be a list
End of explanation
a = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
b = []
for value in a:
    if value % 2 == 1:
        b.append(value)
print(b)
##############################################
c = [value for value in a if value % 2 == 1]
print(c)
Explanation: To be technically correct, the above two are NOT comprehensions but something called generator expressions. But for all practical use you can think of them as unofficial tuple and set comprehension.
Using conditional in list comprehension
End of explanation
import timeit
programs = {
    "Loop : " : 
result = []
for i in range(200):
    result.append(i * 2)
,
    "List Comprehension : " : 'result = [i * 2 for i in range(200)]',
}
for technique, code in programs.items():
    print(technique, timeit.Timer(stmt=code).timeit())
Explanation: So other than it's elegance why do we need list comprehensions ? It's cause they are faster than their for loop variants. Run the next cell and look at the time difference. You'll most probably see that the comprehension is nearly twice as fast as the doubly nested loop.
End of explanation
b = [i ** 2 for i in range(0, 4) for j in range(0, 5)]
print(b)
Explanation: List Comprehension for Nested Loops
End of explanation
c = [i ** 2 for j in range(0, 5) for i in range(0, 4)]
print(c)
Explanation: Rookie Mistake
Let us consider the list comprehension above. A common mistake is to write the inner loops before it's encapsulating loop. Like so :
End of explanation
d = [i + j ** 2 + k ** 3 for i in range(0, 5) for j in range(0, i) for k in range(0, j)]
print(d)
Explanation: So, remember --> Left to right, outer to inner..........
End of explanation
a = [['*' for j in range(0, i)] for i in range(1, 6)]
for sublist in a:
    for j in sublist:
        print(j, end='')
    print()
Explanation: I would strongly encourage you to think directly in terms of list comprehension rather than constructing the nested loop structure first and then convert to comprehension.
Also, the new_value can be anything Python type. It need not maintain the type of the list from which the new one is being constructed. Let us look at how the star program can be done via comprehension.
End of explanation
a = [i * 2 for i in range(0, 5) if i % 2 == 1]
b = [
    i * 2
    for i in range(0, 5)
    if i % 2 == 1
]
##############################################
c = [i + j ** 2 + k ** 3 for i in range(0, 5) for j in range(0, i) for k in range(0, j)]
d = [
    i + j ** 2 + k ** 3
    for i in range(0, 5)
    for j in range(0, i)
    for k in range(0, j)
]
print(a)
print(b)
print(c)
print(d)
Explanation: So, why keep loops at all ?
It's cause when a bunch of statements are to be executed in an iterative construct then it maybe impossible to do in a list comprehension. List comprehension/Generator Expression is for creating a new iterable from an existing one. What if you need to print stuff, do something with list elements that doesn't involve making a new iterable, etc ?
Then list comprehension doesn't make sense cause that's not what it was designed for.
Readability
 It's important to make sure that your code is readable and able to be understood by other developers. We will elaborate on this later on whilst talking about PEP8 and good development practices.
End of explanation
# Empty dictionary
dict1 = {}
# Comma separated list
dict2 = {"one" : 1, "two" : 2, "three" : 3, "four" : 4, "five" : 5}
# Using constructor(arguments as key = value pairs)
dict3 = dict(one=1, two=2, three=3, four=4, five=5)
# Using nested iterables
dict4 = dict([("one", 1), ("two", 2), ("three", 3), ("four", 4), ("five", 5)])
dict5 = dict([["one", 1], ["two", 2], ["three", 3], ["four", 4], ["five", 5]])
# Using separate iterables
dict6 = dict(zip(["one", "two", "three", "four", "five"], [1, 2, 3, 4, 5]))
print(dict1)
print(dict2)
print(dict3)
print(dict4)
print(dict5)
print(dict6)
Explanation: Dictionaries
Dictionaries in Python, are like Hash Maps or Associative Arrays. It is an iterable of key-value pairs. Obviously it is a mutable type.
So, why can't we instead have a list of tuples where every tuple is (key, value) ?
That's because the search/indexing operation will be really slow, that is linear time. Whereas for dictionary it is ideally constant time.
Construction
End of explanation
value1 = dict6['five']
print(value1)
# Throws an error. Better to use get() method
value1 = dict6['five']
print(value1)
value2 = dict6.get("six", None)
print(value2)
print(type(value2))
Explanation: You can see that dictionaries aren't sorted by key or value.
Searching in Dictionaries
End of explanation
from math import factorial
height = 7
lst = [[int(factorial(n) / (factorial(k) * factorial(n - k))) for k in range(n+1)]
  for n in range(height)]
for sublist in lst:
    for value in sublist:
        print(value, end=' ')
    print()
[["x" for j in range(0, i)] for i in range(0, 5)]
a0 = 0
a1 = 1
print(a0)
print(a1)
for i in range(0, 5):
    print(a0 + a1)
    temp = a1
    a1 = a0 + a1
    a0 = temp
    
Explanation: Various methods exist for dictionaries : 
1. get()
2. items()
3. copy()
4. del
.......
Look at this link for more : https://jeffknupp.com/blog/2015/08/30/python-dictionaries/
Short in-class Assignment
Construct Pascal's triangle for a given number of lines. Just print a space between numbers, don't worry about other spaces. The main construction of the values per line should be done via list comprehension.
For example
Input : 7
Output :
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
End of explanation
<END_TASK>