Python Programming

Lecture 8 Advanced Features

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