Alexander Kozlov <akozlov@csc.kth.se>

In this exercise we deal with class inheritance and method overloading.

Calling methods from the base class

Dynamic method order resolution

All classes inherit from the base class object. This is the default base class. Instances of a subclass have acess to a parent class method via built-in function super(type, obj), where type is the subclass name and obj is a subclass instance. Calling super().method() looks up definition of method() within the inheritance tree and resolves it dynamically during run-time.

Simple inheritance and overloading:

class C1:
  def __init__(self):
    self.i = 1

class C2(object):
  def __init__(self):
    super().__init__()
    self.i = 1

class C3(object):
  def __init__(self):
    super(C3, self).__init__()
    self.i = 1

a = C1()
b = C2()
c = C3()
print(a.i, b.i, c.i)
1 1 1

Multiple inheritance:

class First(object):
  def __init__(self):
    print("First")
    super().__init__()
  def f(self):
    print('f() in First')

class Second(object):
  def __init__(self):
    print("Second")
    super().__init__()
  def f(self):
    print('f() in Second')

class Third(First, Second):
  def __init__(self):
    print("Third")
    super().__init__()
  def g(self):
    print('g() in Third')
    print('calls f() in Second')
    Second.f(self)

a = Third()
a.f()
a.g()
Third
First
Second
f() in First
g() in Third
calls f() in Second
f() in Second

In case of complicated inheritance tree, avoid using super() and call constructors explicitly.

class A(object):
    def __init__(self):
        print('A')
        self.data = None

class B(A):
    def __init__(self):
        print('B')
        A.__init__(self)
        self.data = None

class C(A):
    def __init__(self):
        print('C')
        A.__init__(self)
        self.data = None

class D(B, C):
    def __init__(self):
        print('D')
        B.__init__(self)
        C.__init__(self)
        self.data = None

d = D()
D
B
A
C
A

Derive class Contact from the base classes Person and Address and use their methods to print out the contact information.

class Address:
    def __init__(self, street, city):
        self.street = str(street)
        self.city = str(city)
    def show(self):
        print(self.street)
        print(self.city)

class Person:
    def __init__(self, name, email):
        self.name = str(name)
        self.email= str(email)
    def show(self):
        print(self.name + ' ' + self.email)

class Contact(Person, Address):
    def __init__(self, name, email, street, city):
        Person.__init__(self, name, email)
        Address.__init__(self, street, city)
    def show(self):
        Person.show(self)
        Address.show(self)
        print()

class Notebook:
    people = dict()
    def add(self, name, email, street, city):
        self.people[name] = Contact(name, email, street, city)
    def show(self, name):
        if name in self.people:
            self.people[name].show()
        else:
            print('Unknown', name)

notes = Notebook()
notes.add('Alice', '<al@kth.se>', 'Lv 24', 'Sthlm')
notes.add('Bob', '<bb@kth.se>', 'Rtb 35', 'Sthlm')

notes.show('Alice')
notes.show('Carol')
Alice <al@kth.se>
Lv 24
Sthlm
Unknown Carol

Creating user-defined types

Derive class Par (parameter value with name) from class float and make use of it.

class Par(float):
    name = ''
    def __str__(self):
        if self.name:
            return '%f (%s)' % (self, self.name)
        else:
            return '%f' % self

a = Par(1.1)
a.name = 'alpha'

b = Par(2.2)
b.name = 'beta'

t = [1, 2, b, a, a+b, a*b, a/b]

print(t)
t.sort()
for s in t:
    print(s)
[1, 2, 2.2, 1.1, 3.3000000000000003, 2.4200000000000004, 0.5]
0.5
1
1.100000 (alpha)
2
2.200000 (beta)
2.4200000000000004
3.3000000000000003

Create class Param which defines parameter with attributes n (name) and v (value) and defines or overloads some useful methods.

class Param:
  def __init__(self, n, v):
    self.n = str(n)
    self.v = v
  def __str__(self):
    return ('%s= %f' % (self.n, self.v))
  def __lt__(self, x):
    return self.v < x.v
  def __eq__(self, x):
    return self.v == x.v
  def __add__(self, x):
    if isinstance(x, Param):
      return Param('('+ self.n  + '+' + x.n +')', self.v+x.v)
    else:
      return Param('('+ self.n  + '+' + str(x) +')', self.v+x)
  def __mul__(self, x):
    if isinstance(x, Param):
      return Param('('+ self.n  + '*' + x.n +')', self.v*x.v)
    else:
      return Param('('+ self.n  + '*' + str(x) +')', self.v*x)

p1 = Param('a', 1.1)
p2 = Param('b', 2.2)
p3 = Param('c', 3.3)

t = [p3, p2, p1, p1+2, p1+p2, p1+p2*p3]

for s in t:
  print(s)

for s in sorted(t):
  print(s)
c= 3.300000
b= 2.200000
a= 1.100000
(a+2)= 3.100000
(a+b)= 3.300000
(a+(b*c))= 8.360000
a= 1.100000
b= 2.200000
(a+2)= 3.100000
c= 3.300000
(a+b)= 3.300000
(a+(b*c))= 8.360000