## 8.1 Additional Python Conveniences

##### Comprehension Syntax
• Four ways of creating a list


def test1():
l = []
for i in range(100):
l = l + [i]

def test2():
l = []
for i in range(100):
l.append(i)

def test3():
l = list(range(100))

def test4():
l = [i for i in range(100)]


[ expression for value in iterable if condition ]


>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]


>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]


>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']


>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']


>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']

• map()


def f(x):
return x * x

y = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print(list(y))


[1, 4, 9, 16, 25, 36, 49, 64, 81]


>>> print(list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])))
['1', '2', '3', '4', '5', '6', '7', '8', '9']


### Anonymous function: lambda


def add( x, y ):
return x + y

lambda x, y: x + y

lambda x, y = 2: x + y
lambda *z: z



>>> a = lambda x, y: x + y
>>> a( 1, 3 )
4
>>> b = lambda x, y = 2: x + y
>>> b( 1 )
3
>>> b( 1, 3 )
4
>>> c = lambda *z: z
>>> c( 10, 'test')
(10, 'test')

• Sometimes the anonymous function is convenient.

• It has only one expression. (You do not have to use return)


sum = lambda arg1, arg2: arg1 + arg2

print ("The total is : ", sum( 10, 20 ))
print ("The total is : ", sum( 20, 20 ))


>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]


## 8.2 Iterators and Generators

##### Iterator

Many types of objects in Python that qualify as being iterable, such as list, tuple, dictionary. In Python, the mechanism for iteratioin is based upon the following conventions:

• An iterator is an object that manages an iteration through a series of values. If variable, i, identifies an iterator object, then each call to the built-in function, next(i), produces a subsequent element from the underlying series, with StopIteration exception raised to indicate that there are no further elements.
• An iterable is an object, obj, that produces an iterator via syntax iter(obj).

x=[1,2,3,4]
y=iter(x)
print(next(y))
print(next(y))
print(next(y))
print(next(y))
print(next(y))


1
2
3
4
Traceback (most recent call last):
File "test.py", line 7, in <module>
print(next(y))
StopIteration


itertools


import itertools
for item in itertools.repeat('hello world', 3):
print(item)


hello world
hello world
hello world


import itertools

nums = itertools.count(0,2)
for i in nums:
if i > 6:
break
print(i, end = " ")


0 2 4 6


import itertools

cycle_strings = itertools.cycle('ABC')
i = 1
for string in cycle_strings:
if i == 7:
break
print(string, end=" ")
i += 1


A B C A B C

• The most convenient technique for creating iterators in Python is through the use of Generators.

A generator is implemented with a syntax that is very similar to a function, but instead of returning values, a yield statement is executed to indicate each element of the series.

def yrange(n):
i = 0
while i < n:
yield i
i += 1
o = yrange(5)
print(next(o))
print(next(o))
print(next(o))
print(next(o))
print(next(o))
print(next(o))


0
1
2
3
4
Traceback (most recent call last):
File "test.py", line 13, in <module>
print(next(o))
StopIteration


>>> g = (x * x for x in range(4))
>>> g
<generator object <genexpr> at 0x1022ef630>

>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration


def yrange(n):
i = 0
while i < n:
yield i
i += 1

for x in yrange(5):
print(x)


## 8.3 Return a Function

• Closure Function

def calc_sum(*args):
ax = 0
for n in args:
ax = ax + n
return ax


def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum

f = lazy_sum(1, 3, 5, 7, 9)
print(f()) # 25
print(type(f)) # function


f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
print(f1==f2) #False

• Defining a closure function

• When do we have a closure?

• We must have a nested function (function inside a function).

• The nested function must refer to a value defined in the enclosing function.

• The enclosing function must return the nested function.

• When to use closures?

• Closures can avoid the use of global values and provides some form of data hiding. It can also provide an object oriented solution to the problem.

### Example


def print_msg():
msg = "zen of python"
def printer():
return msg
return printer

another = print_msg()
print(another())


def make_multiplier_of(n):
def multiplier(x):
return x * n
return multiplier

times3 = make_multiplier_of(3)

times5 = make_multiplier_of(5)

print(times3(9))

print(times5(3))

print(times5(times3(2)))

• Decorator Basics
• Decorators are “wrappers”, which means that they let you execute code before and after the function they decorate without modifying the function itself.


def my_shiny_new_decorator(a_function_to_decorate):
def the_wrapper_around_the_original_function():
print("Before the function runs")
a_function_to_decorate()
print("After the function runs")
return the_wrapper_around_the_original_function

def a_function():
print("I am a stand alone function.")

a_function()
#outputs:
I am a stand alone function.


a_function_decorated = my_shiny_new_decorator(a_function)
a_function_decorated()


Before the function runs
I am a stand alone function.
After the function runs


@my_shiny_new_decorator
def another_stand_alone_function():
print("Leave me alone")
# @decorator is just a shortcut
another_stand_alone_function()


Before the function runs
Leave me alone
After the function runs


def bread(func):
def wrapper():
print("")
func()
print("<\______/>")
return wrapper

def ingredients(func):
def wrapper():
print("#tomatoes#")
func()
print("~salad~")
return wrapper

def sandwich(food="--ham--"):
print(food)


sandwich()

sandwich = bread(ingredients(sandwich))
sandwich()




--ham--

#tomatoes#
--ham--
~salad~
<\______/>


@bread
@ingredients # The order matters here.
def sandwich(food="--ham--"):
print(food)

sandwich()



#tomatoes#
--ham--
~salad~
<\______/>

• Taking decorators to the next level

• Passing arguments to the decorated function


def a_decorator_passing_arguments(function_to_decorate):
def a_wrapper_accepting_arguments(arg1, arg2):
print("I got args! Look: %s, %s" % (arg1, arg2))
function_to_decorate(arg1, arg2)
return a_wrapper_accepting_arguments

@a_decorator_passing_arguments
def print_full_name(first_name, last_name):
print("My name is %s, %s" % (first_name, last_name))

print_full_name("Peter", "Venkman")


I got args! Look: Peter Venkman
My name is Peter Venkman

• If you're making general-purpose decorator--one you'll apply to any function or method, no matter its arguments--then just use *args, **kwargs:


def a_decorator_passing_arbitrary_arguments(function_to_decorate):
# The wrapper accepts any arguments
def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs):
print("Do I have args?:")
print(args)
print(kwargs)
function_to_decorate(*args, **kwargs)
return a_wrapper_accepting_arbitrary_arguments

@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
print("Python is cool, no argument here.")

function_with_no_argument()


Do I have args?:
()
{}
Python is cool, no argument here.


@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
print(a, b, c)

function_with_arguments(1,2,3)


Do I have args?:
(1, 2, 3)
{}
1 2 3


@a_decorator_passing_arbitrary_arguments
def function_with_named_arguments(a, b, c, platypus="Why not ?"):
print("Do {0}, {1} and {2} like platypus? {3}".format(a, b, c, platypus))

function_with_named_arguments("Bill", "Linus", "Steve", platypus="Indeed!")


Do I have args ? :
('Bill', 'Linus', 'Steve')
{'platypus': 'Indeed!'}
Do Bill, Linus and Steve like platypus? Indeed!


## Summary

• Functions
• Reading: Python for Everybody, Chapter 10.1-10.5, 10.7-10.8
• Reading: Python Crash Course, Chapter 8