Retrospective

  • Interpreter
  • Basic Calculations
  • Data Structures
  • Control flow
  • Functions
  • Documentation

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.


In [17]:
import time

t = time.localtime()
t.tm_mday


Out[17]:
19

Pypi

Python has a very active development community and the Python package index contains a myriad of packages for every need.

These can be install by using pip which is installed with newer python versions:

pip install numpy

Object Orientation

Programming Paradigms

Object orientation in Python

  • everything is a object (even functions)

You can define your own objects by using the class keyword:


In [18]:
class Dog(object):
    color = "yellow"
    def __init__(self, name):
            self.name = name
            
Dog


Out[18]:
__main__.Dog

Instances

Classes can be used to create objects with certain attributes and behavior:


In [19]:
class Dog(object):
    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()


whoof, whoof!

Inheritance

Classes can inherit traits (and even skills) from other classes:


In [20]:
class Dachshund(Dog):
    color = "brownish"
    def __init__(self, name):
        super(Dachshund, self).__init__(name, 20)
        
d = Dachshund('bello')
d.bark()


whoof, whoof!

Shadowing

Classes can alter functionality of their parents:


In [21]:
class Pekingese(Dog):
    color = "brownish"
    def __init__(self, name):
        super(Pekingese, self).__init__(name, 15)
        
    def bark(self):
        print 'whif.'
        
d = Pekingese('tiny')
d.bark()


whif.

Special methods


In [78]:
class Pekingese(Dog):
    color = "brownish"
    def __init__(self, name):
        super(Pekingese, self).__init__(name, 15)

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

In [79]:
p = Pekingese("wang")
len(p)


Out[79]:
15

Mating


In [77]:
class Dog(object):
    color = "yellow"
    def __init__(self, name, size):
            self.name = name
            self.size = size
            
    def __add__(self, dog):
        if not isinstance(dog, Dog):
            raise TypeError
        return Dog(self.name + " " + dog.name, (self.size + dog.size)/2)
    
    def bark(self):
        print 'whoof, whoof!'

In [89]:
b = Dog("blitzer", 100)
dog = b + p
dog.bark()
print dog.name
print dog.size


whoof, whoof!
blitzer wang
57

Why do I need this?

OOP facilitates some desirable features in software projects:

  • code organization
  • encapsulation
  • DRY principle
  • modularization

How we will use it

Our project will make extensive use of object orientation and inheritance.

  • 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

Decorators

Some helpful syntax

Nested Functions

Functions can contain other functions:


In [22]:
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()


out here
in room 1
in room 2
  • try to call inner_1

Wrappers

Decorators are functions wrapped around functions:


In [23]:
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)

In [24]:
polite_talker = greetings(say_something)

polite_talker("Good to see you")


Hello!
Say: Good to see you
Goodbye!

Decorator Syntax


In [25]:
@greetings
def shout(sentence):
    print sentence.upper()

In [26]:
shout("Am I polite, or what?!")


Hello!
AM I POLITE, OR WHAT?!
Goodbye!

How will we use it?


In [27]:
class Molecule(object):
    def __init__(self, mass):
        self.mass = mass
        
    @property
    def mass(self):
        return self.__mass
    
    @mass.setter
    def mass(self, value):
        if not isinstance(value, int):
            raise TypeError("Mass must be Integer.")
        self.__mass = value

In [28]:
m = Molecule(1)

In [29]:
m.mass = "two"


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-29-045978fabf9a> in <module>()
----> 1 m.mass = "two"

<ipython-input-27-dca2e1068b33> in mass(self, value)
     10     def mass(self, value):
     11         if not isinstance(value, int):
---> 12             raise TypeError("Mass must be Integer.")
     13         self.__mass = value

TypeError: Mass must be Integer.
  • getter and setter methods

Further Tricks

Some python specialties you should know about

Copy

By Reference/Value

As you might know you can assign values of one variable to an other like:


In [30]:
A = [5]
B = A
print B


[5]

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


In [31]:
A[0] = 1
print B


[1]

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.


In [32]:
A = 1
B = A
A += 1
print B


1

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.


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


[[2, 3, 1, 4]]
[[1, 2, 3, 4]]

However, some operations directly create new objects.


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


[[2, 3, 1]]
[1, 2, 3]
[[2, 3, 1]]

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.


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


A:  (1, 2, 3, 3, 4, 5)
B:  (1, 2, 3)

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.


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


A:  [1, 2, 3, 1]
B:  [1, 2, 3, 1]

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

Copy

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


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


A:  [1]
B:  [5]

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


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


A:  [[1]]
B:  [[1]]

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.


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


A:  [[1]]
B:  [[5]]

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


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


zipping:  [(1, 'a'), (2, 'b'), (3, 'c')]
unzipping:  [(1, 2, 3), ('a', 'b', 'c')]

Dictionaries

Creating dictionaries


In [41]:
# 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


dictionary comprehension:  {'a': 0, 'c': 4, 'b': 1, 'e': 16, 'd': 9}

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


dictionary:  {'A': 1, 'C': 3, 'B': 2}

In [43]:
# default dictionary
from collections import defaultdict
default_dict = defaultdict(int)
default_dict['a'] += 1  # increasing the value of key "a" directly during the initialisation of the key
print "default dictionary: ", default_dict

# normal dictionary initialisation
no_default = dict()
no_default['a'] += 1


default dictionary:  defaultdict(<type 'int'>, {'a': 1})
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-43-c4f0387b3348> in <module>()
      7 # normal dictionary initialisation
      8 no_default = dict()
----> 9 no_default['a'] += 1

KeyError: 'a'

Inverting dictionaries


In [44]:
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


dictionary:  {'a': 1, 'c': 3, 'b': 2}
inverted dict:  {1: 'a', 2: 'b', 3: 'c'}

Ordered dictionaries


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


ordered dictionary:  OrderedDict([('a', 0), ('b', 1), ('c', 4), ('d', 9), ('e', 16)])

Itertools

Sliding window


In [46]:
import itertools
def k_mer(word, size):
    z = (itertools.islice(word, i, None) for i in range(size))
    return zip(*z)

string = "HelloWorld"
k_mer(string,3)


Out[46]:
[('H', 'e', 'l'),
 ('e', 'l', 'l'),
 ('l', 'l', 'o'),
 ('l', 'o', 'W'),
 ('o', 'W', 'o'),
 ('W', 'o', 'r'),
 ('o', 'r', 'l'),
 ('r', 'l', 'd')]

Permutations


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


permutations:  (1, 2, 3)
permutations:  (1, 3, 2)
permutations:  (2, 1, 3)
permutations:  (2, 3, 1)
permutations:  (3, 1, 2)
permutations:  (3, 2, 1)

Flatting Lists


In [48]:
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


a_flatted_list:  [1, 2, 3, 4, 5, 6]