# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# + [markdown] slideshow={"slide_type": "slide"}
# # Retrospective  
#
# - Interpreter
# - Basic Calculations
# - Data Structures
# - Control flow
# - Functions
# - Documentation

# + [markdown] slideshow={"slide_type": "slide"}
# # Packages
#
# Packages are folders containing Modules. If you are lucky, these modules also work together in a way. The folder needs to contain an `__init__.py` file that will tell python what to do with the package.

# + slideshow={"slide_type": "fragment"}
import time

t = time.localtime()
t.tm_mday


# + [markdown] slideshow={"slide_type": "fragment"}
# ## Pypi
#
# Python has a very active development community and the [**Py**thon **p**ackage **i**ndex](www.pypi.python.org) contains a myriad of packages for every need.

# + [markdown] slideshow={"slide_type": "fragment"}
# These can be install by using `pip` which is installed with newer python versions:
# ```
# pip install numpy
# ```

# + [markdown] slideshow={"slide_type": "slide"}
# # Object Orientation
#
# ![](http://www.codercaste.com/wp-content/uploads/2011/01/animals.png)

# + [markdown] slideshow={"slide_type": "subslide"}
# # Programming Paradigms
#
# ![](http://academic.udayton.edu/SaverioPerugini/courses/cps343/lecture_notes/images/summary.png)

# + [markdown] slideshow={"slide_type": "slide"}
# # Object orientation in Python
#
# - everything is a object (even functions)
#
# You can define your own objects by using the `class` keyword:

# + slideshow={"slide_type": "fragment"}
class Dog:
    color = "yellow"
    def __init__(self, name):
        self.name = name
            
Dog


# + [markdown] slideshow={"slide_type": "subslide"}
# # Instances
#
# Classes can be used to create objects with certain attributes and behavior:

# + slideshow={"slide_type": "fragment"}
class Dog:
    color = "yellow"
    def __init__(self, name, size):
        self.name = name
        self.size = size
    def bark(self):
        print('whoof, whoof!')

b = Dog("blitzer", 100)
b.bark()


# + [markdown] slideshow={"slide_type": "subslide"}
# # Inheritance
#
# Classes can inherit traits (and even **skills**) from other classes:

# + slideshow={"slide_type": "fragment"}
class Dachshund(Dog):
    color = "brownish"
    def __init__(self, name):
        super().__init__(name, 20)
        
d = Dachshund('bello')
d.bark()


# + [markdown] slideshow={"slide_type": "subslide"}
# ## Shadowing
#
# Classes can alter functionality of their parents:

# + slideshow={"slide_type": "fragment"}
class Pekingese(Dog):
    color = "brownish"
    def __init__(self, name):
        super().__init__(name, 15)
        
    def bark(self):
        print('whif.')
        
d = Pekingese('tiny')
d.bark()


# + [markdown] slideshow={"slide_type": "subslide"}
# # Special methods

# + slideshow={"slide_type": "fragment"}
class Pekingese(Dog):
    color = "brownish"
    def __init__(self, name):
        super().__init__(name, 15)

    def __len__(self):
        return self.size
        
    def bark(self):
        print('whif.')


# + slideshow={"slide_type": "fragment"}
p = Pekingese("wang")
len(p)


# -

# # Mating

# + slideshow={"slide_type": "fragment"}
class Dog:
    color = "yellow"
    def __init__(self, name, size):
        self.name = name
        self.size = size
            
    def __add__(self, other):
        if not isinstance(other, Dog):
            raise TypeError
        return Dog(self.name + "-" + other.name, (self.size + other.size)/2)
    
    def bark(self):
        print('whoof, whoof!')


# + slideshow={"slide_type": "fragment"}
p = Dog('bello', 25)
b = Dog("blitzer", 100)
dog = b + p
dog.bark()
print(dog.name)
print(dog.size)


# + [markdown] slideshow={"slide_type": "subslide"}
# # Why do I need this?
#
# OOP facilitates some desirable features in software projects:
#
# - code organization
# - encapsulation
# - DRY principle
# - modularization

