In [1]:
%%javascript
Jupyter.notebook.config.update({"load_extensions":{"calico-spell-check":true,
                                                  "calico-document-tools":true,
                                                  "calico-cell-tools":true}})


Programming Python

Putting it all together

Structuring a program

Programming paradigms

What are programming paradigms?

  • classification of programming languages
  • programming languages can follow several paradigms (Python: object oriented, imperative, functional, procedural)
  • paradigms might be concerned with the execution of the language (e.g. side effects yes or no)
  • paradigms might just describe the organization of code
  • ... or the style of the syntax and grammar of the language

Today's Lecture

Let's write a program...

  • Imperative Programming
  • Procedural Programming
    • Functions
  • Functional Programming
  • Object Oriented Programming

Let's write a program...

  • This is a toy example.
  • There are better ways to do what we're going to do.
  • But we want to illustrate how different programming needs influence the prgramming 'style'.
  • In our example we deal with vectorial quantities (forces, velocities, ...).
  • In this example we deal with two 2dim vectors.
  • Here is our first vector.

In [1]:
x = 5
y = 7
  • And here is another one:

In [2]:
x2 = -3   # oops maybe the choice of variable names is not optimal
y2 = 17
  • In our example we want to apply some vector arithmetics.
  • Let's first add those two vectors:

In [4]:
x3 = x + x2
y3 = y + y2

print(x3, y3)


(2, 24)
  • Now, what is the length of this new vector?
  • Well, we know that $\qquad|v| = \sqrt{v_x^2+v_y^2}$.

In [6]:
from math import sqrt
length_3 = sqrt(x3 * x3 + y3 * y3)

print length_3


24.0831891576
  • How does this length compare to the length of the first vector?

In [8]:
length_1 = sqrt(x * x + y * y)
print length_1


8.60232526704

Hm, that's kind of annoying.

What we are doing here is called

<font, color='red'> Imperative Programming</font>

  • IP uses statements to change the state of a program.
  • Changes are here-and-now changes (see length calculation above).
  • IP goes back to the early days of computers when programming consisted of simple statements.
  • IP is very powerful when comes to data manipulation.
  • But...

Interlude: The Magical Number 7$\pm$2

  • George Miller
  • research on short-term memory capacity
  • 1956: paper on the Magical Number 7$\pm$2.
  • 7 $\pm$ 2 is the limit of storable information
  • But it's also a good rule of thumb when it comes to programming.

When you need more than 7 +/- 2 lines of code to describe something, it's very likely that you need to break down the problem in smaller chunks.

Back to our problem...

  • Imperative programming forced us to blow up our source code.
  • And we easily violated Miller's Rule!
  • Do we have to type the same thing over and over again?
  • Well, of course not!
  • We know that we can 'outsource' common program code in functions!

This is in short the idea of the procedural programming paradigm.

  • We know how to write a function:

In [10]:
def length(x, y):
    return sqrt(x* x + y * y)
  • What about other functions?

In [12]:
def vector_sum(vx1, vy1, vx2, vy2):
    x = vx1 + vx2
    y = vy1 + vy2
    return x, y      #Nice, there's no restriction on the amount of return values.

In [13]:
def scalar_prod(vx1, vy1, vx2, vy2):
    return (vx1 * vx2 + vy2 * vy2)
  • Looks like we can re-write our length function:

In [14]:
def length(vx1, vy1):
    return sqrt(scalar_prod(vx1, vy1, vx1, vy1))   #hmmmm, a function of a function

Let's give it a try:


In [17]:
print length(3, 4)


5.0

Functions seem to be a powerful programming tool.

It might be a good idea to go through Python and its function functionality.

More on Functions

Function Defintion


In [ ]:
def functionname( parameters ):
   "function_docstring"
   function_suite
   return [expression]
  • We have dealt with the actual function code.
  • We know about the return value(s).
  • But what about the parameters?

Function Arguments (Parameters)

  • required arguments
  • keyword arguments
  • default arguments
  • variable length arguments

