x = 5
y = 7

"""
Explanation: 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.
End of explanation
"""


x2 = -3   # oops maybe the choice of variable names is not optimal
y2 = 17

"""
Explanation: And here is another one:
End of explanation
"""


x3 = x + x2
y3 = y + y2

print(x3, y3)

"""
Explanation: In our example we want to apply some vector arithmetics.
Let's first add those two vectors:
End of explanation
"""


from math import sqrt
length_3 = sqrt(x3 * x3 + y3 * y3)

print length_3

"""
Explanation: Now, what is the length of this new vector?
Well, we know that $\qquad|v| = \sqrt{v_x^2+v_y^2}$.
End of explanation
"""


length_1 = sqrt(x * x + y * y)
print length_1

"""
Explanation: How does this length compare to the length of the first vector?
End of explanation
"""


def length(x, y):
    return sqrt(x* x + y * y)

"""
Explanation: 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 <font color='red'> procedural programming paradigm</font>.

We know how to write a function:
End of explanation
"""


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.

def scalar_prod(vx1, vy1, vx2, vy2):
    return (vx1 * vx2 + vy2 * vy2)

"""
Explanation: What about other functions?
End of explanation
"""


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

"""
Explanation: Looks like we can re-write our length function:
End of explanation
"""


print length(3, 4)

"""
Explanation: Let's give it a try:
End of explanation
"""


def functionname( parameters ):
   "function_docstring"
   function_suite
   return [expression]

"""
Explanation: 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
End of explanation
"""


def foo( s ):
    print(s)
    
foo()

"""
Explanation: 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
End of explanation
"""


def foo(name, age):
    print("name:", name)
    print("age: ", age)

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

"""
Explanation: keyword arguments
Keyword argument allow the caller to identify the arguments by their parameter names:
End of explanation
"""


def foo(name, age = 23): 
    print("name:", name)
    print("age: ", age)
  

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

"""
Explanation: default arguments
End of explanation
"""


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

print vector_sum(1, 2, 3, 4)

print vector_sum(1, 2, 3, 4, 8,8)

"""
Explanation: 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$
End of explanation
"""


import datetime as dt     # What's that?

print dt.datetime.now()

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

log_time("message 1")

log_time("message 2")

log_time("message 3")

"""
Explanation: 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:
End of explanation
"""


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

print(foo(1))

print(foo(2))

print(foo('uh oh'))

"""
Explanation: One more example: default arguments and mutable variables:
End of explanation
"""


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

print(foo(1))

print(foo(2))

print(foo('yeah!!!'))

"""
Explanation: Here's how it should look like:
End of explanation
"""


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

"""
Explanation: variable-length arguments
End of explanation
"""


class Vehicle:
    pass

"""
Explanation: 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:
End of explanation
"""


car = Vehicle()
print(car)

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


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

car = Vehicle(4, 5, 200.0)
bike = Vehicle(2, 2, 50.0)
print car.number_of_wheels
print bike.number_of_seats

"""
Explanation: 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:
End of explanation
"""


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")

car = Vehicle(4, 5, 200.0)
print car.get_max_velocity()
print car.max_velocity
car.make_noise()

"""
Explanation: 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.
End of explanation
"""


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

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()

"""
Explanation: We have know now all the basics to actually implement our vector problem $\Rightarrow$
End of explanation
"""


v1 = MyVector(10, 20)
v2 = MyVector(20, 10)
v3 = v1 + v2


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)

v1 = MyVector(10, 20)
v2 = MyVector(20, 10)
v3 = v1 + v2
print(v3.x, v3.y)

"""
Explanation: 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.
End of explanation
"""


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()

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

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()



"""
Explanation: 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:
End of explanation
"""


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

"""
Explanation: 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.
End of explanation
"""


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))

"""
Explanation: 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.
End of explanation
"""


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)

"""
Explanation: 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.
End of explanation
"""


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)

"""
Explanation: 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.
End of explanation
"""


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

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

"""
Explanation: 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:
End of explanation
"""


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

"""
Explanation: 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
End of explanation
"""