# + [markdown] slideshow={"slide_type": "subslide"}
# # How we will use it
#
# Our project will make extensive use of object orientation and inheritance.
# ![Class diagramm](http://1.bp.blogspot.com/-tWAOmi7yyRs/T-6x2UB0NZI/AAAAAAAAAMI/fOivujFWstE/s1600/CLASS+DIAGRAM.png)

# + [markdown] slideshow={"slide_type": "notes"}
# - classes use each other to run the model
# - each process knows which species to update, makes modularization possible
# - simulation class manages everything, registers processes
# - states/processes inherit from base class

# + [markdown] slideshow={"slide_type": "slide"}
# # Decorators
#
# Some helpful syntax

# + [markdown] slideshow={"slide_type": "subslide"}
# # Nested Functions
#
# Functions can contain other functions:

# +
def outer_function():
    print("out here")
    def inner_1():
        return "in room 1"
        
    def inner_2():
        return "in room 2"
        
    print(inner_1())
    print(inner_2())
        
outer_function()


# + [markdown] slideshow={"slide_type": "subslide"}
# ## Wrappers
# Decorators are functions wrapped around functions:

# + slideshow={"slide_type": "fragment"}
def greetings(some_function):
    def wrapper(args):
        print("Hello!")
        some_function(args)
        print("Goodbye!")
    return wrapper

def say_something(sentence):
    print("Say: {0}".format(sentence))


# + slideshow={"slide_type": "fragment"}
polite_talker = greetings(say_something)

polite_talker("Good to see you")


# + [markdown] slideshow={"slide_type": "subslide"}
# # Decorator Syntax 

# + slideshow={"slide_type": "fragment"}
@greetings
def shout(sentence):
    print(sentence.upper())


# + slideshow={"slide_type": "fragment"}
shout("Am I polite, or what?!")


# + [markdown] slideshow={"slide_type": "subslide"}
# # How will we use it?

# +
class Molecule(object):
    def __init__(self, mass):
        self.__mass = mass
        
    def get_mass(self):
        return self.__mass
    
    def set_mass(sefl, value):
        if not isinstance(value, int):
            raise TypeError("Mass must be Integer.")
        self.__mass = value
        
m = Molecule(12)
m.set_mass('13')


# + slideshow={"slide_type": "fragment"}
class Molecule(object):
    def __init__(self, mass):
        self.mass = mass
        
    @property
    def mass(self):
        print("getter")
        return self.__mass
    
    @mass.setter
    def mass(self, value):
        print("setter")
        if not isinstance(value, int):
            raise TypeError("Mass must be Integer.")
        self.__mass = value


# + slideshow={"slide_type": "fragment"}
m = Molecule(1)
# -

m.mass 

# + slideshow={"slide_type": "fragment"}
m.mass = "two"

# + [markdown] slideshow={"slide_type": "notes"}
# - getter and setter methods

# + [markdown] slideshow={"slide_type": "skip"}
# # Further Tricks
# ## Some python specialties you should know about

# + [markdown] slideshow={"slide_type": "skip"}
# ## Copy

# + [markdown] slideshow={"slide_type": "skip"}
# ### By Reference/Value

# + [markdown] slideshow={"slide_type": "skip"}
# As you might know you can assign values of one variable to an other like:

# + slideshow={"slide_type": "skip"}
A = [5]
B = A
print(B)
# -

# But if you change the value of variable A as it is done blow, variable B changes, too. 

A[0] = 1
print(B)

# The problem is that variables are just names referring to an object. Assigning the object of a variable to an other does not create a copy of this object. It creates a new variable B which refers to the same object A refers to. Hence, there is only one object but two variables are referring to this one. This holds only for mutable objects like lists, sets, dictionaries and so on.
#
# There are also immutable objects like inits, floats, strings, tuple and so on. 

A = 1
B = A
A += 1
print(B)

# The difference in this example is, that we are not incrementing the value of A, but instead we are creating a new object and assigning this new object to our variable A. After this we have two objects the Integer 1 and the Integer 2 and two variables B and A referring to the objects, respectively.
#
# Some operations change the object instead creating a copy of the object assigned to a variable.

A = [[2,3,1]]
B = A
A[0].append(4)
print(B)
A[0].sort()
print(B)

# However, some operations directly create new objects.

A = [[2,3,1]]
B = A
A = A + [4]
print(B)
print(sorted(A[0]))
print(B)

# So we end up with two objects and two variables referring to these objects, respectively.
#
# But we have to pay attention when we use operations like +=. The behaviour of this kind of operations depends on the object it is applied on.

A = ((1,2,3))  # assigning a immutable tuple object to A
B = A
A += (3,4,5)
print("A: ", A)
print("B: ", B)

# If we apply this operation on a variable referring to an immutable object - a new object is created, but if we apply this to an object referring to a mutable object like lists, sets and so on we are just changing the object.

A = [1,2,3]  # assigning a mutable list object to A
B = A
A += [1]
print("A: ", A)
print("B: ", B)

# In this case the += operation on a list is equivalent to the A.extend([1]) operation.

# + [markdown] slideshow={"slide_type": "skip"}
# ### Copy
# -

# The copy operation can be used to make copies of an object. Like

import copy
A = [5]
B = copy.copy(A)
A[0] = 1
print("A: ", A)
print("B: ", B)

# The copy operation creates a copy of the object (in this case a list) assigned to variable A and set references of the inner objects. The inner object (integer 1) is immutable, hence, the variable B will not change if we change A. But if we have something like 

A = [[5]]
B = copy.copy(A)
A[0][0] = 1
print("A: ", A)
print("B: ", B)

# We will change variable B if we change the inner object of variable A. This is because we have a copy of the first object (outer list) and just set a reference of the inner object. This brings us back to the problem we discussed above.

# ### Deepcopy

# To avoid this we have the possibility to make a deepcopy of our object assigned to a variable.

import copy
A = [[5]]
B = copy.deepcopy(A)
A[0][0] = 1
print("A: ", A)
print("B: ", B)

# With this operation we are creating a new object of the first object and for all nested objects, too. This means that the entire structure is copied and we end up with two objects (containing arbitray nested objects) assigned to two variables.

# ### Zipping unzipping lists and iterables

A = [1, 2, 3]
B = ['a', 'b', 'c']
z = list(zip(A, B))
print("zipping: ", z)
print("unzipping: ", list(zip(*z)))

# ## Dictionaries

# ### Creating dictionaries

# dictionary comprehension
name_space = "a","b","c","d","e"
dictionary = {name_space[x]: x**2 for x in range(len(name_space))}
print("dictionary comprehension: ", dictionary)

# creating a dictionary using zip function
dictionary = dict(zip(['A', 'B', 'C'], [1, 2, 3]))
print("dictionary: ", dictionary)

# +
# default dictionary
from collections import defaultdict
default_dict = defaultdict(list)
default_dict['a'].append(12)  # if a value has not been set for 'a', initialize with empty list
print("default dictionary: ", default_dict)

# normal dictionary initialisation
no_default = dict()
no_default['a'].append(12)
# -

# ### Inverting dictionaries

# +
dictionary = dict(a=1, b=2, c=3)  # normal dict generation
print("dictionary: ", dictionary)

# inverting dictionary using zip function
inverted_dict = dict(zip(dictionary.values(), dictionary.keys()))
print("inverted dict: ", inverted_dict)
# -

# ### Ordered dictionaries

from collections import OrderedDict
ordered_dict = OrderedDict((name_space[x], x**2) for x in range(len(name_space)))
print("ordered dictionary: ", ordered_dict)

# ## Itertools

# ### Sliding window

# +
import itertools
def k_mer(word, size):
    z = (itertools.islice(word, i, None) for i in range(size))
    return zip(*z)

string = "HelloWorld"
list(k_mer(string,3))
# -

# ### Permutations

import itertools
for p in itertools.permutations([1,2,3]):
    print("permutations: ", p)

# ## Flatting Lists

# +
import itertools
a_list = [[1,2],[3,4],[5,6]]

a_flatted_list = [x for i in a_list for x in i]
print("a_flatted_list: ", a_flatted_list)

# -



StarcoderdataJupyterScriptsDedupFiltered