required arguments


In [18]:
def foo( s ):
    print(s)
    
foo()


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-18-1aef52df3582> in <module>()
      2     print(s)
      3 
----> 4 foo()

TypeError: foo() takes exactly 1 argument (0 given)

keyword arguments

Keyword argument allow the caller to identify the arguments by their parameter names:


In [19]:
def foo(name, age):
    print("name:", name)
    print("age: ", age)

foo('Alice', 35)
print('----')
foo(19, 'Bob')
print('----')
foo(age = 78, name = 'Dave')


('name:', 'Alice')
('age: ', 35)
----
('name:', 19)
('age: ', 'Bob')
----
('name:', 'Dave')
('age: ', 78)

default arguments


In [22]:
def foo(name, age = 23): 
    print("name:", name)
    print("age: ", age)
  

foo('Alice')
foo('Bob', 29)


('name:', 'Alice')
('age: ', 23)
('name:', 'Bob')
('age: ', 29)
  • The application of default arguments seem to be obvious.
  • But there are some not so obvious applications: <font, color='red'> method overriding</font>.
  • MO might be known from other languages:
    • Define multiple functions with the same name
    • Functions are then distinguished by their number and type of arguments.
  • Python can do something similar (and in most cases it's more elegant).
  • Here is a simple example $\Rightarrow$

In [25]:
def vector_sum(vx1, vy1, vx2, vy2, vz1=None, vz2=None):
    if vz1 == None or vz2 == None:             # making sure that both values are set
        return vx1 + vx2, vy1 + vy2
    else:
        return vx1 + vx2, vy1 + vy2, vz1 + vz2

In [27]:
print vector_sum(1, 2, 3, 4)


(4, 6)

In [28]:
print vector_sum(1, 2, 3, 4, 8,8)


(4, 6, 16)

Careful with the amount of code inside a function (7$\pm$2 rule)!

  • Default arguments are pretty powerful!
  • However, there's a caveat here.
  • Default arguments are evaluated at definition-time and not at call-time.
  • What does that entail? Here is an example of a function that logs a message together with a time stamp:

In [31]:
import datetime as dt     # What's that?

print dt.datetime.now()


2017-11-08 10:56:53.681709

In [33]:
def log_time(message, time=dt.datetime.now()):
    print("{0}: {1}".format(time.isoformat(), message))

In [34]:
log_time("message 1")


2017-11-08T10:56:56.496843: message 1

In [36]:
log_time("message 2")


2017-11-08T10:56:56.496843: message 2

In [37]:
log_time("message 3")


2017-11-08T10:56:56.496843: message 3

One more example: default arguments and mutable variables:


In [38]:
def foo(value, a=[]):
    a.append(value)
    return a

In [39]:
print(foo(1))


[1]

In [40]:
print(foo(2))


[1, 2]

In [41]:
print(foo('uh oh'))


[1, 2, 'uh oh']

Here's how it should look like:


In [42]:
def foo(value, a=None):
    if a == None:
        a = []
    
    a.append(value)
    return a

In [43]:
print(foo(1))


[1]

In [44]:
print(foo(2))


[2]

In [45]:
print(foo('yeah!!!'))


['yeah!!!']

variable-length arguments


In [46]:
def foo(arg1, *argtuple):
    print(arg1)
    for arg in argtuple:
        print(arg)
    
foo('Hello World')
foo('x', 1, 'hello', 3.1415, '20')
foo()


Hello World
x
1
hello
3.1415
20
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-46-09acbea4f0ce> in <module>()
      6 foo('Hello World')
      7 foo('x', 1, 'hello', 3.1415, '20')
----> 8 foo()

TypeError: foo() takes at least 1 argument (0 given)
  • Procedural programming seems to be enourmos improvement over imperative programming.
    • code maintainance
    • debugging
    • code encapsulation (the user doesn't need to know about the implementation)
    • code sharing
  • It's natural to ask Can we extend the concept of procedural programming?
  • Yes, it's called <font, color='red'> Functional Programming</font>

What is Functional Programming?

Everything is a function!

Everything?

Everything!!!

Even loops?

Even loops!!!

  • Functional programming (FP) surely sounds like a radical approach.
  • But not for a software engineer or a mathematician ($\lambda$-calculus).
  • Especially mathematicians deal with expressions like this all the time: $\qquad f(g(h(x)))$
  • Mapping such a construct into a source code surely has it's advantages.
  • Python was never designed with FP in mind.
  • That being said, FP works quite well in Python.
  • However, it probably requires more than just an hour to dive into the details.

Let's go back to out little program...

  • We structured our code with the help of functions/methods/procedures.
  • In principle we can extend our code by adding new methods.
  • And we caneven share code by exchanging functions.
  • Now a colleague is interested in our little vector program.
  • He wants to do some vector calculations as well.
  • Unfortunately, he doesn't have vectors with cartesian coordinates but polar coordinates.
  • No problem! We write a function that converts all the polar coordinate tuples to cartesian coordinate tuples.
  • That works fine.
  • Until a third colleague comes along.
  • He now has both, vectors in cartesian coordinates and vectors in polar coordinates.
  • On top of the conversion function we need to test now what kind of vector we have.
  • It looks like that most of the code is now about conversion and book-keeping.
  • But this is really beside the actual goal: Vector Operations.
  • And you wonder, there should be a better way to organize the code...
  • In principle a vector is a vector is a vector.
  • Its behavior invariant under the choice of a coordinate system.
  • Can we actually map a vector (its properties) in our code?
  • What if we coulf define an object that is a 2dim vector?
  • <font, color='red'>Object Oriented Programming</font> does exactly that.

Object Oriented Programming

Let's start with some theory

  • Objects are representations of real world things: cars, dogs, bank accounts, vectors, ...
  • Objects share two main characteristics: data and behaviour
    • Cars have data: number of wheels, velocity, power, weight,...
    • Cars have behaviors: drive, brake, move windshield wipers, honk, ...
  • What about vectors? -data: length, direction -behavior: scale by a factor, add another vector,...
  • In Python we don't talk about data and behaviour.
  • data $\Rightarrow$ attributes
  • behaviour $\Rightarrow$ methods
  • We define the blueprint of an object in a class.
  • Let's have a look at some syntax and then the concept should get clearer.

In Python we define a class like this:


In [47]:
class Vehicle:
    pass

Obviously this class is pretty boring. Nevertheless, we can now create objects of type Vehicle by instanciating:


In [48]:
car = Vehicle()
print(car)


<__main__.Vehicle instance at 0x10d8fb0e0>

We just created an object that we called car of type Vehicle.

  • Let's add some attributes and methods to our class.
  • In order to initialize an object with a set of attributes we use the __init__ method:

In [49]:
class Vehicle():
    def __init__(self, number_of_wheels, number_of_seats, max_velocity):
        self.number_of_wheels = number_of_wheels
        self.number_of_seats = number_of_seats
        self.max_velocity = max_velocity

In [50]:
car = Vehicle(4, 5, 200.0)
bike = Vehicle(2, 2, 50.0)
print car.number_of_wheels
print bike.number_of_seats


4
2
  • We call __init__ the constructor method.
  • self represents the instance of the class.
  • By using the self keyword we can access the attributes and methods of the class in python.
  • Let's add some more methods.

In [51]:
class Vehicle():
    def __init__(self, number_of_wheels, number_of_seats, max_velocity):
        self.number_of_wheels = number_of_wheels
        self.number_of_seats = number_of_seats
        self.max_velocity = max_velocity
        
    def get_max_velocity(self):
        return self.max_velocity
    
    def make_noise(self):
        print("vrummmmm")

In [52]:
car = Vehicle(4, 5, 200.0)
print car.get_max_velocity()
print car.max_velocity
car.make_noise()


200.0
200.0
vrummmmm
  • We have know now all the basics to actually implement our vector problem $\Rightarrow$

In [53]:
from math import cos, sin, sqrt

class MyVector():
    def __init__(self, x=None, y=None, r=None, phi=None):
        if x == None or y == None:
            self.x = r * cos(phi)
            self.y = r * sin(phi)
        else:
            self.x = x
            self.y = y
        
    def get_length(self):
        return sqrt(self.x * self.x + self.y * self.y)
    
    def scale_vector(self, a):
        self.x *= a
        self.y *= a
        
    def add_vector(self, vector):
        self.x += vector.x
        self.y += vector.y

In [54]:
v1 = MyVector(10, 20)
print v1.get_length()
v2 = MyVector(r=10, phi=2.7)
print v2.get_length()
v1.add_vector(v2)
print v1.get_length()


22.360679775
10.0
24.2927463184
  • That's almost everything we had defined before.
  • But now I do not have to deal with coordinates.
  • The implementation is nicely hidden.
  • But how do we define vector arithmetics?
  • We would need to tell Python how to add two MyVector objects.

In [55]:
v1 = MyVector(10, 20)
v2 = MyVector(20, 10)
v3 = v1 + v2


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-55-0e604cf78aa6> in <module>()
      1 v1 = MyVector(10, 20)
      2 v2 = MyVector(20, 10)
----> 3 v3 = v1 + v2

TypeError: unsupported operand type(s) for +: 'instance' and 'instance'

In [64]:
from math import cos, sin, sqrt

class MyVector():
    def __init__(self, x=None, y=None, r=None, phi=None):
        if x == None or y == None:
            self.x = r * cos(phi)
            self.y = r * sin(phi)
        else:
            self.x = x
            self.y = y
        
    def get_length(self):
        return sqrt(self.x * self.x + self.y * self.y)
    
    def scale_vector(self, a):
        self.x *= a
        self.y *= a
        
    def add_vector(self, vector):
        self.x += vector.x
        self.y += vector.y
        
    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return MyVector(x, y)

In [63]:
v1 = MyVector(10, 20)
v2 = MyVector(20, 10)
v3 = v1 + v2
print(v3.x, v3.y)


(-10, 10)
  • Wait, there's more!
  • We found a way to map real objects in source code.
  • But so far we have ignored a possible hierarchical relation among objects.
  • For example: animal -> mammal -> cat
  • It looks like in such a hierarchy some properties are shared and some are not.
  • It's not too far-fetched to assume that this sort of hierarchies makes also sense in programming.
  • It's called inheritance and it works in Python like this:

In [71]:
from math import cos, sin, sqrt

class MyVector():
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def get_length(self):
        return sqrt(self.x * self.x + self.y * self.y)
    
    def scale_vector(self, a):
        self.x *= a
        self.y *= a
        
    def add_vector(self, vector):
        self.x += vector.x
        self.y += vector.y
        
    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return MyVector(x, y)
    
    def get_r(self):
        return self.get_length()

In [72]:
class MyPolarVector(MyVector):
    def __init__(self, r, phi):
        self.r = r
        MyVector.__init__(self, r * cos(phi), r * sin(phi))
    
    def get_length(self):
        print "inside MyPolarVector"
        return self.r
    
    def get_r(self):
        return self.r

In [73]:
v1 = MyVector(10, 20)
v2 = MyPolarVector(12, 2.7)

print (v1 + v2).get_r()

print v1.get_length()
print v2.get_length()
print (v1 + v2).get_length()


25.1428922049
22.360679775
inside MyPolarVector
12
25.1428922049
  • That was a crash course in OO programming.
  • We haven't touched topics like data encapsulation, Class and instance variables, ...
  • Unfortunately, this is beyond the scope of our lecture.
  • Let's compare the programming paradigms and finish with another OO example.

Comparing the four programming paradigms

Imperative: Computation is performed as a direct change to program state. This style is especially useful when manipulating data structures and produces elegant, but simple, code.


In [74]:
my_list = [1, 2, 3, 4, 5]
sum = 0
for x in my_list:
    sum += x
print(sum)


15

The focus of imperative programming is on how a program operates. It changes state information as needed in order to achieve a goal.

Procedural: Tasks are treated as step-by-step iterations where common tasks are placed in functions that are called as needed. This coding style favors iteration, sequencing, selection, and modularization.


In [75]:
def do_add(a_list):
    sum = 0
    if type(a_list) is list:
        for x in a_list:
            sum += x
    return sum
my_list = [1, 2, 3, 4, 5]
print(do_add(my_list))


15
  • The procedural style relies on procedure calls to create modularized code.
  • It seeks to simplify the application code by creating small pieces that a developer can view easily.
  • Even though the procedural coding style is an older form of application development, it’s still a viable approach when a task lends itself to step-by-step execution.

Functional: Any form of state or mutable data are avoided. The main advantage of this approach is that there aren’t any side effects to consider. In addition, this coding style lends itself well to parallel processing because there is no state to consider.


In [76]:
import functools
my_list = [1, 2, 3, 4, 5]
def add(x, y):
    return (x + y)
sum = functools.reduce(add, my_list) 
# calculates add(add(add(add(1, 2), 3), 4), 5)

print(sum)


15

The functional coding style treats everything like a math equation.

Object-oriented: Relies on data fields that are treated as objects and manipulated only through prescribed methods. Python doesn’t fully support this coding form because it can’t implement features such as data hiding. This coding style also favors code reuse.


In [77]:
class ChangeList:
    def __init__(self, a_list):
        self.my_list = []
        if type(a_list) is list:
            self.my_list = a_list
    def do_add(self):
        self.my_sum = 0
        for x in self.my_list:
            self.my_sum += x
create_sum_obj = ChangeList([1, 2, 3, 4, 5])
create_sum_obj.do_add()
print(create_sum_obj.my_sum)


15

The object-oriented coding style is all about increasing the ability of applications to reuse code and making code easier to understand. The encapsulation that object-orientation provides allows developers to treat code as a black box.

Summary

  • We have seen various approaches to structure source code.
  • Python leaves it up to you to choose and pick.
  • None of them is right or wrong.
  • The choice strongly depends on your problem.
  • You need something 'quick and dirty': Imperative Style
  • You are working on a larger project that maybe even involves several developers: OO
  • Another way to look at it, is how data is being manipulated.
  • You have a fixed set of operations on things: OO
  • You have a fixed set of things: Functional/Procedural Programming

Exercise

  • In this week's exercise you are supposed to get familiar with OOP.
  • We want to write the code for a linked list.
  • A linked list is a container like lists, arrays, dictionaries, etc.
  • The speciality of a linked list is that each element in a list (called node) contains also a pointer to the following element.

  • Here is the code for a node:

In [78]:
class ListNode:  
    def __init__(self, data):
        "constructor to initiate this object"

        # store data
        self.data = data

        # store reference (next item)
        self.next = None
        return

    def contains_value(self, value):
        "method to compare the value with the node data"
        if self.data == value:
            return True
        else:
            return False

In [79]:
node1 = ListNode('data point 1')
node2 = ListNode([15, 16 , 3.14])
node3 = ListNode(16.6)

No we want to write a class that represents a linked list. We identify the following methods as required:

  • __init__(): initiate an object
  • list_length(): return the number of nodes
  • output_list(): outputs the node values
  • add_list_item(): add a node at the end of the list
  • unordered_search(): search the list for the nodes with a specified value
  • remove_list_item_by_id(): remove the node according to its id

In [ ]:
class SingleLinkedList:  
    def __init__(self):
        "constructor to initiate this object"

        self.head = None
        self.tail = None
        return
    
    def list_length(self):
        pass
    
    def output_list(self):
        pass
    
    def add_list_item(self, item):
        pass
    
    def unordered_search(self):
        pass
    
    def remove_list_item_by_id(self):
        pass

In [ ]: