Introduction to objects and classes in Python

We will touch upon some basic aspects, including

  • code reuse
  • abstraction
  • Encapsulation
  • subclasses and hierarchies

In [8]:
class Dog:
    def __init__(self, name):
        self.age = 0
        self.name = name
        self.noise = "Woof!"
        self.food = "dog biscuits"
    
    def make_sound(self):
        print(self.noise)
        
    def eat_food(self):
        print("Eating " + self.food + ".")
        
    def increase_age(self, n = 1):
        self.age = self.age + n

d1 = Dog('Buster')
d1.make_sound()
d2 = Dog('Tiger')
d2.noise = 'Bark'
d2.make_sound()
d1.make_sound()
d1.eat_food()
d1.increase_age(3)
print(d1.age)


Woof!
Bark
Woof!
Eating dog biscuits.
3

In [9]:
class Cat:
    def __init__(self, name):
        self.age = 0
        self.name = name
        self.noise = "Meow!"
        self.food = "cat food"
    
    def make_sound(self):
        print(self.noise)
        
    def eat_food(self):
        print("Eating " + self.food + ".")
        
    def increase_age(self, n = 1):
        self.age = self.age + n

c1 = Cat('Harvey')
c1.make_sound()
c1.eat_food()


Meow!
Eating cat food.

In the above examples, it becomes clear that there is much repetition, and we can make the code more compact. Let us abstract common functionality into an abstract class.


In [3]:
from abc import ABCMeta, abstractmethod
class Mammal(metaclass=ABCMeta):

    @abstractmethod
    def __init__(self, name):
        self.age = 0
        self.name = name
        self.noise = "None!"
        self.food = "none"
    
    def make_sound(self):
        print(self.name + " says " + self.noise)
        
    def eat_food(self):
        print(self.name + " is eating " + self.food + ".")
        
    def increase_age(self, n = 1):
        self.age = self.age + n

class Dog(Mammal):
    def __init__(self, name):
        super(Dog, self).__init__(name)
        self.noise = "Bark!"
        self.food = "dog biscuits"

class Cat(Mammal):
    def __init__(self, name):
        super(Cat, self).__init__(name)
        self.noise = "Meow!"
        self.food = "cat food"

d = Dog("Buster")
c = Cat("Harvey")
d.make_sound()
c.make_sound()
c.eat_food()
m = Mammal("Name")
m.make_sound()
m.eat_food()
import sys
print(sys.version)


Buster says Bark!
Harvey says Meow!
Harvey is eating cat food.
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-ff50dc6e4225> in <module>()
     35 c.make_sound()
     36 c.eat_food()
---> 37 m = Mammal("Name")
     38 m.make_sound()
     39 m.eat_food()

TypeError: Can't instantiate abstract class Mammal with abstract methods __init__

In [23]:
animal_house = [Dog("MyDog" + str(i))
                for i in range(1, 5)]
animal_house.extend([Cat("MyCat" + str(i))
                     for i in range(1, 5)])
for i in animal_house:
    i.make_sound()


MyDog1 says Bark!
MyDog2 says Bark!
MyDog3 says Bark!
MyDog4 says Bark!
MyCat1 says Meow!
MyCat2 says Meow!
MyCat3 says Meow!
MyCat4 says Meow!

Iterators

How do I add an iteration facility to my own objects so that they can be used with for loops?


In [40]:
class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def next(self): # def next(self): in Python 2!
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

rev = iter(Reverse([10, 30, 200, 0.0, 'ABC']))

for i in rev:
    print(i)


ABC
0.0
200
30
10

Generators

Use functions to create iterators, instead of classes


In [31]:
def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]

for char in reverse("Madam, I'm Adam"):
    print(char)


m
a
d
A
 
m
'
I
 
,
m
a
d
a
M

Summing series

We want to efficiently find the sum $$ \sum_{n = 0}^N \frac{1}{n^2} $$

As $N$ becomes larger, this becomes $\frac{\pi^2}{6}$.


In [3]:
import math
def series_sum(max_terms=1000):
    n = 0
    while n < max_terms:
        n = n + 1
        yield 1.0 / n**2

print(sum(series_sum(100000)) - math.pi**2 / 6)


-9.999949984074163e-06

The key thing to note is that this is much more efficient than generating a list of terms in memory and summing it. That is, more efficient than


In [5]:
print(sum([1.0 / i**2 for i in range(1, 10000)]))


1.6448340618480652

Decorators

Alter the behaviour of functions (somewhat)

Example:

  • I wish to print the arguments to a function before I call it.
  • One way: Edit the function. But it's clumsy!
  • Better way, wrap it in another function:

In [51]:
def add_numbers(a, b):
    return a + b

def arg_wrapper(f, *args, **kwargs):
    print("The function arguments are:")
    print(args)
    print(kwargs)
    print("Now running the function!")
    return f(*args, **kwargs)

#print(add_numbers(1, 2))
#print(arg_wrapper(add_numbers, 1, 2))

def myfunction(name='Test', age=30):
    print("Name: %s, Age: %d" % (name, age))

arg_wrapper(myfunction, name='Harvey', age=3)


The function arguments are:
()
{'age': 3, 'name': 'Harvey'}
Now running the function!
Name: Harvey, Age: 3

In [57]:
import time

def timing_function(some_function):
    def wrapper():
        t1 = time.time()
        some_function()
        t2 = time.time()
        return "Time it took to run the function: " + str((t2 - t1)) + "\n"
    return wrapper

@timing_function
def my_function():
    num_list = []
    for num in (range(0, 10000)):
        num_list.append(num)
    print("\nSum of all the numbers: " + str((sum(num_list))))


my_function()


Sum of all the numbers: 49995000
Out[57]:
'Time it took to run the function: 0.0010094642639160156\n'