Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Object Oriented Programming in Python

Why Object Oriented Programming?

  • Better encapsulation of intent.
  • Integration between data and functionality (attributes and methods)
  • Better modelling for some part of the world.
  • Another level of code-reuse.
  • Clearer separation between "usage" and "implementation". (Private data in some cases)
  • Clearer connection between "classes" of things.
  • In reality: avoid using "global".

Generic Object Oriented Programming terms

  • OOP differs a lot among programming languages!
  • Classes (blueprints)
  • Objectes / instances (actual)
  • Members: Attributes and Methods
  • Attributes / Properties (variables - data)
  • Methods (functions) (private, public, virtual)
  • Inheritance (is a)
  • Composition (has a)
  • Constructor
  • Destructor

OOP in Python

  • Everything is an object
  • Numbers, strings, list, ... even classes are objects.
  • Class objects
  • Instance objects
  • Nothing is private.

OOP in Python (numbers, strings, lists)

There are programming languages such as Java and C# that are Object Oriented languages where in order to do anything, even to print to the screen you need to understand OOP and implement a class.

Python is Object Oriented in a different way. You can get by without creating your own classes for a very long time in your programming career, but you are actually using features of the OOP nature of Python from the beginning.

In Python they say "everything is an object" and what they mean is that everything, including literal values such as numbers or strings, or variables holding a list are instances of some class and that they all the features an instance has. Most importantly they have methods. Methods are just function that are used in the "object.method()" notation instead of the "function( parameter )" notation.

Some of these methods change the underlying object (e.g. the append method of lists), some will return a copy of the object when the object is immutable. (e.g. the capitalize method of strings).


# numbers
print((255).bit_length())    # 8
print((256).bit_length())    # 9
x = 255
print(x.bit_length())
x = 256
print(x.bit_length())

# strings
print( "hello WOrld".capitalize() )  # Hello world
print( ":".join(["a", "b", "c"]) )   # a:b:c


# lists
numbers = [2, 17, 4]
print(numbers)        # [2, 17, 4]

numbers.append(7)
print(numbers)        # [2, 17, 4, 7]

numbers.sort()
print(numbers)        # [2, 4, 7, 17]

OOP in Python (argparse)

There are more complex OOP usage cases that you have surely encountered already in Python. Either while programming or in my course. For example parsing the command line arguments using argparse.

Here we call the ArgumentParser() method of the argparse object to create an instance of the argparse.ArgumentParser class. Then we call the add_argument method a few times and the parse_args method. This returns an instance of the argparse.Namespace class.

So in fact you have already used OOP quite a lot while using various already existing classes and instances of those classes.

Now we are going to learn how can you create your own classes.

import argparse

def get_args():
    print(type(argparse))            # <class 'module'>

    parser = argparse.ArgumentParser()
    print(parser.__class__)          # <class 'argparse.ArgumentParser'>
    print(parser.__class__.__name__) # ArgumentParser

    parser.add_argument('--name')
    parser.add_argument('--email')

    # print(dir(parser))
    # print( parser.format_help() )
    # parser.print_help()

    return parser.parse_args()

args = get_args()
print(args.__class__)          # <class 'argparse.Namespace'>
print(args.__class__.__name__) # Namespace

print(args.name)      # None

Create a class

  • class
  • class
  • name
  • dir

In order to create a class in Python you only need to use the class keyword with a new class-name. Usually the first letter is capitalized.

In such a minimal class that does not do anything yet, Python still requires us to write some code.

class Point:
    pass

Create instance of class

class Point:
    pass

p1 = Point()
print(p1)                    # <__main__.Point object at 0x7f1cc1e3d1c0>
print(type(p1))              # <class '__main__.Point'>
print(p1.__class__.__name__) # Point

Import module containing class

You probably want your classes to be reusabel by multiple programs, so it is better to put the class and your code using it in separate files right from the beginning. In this example you can see how to do that importing the module and then using the dot notation to get to the class.

import shapes

p = shapes.Point()
print(p)          # <shapes.Point instance at 0x7fb58c31ccb0>
class Point:
    pass

Import class from module

Alternatively you can import the class from the modue and then you can use the classname without any prefix.

from shapes import Point

p = Point()
print(p)          # <shapes.Point instance at 0x7fb58c31ccb0>

Initialize instance (not a constructor)

  • init
class Point:
    def __init__(self):
       pass

p1 = Point()
print(p1)    # <__main__.Point object at 0x7f57922ec1c0>

Self is the instance

Self is already the instance that will be returned

class Point:
    def __init__(self):
       print('in __init__')
       print(self)

pnt = Point()
print(pnt)

# in __init__
# <__main__.Point object at 0x7ff3f45821c0>
# <__main__.Point object at 0x7ff3f45821c0>

Init uses same name as attribute and getters

  • init
  • self
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

from shapes2 import Point

p1 = Point(2, 3)
print(p1)          # <shapes2.Point object at 0x7f2c22c1ec70>
print(p1.x)        # 2
print(p1.y)        # 3

p2 = Point(y=7, x=8)
print(p2)          # <shapes2.Point object at 0x7f2c22c1e700>
print(p2.x)        # 8

Initialize an instance - (not a constructor), attributes and getters

  • init
  • self

In Python we dont explicitely declare attributes so what people usually do is add a method calles __init__ and let that method set up the initial values of the insance-object.

class Point:
    def __init__(self, a, b):
        self.x = a
        self.y = b

from shapes import Point

p1 = Point(2, 3)
print(p1)          # <shapes.Point instance at 0x7fb58c31ccb0>
print(p1.x)        # 2
print(p1.y)        # 3

p2 = Point(b=7, a=8)
print(p2)          # <shapes.Point instance at 0x7fb58c31cd00>
print(p2.x)        # 8
print(p2.y)        # 7

Setters - assign to the attributes

from shapes import Point

p1 = Point(4, 5)
print(p1.x)  # 4
print(p1.y)  # 5

p1.x = 23
p1.y = 17
print(p1.x)  # 23
print(p1.y)  # 17

Attributes are not special

  • There is no automatic protection from this
from shapes import Point

p1 = Point(4, 5)
print(p1.x)  # 4
print(p1.y)  # 5

p1.color = 'blue'
print(p1.color) # blue


p2 = Point(7, 8)
print(p2.x)  # 7
print(p2.y)  # 8
print(p2.color)  # AttributeError: 'Point' object has no attribute 'color'

Private attributes

class Thing:
     def __init__(self):
         self._name = 'This should be private'

t = Thing()
print(t._name)  # This should be private
print(dir(t))   # [..., '_name']

t._name = 'Fake'
print(t._name)  # Fake

Methods

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, dx, dy):
        self.x += dx
        self.y += dy
from shapes import Point

p1 = Point(2, 3)
print(p1.x)    # 2
print(p1.y)    # 3

p1.move(4, 5)
print(p1.x)    # 6
print(p1.y)    # 8


print(p1)      # <shapes.Point object at 0x7fb0691c3e48>

Inheritance

  • super
class Point:
    def __init__(self, x, y):
        print('__init__ of Point')
        self.x = x
        self.y = y

    def move(self, dx, dy):
        self.x += dx
        self.y += dy

class Circle(Point):
    def __init__(self, x, y, r):
        print('__init__ of Circle')
        super().__init__(x, y)
        self.r = r

    def area(self):
        return self.r * self.r * 3.14
import shapes

c = shapes.Circle(2, 3, 10)   # __init__ of Circle
                              # __init__ of Point
print(c)          # <shapes.Circle instance at 0x7fb58c31ccb0>
print(c.x)        # 2
print(c.y)        # 3
print(c.r)        # 10

c.move(4, 5)
print(c.x)        # 6
print(c.y)        # 8
print(c.area())   # 314.0

Inheritance - another level

class Point:
    def __init__(self, x, y):
        print('__init__ of Point')
        self.x = x
        self.y = y

class Circle(Point):
    def __init__(self, x, y, r):
        print('__init__ of Circle')
        super().__init__(x, y)
        self.r = r

    def area(self):
        return self.r * self.r * 3.14

class Ball(Circle):
    def __init__(self, x, y, r, z):
        print('__init__ of Ball')
        super().__init__(x, y, r)
        self.z = z


b = Ball(2, 3, 9, 7)
print(b)
print(b.area())

# __init__ of Ball
# __init__ of Circle
# __init__ of Point
# <__main__.Ball object at 0x103dea190>
# 254.34

Modes of method inheritance

  • Implicit
  • Override
  • Extend
  • Delegate - Provide

  • Composition

Modes of method inheritance - implicit

Inherit method

class Parent:
    def greet(self):
        print("Hello World")

class Child(Parent):
    pass

p = Parent()
p.greet()    # Hello World

c  = Child()
c.greet()    # Hello World

Modes of method inheritance - override

Replace method

class Parent():
    def greet(self):
        print("Hello World")

class Child(Parent):
    def greet(self):
        print("Hi five!")

p = Parent()
p.greet()
print()

c  = Child()
c.greet()
Hello World

Hi five!

Modes of method inheritance - extend

  • super

Extend method before or after calling original.

class Parent():
    def greet(self):
        print("Hello World")

class Child(Parent):
    def greet(self):
        print("Hi five!")
        super().greet()
        print("This is my world!")

p = Parent()
p.greet()
print()

c  = Child()
c.greet()
Hello World

Hi five!
Hello World
This is my world!

Modes of method inheritance - delegate - provide

Let the child implement the functionality.

class Parent():
    def greet(self):
        print("Hello", self.get_name())

class Child(Parent):
    def __init__(self, name):
        self.name = name

    def get_name(self):
        return self.name

# Should not create instance from Parent
p = Parent()
# p.greet()    # AttributeError: 'Parent' object has no attribute 'get_name'

c  = Child('Foo')
c.greet()    # Hello Foo
  • Should we have a version of greet() in the Parent that throws an exception?
  • Do we want to allow the creation of instance of the Parent class?
  • Abstract Base Class (abc)

Composition - Line

When an object holds references to one or more other objects.

Composition - Line with type annotation

class Point():
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Line():
    def __init__(self, a:Point, b:Point):
        self.a = a
        self.b = b

    def length(self):
        return ((self.a.x - self.b.x) ** 2 + (self.a.y - self.b.y) ** 2) ** 0.5

p1 = Point(2, 3)
p2 = Point(5, 7)
blue_line = Line(p1, p2)

print(blue_line.a) # <__main__.Point object at 0x0000022174B637B8>
print(blue_line.b) # <__main__.Point object at 0x0000022174B3C7B8>
print(blue_line.length())   # 5.0

xl = Line(4, 6)
print(xl)  # <__main__.Line object at 0x7fb15f8f5ee0>
mypy examples/oop/composition/line_with_types.py
examples/oop/composition/line_with_types.py:22: error: Argument 1 to "Line" has incompatible type "int"; expected "Point"
examples/oop/composition/line_with_types.py:22: error: Argument 2 to "Line" has incompatible type "int"; expected "Point"
Found 2 errors in 1 file (checked 1 source file)

Hidden attributes

  • Primarily useful to ensure inheriting classes don't accidently overwrite attributes.
class Thing:
    def __init__(self):
        self.__hidden = 'lake'

    def get_hidden(self):
        return self.__hidden
from hidden import Thing

t = Thing()
#print(t.__hidden)  # AttributeError: 'Thing' object has no attribute '__hidden'

print(t.get_hidden())    # lake

print(dir(t))            # ['_Thing__hidden',  ...]

print(t._Thing__hidden)  # lake

t._Thing__hidden = 'Not any more'
print(t._Thing__hidden)  # Not any more

Hidden attributes in a subclass

from hidden import Thing

class SubThing(Thing):
    def __init__(self):
        super().__init__()
        self.__hidden = 'river'

    def get_sub_hidden(self):
        return self.__hidden

st = SubThing()
print(dir(st))
print(st._Thing__hidden)
print(st._SubThing__hidden)


print(st.get_hidden())
print(st.get_sub_hidden())

Some comments

  • There are no private attributes. The convention is to use leading underscore to communicate to other developers what is private.
  • Using the name self for the current object is just a consensus.

Exercise: Add move_rad to based on radians

  • From the Python: Methods take the examples/oop/methods/shapes.py and add a method called move_rad(dist, angle) that accpets a distance and an angle and moved the point accordingly.
delta_x = dist * cos(angle)
delta_y = dist * sin(angle)

Exercise: Improve previous examples

  • Take the previous example Python: Inheritance - another level and the example file called examples/oop/inheritance/ball_shape.py and change it so the Ball class will accept x, y, z, r.
  • Add a method called move to the new Ball class that will accept dx, dy, dz.
  • Implement a method that will return the volume of the ball.

Exercise: Polygon

  • Implement a class representing a Point.
  • Make the printing of a point instance nice.
  • Implement a class representing a Polygon. (A list of points)
  • Allow the user to "move a polygon" calling poly.move(dx, dy) that will change the coordinates of every point by (dx, dy)
class Point():
    pass

class Polygon():
    pass

p1 = Point(0, 0)  # Point(0, 0)
p2 = Point(5, 7)  # Point(5, 7)
p3 = Point(4, 9)  # Point(4, 9)
print(p1)
print(p2)
print(p3)
p1.move(2, 3)
print(p1)         # Point(2, 3)

poly = Polygon(p1, p2, p3)
print(poly)       # Polygon(Point(2, 3), Point(5, 7), Point(4, 9))
poly.move(-1, 1)
print(poly)       # Polygon(Point(1, 4), Point(4, 8), Point(3, 10))

Exercise: Number

Turn the Number guessing game into a class. Replace every print statement with a call to an output method. Do the same with the way you get the input. Then create a subclass where you override these methods. You will be able to launch the game with a hidden value you decide upon. The input will feed a pre-defined list of values as guesses to the game and the output methods will collect the values that the game prints in a list.

Exercise: Library

Create a class hierarchy to represent a library that will be able to represent the following entities.

  • Author (name, birthdate, books)
  • Book (title, author, language, who_has_it_now?, is_on_waiting_list_for_whom?)
  • Reader (name, birthdate, books_currently_lending)

Methods:

  • write_book(title, language,)

Exercise: Bookexchange

It is like the library example, but instead of having a central library with books, each person owns and lends out books to other people.

Exercise: Represent turtle graphics

There is a cursor (or turtle) in the x-y two-dimensional sphere. It has some (x,y) coordinates. It can go forward n pixels. It can turn left n degrees. It can lift up the pencil or put it down.

Solution - Polygon

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return "Point({}, {})".format(self.x, self.y)

    def move(self, dx, dy):
        self.x += dx
        self.y += dy

class Polygon:
    def __init__(self, *args):
        self.points = args

    def __repr__(self):
        return 'Polygon(' + ', '.join(map(lambda p: str(p), self.points)) + ')'

    def move(self, dx, dy):
        for p in self.points:
            p.move(dx, dy)

p1 = Point(0, 0)  # Point(0, 0)
p2 = Point(5, 7)  # Point(5, 7)
p3 = Point(4, 9)  # Point(4, 9)
print(p1)
print(p2)
print(p3)
p1.move(2, 3)
print(p1)         # Point(2, 3)

poly = Polygon(p1, p2, p3)
print(poly)       # Polygon(Point(2, 3), Point(5, 7), Point(4, 9))
poly.move(-1, 1)
print(poly)       # Polygon(Point(1, 4), Point(4, 8), Point(3, 10))

Advanced OOP

Stringify class

  • str

  • repr

  • repr "should" return Python-like code

  • str should return readable representation

  • If str does not exist, repr is called instead.

class Point():
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
       return 'Point({}, {})'.format(self.x, self.y)

    def __str__(self):
       return '({}, {})'.format(self.x, self.y)
import shapes

p1 = shapes.Point(2, 3)
print(repr(p1)) # Point(2, 3)
print(str(p1))  # (2, 3)
print(p1)       # (2, 3)

Multiple inheritance

class ParentA:
    def __init__(self):
        print('__init__ of ParentA')

    def in_parent_a(self):
        print('in_parent_a')

    def in_both(self):
        print('in_both in parent A')

class ParentB:
    def __init__(self):
        print('__init__ of ParentB')

    def in_parent_b(self):
        print('in_parent_b')

    def in_both(self):
        print('in_both in paernt B')

class Child(ParentA, ParentB):
    def __init__(self):
        print('__init__ of Child')
        super().__init__()

    def in_child(self):
        print('in_child')

c = Child()
c.in_parent_a()
c.in_parent_b()
c.in_child()
c.in_both()

__init__ of Child
__init__ of ParentA
in_parent_a
in_parent_b
in_child
in_both in parent A

Multiple inheritance - diamond

  • Not Bot ParentA and ParentB inherit attributes from GrandParent,
  • but they are now merged.
class GrandParent:
    ...

class ParentA(GrandParent):
    ...

class ParentB(GrandParent):
    ...

class Child(ParentA, ParentB):
    ...

c = Child()

Interfaces

  • Parent and Child can have attributes
  • Tools only has methods
class Parent:
    def __init__(self):
        print('__init__ of Parent')

    def in_parent(self):
        print('in_parent')

class Tools:
    def some_tool(self):
        print('some_tool')

class Child(Parent, Tools):
    def __init__(self):
        print('__init__ of Child')
        super().__init__()

    def in_child(self):
        print('in_child')

c = Child()
c.in_parent()
c.some_tool()
c.in_child()

__init__ of Child
__init__ of Parent
in_parent
some_tool
in_child

Abstract Base Class

  • Create a class object that cannot be used to create an instance object. (It must be subclassed)
  • The subclass must implement certain methods required by the base-class.

Abstract Base Class with abc

  • abc

  • abstractmethod

  • abc

from abc import ABC, abstractmethod

class Base(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

ABC working example

from with_abc3 import Base

class Real(Base):
    def foo(self):
        print('foo in Real')

    def bar(self):
        print('bar in Real')

    def other(self):
        pass

r = Real('Jane')
print(r.name)      # Jane
Jane

ABC - cannot instantiate the base-class

from with_abc3 import Base

b = Base('Boss')
Traceback (most recent call last):
  File "with_abc3_base.py", line 3, in <module>
    b = Base('Boss')
TypeError: Can't instantiate abstract class Base with abstract methods bar, foo

ABC - must implement methods

from with_abc3 import Base

class Fake(Base):
    def foo(self):
        print('foo in Fake')

f = Fake('Joe')
Traceback (most recent call last):
  File "with_abc3_fake.py", line 7, in <module>
    f = Fake('Joe')
TypeError: Can't instantiate abstract class Fake with abstract methods bar

Class Attributes

  • Class attributes can be created inside a class.
  • Assign to class attribute and fetch from it.
  • Class attributes can be also created from the outside.
  • Creating a instance does not impact the class attribute.
class Person:
    name = 'Joseph'

print(Person.name)    # Joseph

Person.name = 'Joe'
print(Person.name)    # Joe

Person.email = 'joe@foobar.com'
print(Person.email)   # joe@foobar.com

x = Person()
print(Person.name)    # Joe
print(Person.email)   # joe@foobar.com

Class count instances

class Thing:
    count = 0
    def __init__(self):
        Thing.count += 1

def main():
    print(Thing.count)  # 0
    t1 = Thing()
    print(Thing.count)  # 1
    t2 = Thing()
    print(Thing.count)  # 2
    t3 = Thing()
    print(Thing.count)  # 3
    t3 = None
    print(Thing.count)  # 3

main()
print(Thing.count)  # 3

Destructor: del

  • init
  • del
class Thing:
    def __init__(self):
        print('__init__')
    def __del__(self):
        print('__del__')

def main():
    a = Thing()
    print('in main - after')

main()
print('after main')

__init__
in main - after
__del__
after main

Class count instances - decrease also (destructor: del)

  • del
class Thing:
    count = 0
    def __init__(self):
        Thing.count += 1
    def __del__(self):
        Thing.count -= 1

def main():
    print(Thing.count)  # 0
    t1 = Thing()
    print(Thing.count)  # 1
    t2 = Thing()
    print(Thing.count)  # 2
    t3 = Thing()
    print(Thing.count)  # 3
    t3 = None
    print(Thing.count)  # 2

main()
print(Thing.count)  # 0

Keep track of instances

def prt():
    print(list(Thing.things.keys()))

class Thing:
    things = {}
    def __init__(self):
        Thing.things[id(self)] = self

    def __del__(self):
        print('__del__')
        del(Thing.things[id(self)])


def main():
    prt()
    t1 = Thing()
    prt()
    t2 = Thing()
    prt()
    t3 = Thing()
    prt()
    t4 = None
    prt()

main()
prt()
[]
[140300136976832]
[140300136976832, 140300136335632]
[140300136976832, 140300136335632, 140300136334528]
[140300136976832, 140300136335632, 140300136334528]
[140300136976832, 140300136335632, 140300136334528]
__del__
__del__
__del__

Keep track of instances properly (weakref)

  • weakref
import weakref

def prt():
    print(list(Thing.things.keys()))

class Thing:
    things = {}
    def __init__(self):
        Thing.things[id(self)] = weakref.ref(self)

    def __del__(self):
        print('__del__')
        del(Thing.things[id(self)])


def main():
    prt()
    t1 = Thing()
    prt()
    t2 = Thing()
    prt()
    t3 = Thing()
    prt()
    t3 = None
    prt()

main()
prt()

[]
[139793357111744]
[139793357111744, 139793355980224]
[139793357111744, 139793355980224, 139793355980080]
__del__
[139793357111744, 139793355980224]
__del__
__del__
[]

Destructor delayed

Because the object has a reference to itself. (Python uses both reference count and garbage collection.)

class Thing:
    def __init__(self, name):
        self.name = name
        print(f'__init__ {name}')

    def __del__(self):
        print(f'__del__ {self.name}')

def main():
    a = Thing('A')
    b = Thing('B')
    a.partner = a
    print('in main - after')

main()
print('after main')

__init__ A
__init__ B
in main - after
__del__ B
after main
__del__ A

Destructor delayed for both

Because the instances reference each other

class Thing:
    def __init__(self, name):
        self.name = name
        print(f'__init__ for {self.name}')
    def __del__(self):
        print(f'__del__ for {self.name}')

def main():
    a = Thing('A')
    b = Thing('B')
    a.partner = b
    b.partner = a
    print('in main - after')

main()
print('after main')

__init__ for A
__init__ for B
in main - after
after main
__del__ for A
__del__ for B

Class Attributes in Instances

class Person:
    name = 'Joe'

# Class Attributes are inherited by object instances when accessing them.
print(Person.name)    # Joe
x = Person()
print(x.name)         # Joe
y = Person()
print(y.name)         # Joe

# Changes to class attribute are reflected in existing instances as well
Person.name = 'Bar'
print(Person.name)    # Bar
print(x.name)         # Bar


# Setting the attribute via the instance will create an instance attribute that shadows the class attribute:
x.name = 'Joseph'
print(x.name)         # Joseph

# You can still access the class attribute directly:
print(Person.name)    # Bar

# It does not impact the instance attribute of other instances:
print(y.name)         # Bar

# Both instance and class have a dictionary containing its members:
print(x.__dict__)       # {'name': 'Joseph'}
print(y.__dict__)       # {}
print(Person.__dict__)  # {..., 'name': 'Bar'}

Attributes with method access

  • Use a method (show) to access it.
class Person():
    name = 'Joe'
    print(f'Hello {name}')

    def show(self):
        print(Person.name)

x = Person()          # Hello Joe
x.show()              # Joe
print(x.name)         # Joe
print(Person.name)    # Joe

Person.name = 'Jane'
print(x.name)         # Jane
print(Person.name)    # Jane
x.show()              # Jane

x.name = 'Hilda'      # creating and setting the instance attribute
print(x.name)         # Hilda
print(Person.name)    # Jane

x.show()              # Jane

Methods are class attributes - add method

In this example we are going to add a newly created method to the class. (monkey patching)

class Person():
    def __init__(self, name):
        self.name = name

y = Person('Jane')
print(y.name)           # Jane

def show(some_instance):
    print("Hello " + some_instance.name)

Person.show = show
y.show()                # Hello Jane

Methods are class attributes - replace method

In this example we are going to replace the method in the class by a newly created function. (monkey patching)

class Person():
    def __init__(self, name):
        self.name = name

    def show(self):
        print(self.name)

y = Person('Jane')
print(y.name)    # Jane
y.show()         # Jane

def new_show(some_instance):
    print("Hello " + some_instance.name)

Person.show = new_show
y.show()         # Hello Jane

Methods are class attributes - Enhance method (Monkey patching)

class Circle():
    def __init__(self, x, y, r):
        self.x = x
        self.y = y
        self.r = r

    def area(self):
        print('in area')
        return self.r * self.r * 3.14

from shapes import Circle
from tools import add_debug

x = Circle(2, 3, 4)
print(x.area())
print('-----')

add_debug(Circle, 'area')

print(x.area())
# print('-----')
# print(x.area.__name__)


in area
50.24
-----
Before method
in area
After method
50.24
import functools

def add_debug(cls, method):
    original = getattr(cls, method)
    @functools.wraps(original)
    def debug(*args, **kwargs):
        print("Before method")
        result = original(*args, **kwargs)
        print("After method")
        return result
    setattr(cls, method, debug)

Method types

  • Instance methods - working on self
  • Class methods - working on the class (e.g. alternative constructor)
  • Static methods - have no self or class (helper functions)

Instance methods

Regular functions (methods) defined in a class are "instance methods". They can only be called on "instance objects" and not on the "class object" as see in the 3rd example.

The attributes created with "self.something = value" belong to the individual instance object.

class Date:
    def __init__(self, Year, Month, Day):
        self.year  = Year
        self.month = Month
        self.day   = Day

    def __str__(self):
        return 'Date({}, {}, {})'.format(self.year, self.month, self.day)

    def set_date(self, y, m, d):
        self.year = y
        self.month = m
        self.day = d

from mydate import Date

d = Date(2013, 11, 22)
print(d)

# We can call it on the instance
d.set_date(2014, 1, 27)
print(d)

# If we call it on the class, we need to pass an instance.
# Not what you would normally do.
Date.set_date(d, 2000, 2, 1)
print(d)


# If we call it on the class, we get an error
Date.set_date(1999, 2, 1)

set_date is an instance method. We cannot properly call it on a class.

Date(2013, 11, 22)
Date(2014, 1, 27)
Date(2000, 2, 1)
Traceback (most recent call last):
  File "run.py", line 17, in <module>
    Date.set_date(1999, 2, 1)
TypeError: set_date() missing 1 required positional argument: 'd'

Class methods

  • @classmethod

  • Access class attributes

  • Create alternative constructor

Class methods accessing class attributes

  • @classmethod

"total" is an attribute that belongs to the class. We can access it using Date.total. We can create a @classmethod to access it, but actually we can access it from the outside even without the class method, just using the "class object"

class Date:
    total = 0

    def __init__(self, Year, Month, Day):
        self.year  = Year
        self.month = Month
        self.day   = Day
        Date.total += 1

    def __str__(self):
        return 'Date({}, {}, {})'.format(self.year, self.month, self.day)

    def set_date(self, y, m, d):
        self.year = y
        self.month = m
        self.day = d

    @classmethod
    def get_total(class_object):
        print(class_object)
        return class_object.total
from mydate import Date

d1 = Date(2013, 11, 22)
print(d1)
print(Date.get_total())
print(Date.total)
print('')

d2 = Date(2014, 11, 22)
print(d2)
print(Date.get_total())
print(Date.total)
print('')

d1.total = 42
print(d1.total)
print(d2.total)
print(Date.get_total())
print(Date.total)

Date(2013, 11, 22)
<class 'mydate.Date'>
1
1

Date(2014, 11, 22)
<class 'mydate.Date'>
2
2

42
2
<class 'mydate.Date'>
2
2

Default Constructor

  • The "class" keyword creates a "class object". The default constructor of these classes are their own names.
  • The actual code is implemented in the __new__ method of the object.
  • Calling the constructor will create an "instance object".

Alternative constructor with class method

  • @classmethod

Class methods are used as Factory methods, they are usually good for alternative constructors. In order to be able to use a method as a class-method (Calling Date.method(...) one needs to mark the method with the @classmethod decorator)

  • Normally we create a Date instance by passing 3 numbers for Year, Monh, Day.
  • We would also like to be able to create an instance using a string like this: 2021-04-07
class Date:
    def __init__(self, Year, Month, Day):
        self.year  = Year
        self.month = Month
        self.day   = Day

    def __str__(self):
        return 'Date({}, {}, {})'.format(self.year, self.month, self.day)

    def set_date(self, y, m, d):
        self.year = y
        self.month = m
        self.day = d

    @classmethod
    def from_str(cls, date_str):
        '''Call as
           d = Date.from_str('2013-12-30')
        '''
        print(cls)
        year, month, day = map(int, date_str.split('-'))
        return cls(year, month, day)

from mydate import Date

d = Date(2013, 11, 22)
print(d)

d.set_date(2014, 1, 27)
print(d)
print('')

x = Date.from_str('2013-10-20')
print(x)
print('')

# This works but it is not recommended
z = d.from_str('2012-10-20')
print(d)
print(z)
Date(2013, 11, 22)
Date(2014, 1, 27)

<class 'mydate.Date'>
Date(2013, 10, 20)

<class 'mydate.Date'>
Date(2014, 1, 27)
Date(2012, 10, 20)

Static methods

  • @staticmethod

Static methods are used when no "class-object" and no "instance-object" is required. They are called on the class-object, but they don't receive it as a parameter.

class Date(object):
    def __init__(self, Year, Month, Day):
        if not Date.is_valid_date(Year, Month, Day):
            raise Exception('Invalid date')
        self.year  = Year
        self.month = Month
        self.day   = Day

    def __str__(self):
        return 'Date({}, {}, {})'.format(self.year, self.month, self.day)

    @staticmethod
    def is_valid_date(year, month, day):
        if 0 <= year <= 3000 and  1 <= month <= 12 and 1 <= day <= 31:
            return True
        else:
            return False


import mydate

a = mydate.Date(2013, 10, 20)
print(a)

print(mydate.Date.is_valid_date(2013, 10, 40))

b = mydate.Date(2013, 13, 20)

Date(2013, 10, 20)
False
Traceback (most recent call last):
  File "run.py", line 8, in <module>
    b = mydate.Date(2013, 13, 20)
  File "static_method/mydate.py", line 4, in __init__
    raise Exception('Invalid date')
Exception: Invalid date

Module functions

Static methods might be better off placed in a module as simple functions.

def is_valid_date(year, month, day):
    if 0 <= year <= 3000 and  1 <= month <= 12 and 1 <= day <= 31:
        return True
    else:
        return False

class Date(object):
    def __init__(self, Year, Month, Day):
        if not is_valid_date(Year, Month, Day):
            raise Exception('Invalid date')
        self.year  = Year
        self.month = Month
        self.day   = Day

    def __str__(self):
        return 'Date({}, {}, {})'.format(self.year, self.month, self.day)


import mydate

a = mydate.Date(2013, 10, 20)
print(a)

print(mydate.is_valid_date(2013, 10, 40))

b = mydate.Date(2013, 13, 20)

Date(2013, 10, 20)
False
Traceback (most recent call last):
  File "run.py", line 8, in <module>
    b = mydate.Date(2013, 13, 20)
  File "module_function/mydate.py", line 10, in __init__
    raise Exception('Invalid date')
Exception: Invalid date

Class and static methods

  • @classmethod
  • @staticmethod
def other_method(val):
    print(f"other_method: {val}")

class Date(object):
    def __init__(self, Year, Month, Day):
        self.year  = Year
        self.month = Month
        self.day   = Day

    def __str__(self):
        return 'Date({}, {}, {})'.format(self.year, self.month, self.day)

    @classmethod
    def from_str(class_object, date_str):
        '''Call as
           d = Date.from_str('2013-12-30')
        '''
        print(f"from_str: {class_object}")
        year, month, day = map(int, date_str.split('-'))

        other_method(43)

        if class_object.is_valid_date(year, month, day):
            return class_object(year, month, day)
        else:
            raise Exception("Invalid date")

    @staticmethod
    def is_valid_date(year, month, day):
        if 0 <= year <= 3000 and  1 <= month <= 12 and 1 <= day <= 31:
            return True
        else:
            return False

import mydate

dd = mydate.Date.from_str('2013-10-20')
print(dd)

print('')
print(mydate.Date.is_valid_date(2013, 10, 20))
print(mydate.Date.is_valid_date(2013, 10, 32))
print('')

x = mydate.Date.from_str('2013-10-32')

from_str: <class 'mydate.Date'>
other_method: 43
Date(2013, 10, 20)

True
False

from_str: <class 'mydate.Date'>
other_method: 43
Traceback (most recent call last):
  File "run.py", line 11, in <module>
    x = mydate.Date.from_str('2013-10-32')
  File "/home/gabor/work/slides/python-programming/examples/classes/mydate4/mydate.py", line 26, in from_str
    raise Exception("Invalid date")
Exception: Invalid date

Special methods

  • str

  • repr

  • __str__

  • __repr__

  • __eq__

  • __lt__

  • ...

Opearator overloading

  • mul
  • rmul
import copy

class Rect:
    def __init__(self, w, h):
        self.width  = w
        self.height = h

    def __str__(self):
        return 'Rect[{}, {}]'.format(self.width, self.height)

    def __mul__(self, other):
        o = int(other)
        new = copy.deepcopy(self)
        new.height *= o
        return new
import shapes

r = shapes.Rect(10, 20)
print(r)
print(r * 3)
print(r)

print(4 * r) 
Rect[10, 20]
Rect[10, 60]
Rect[10, 20]
Traceback (most recent call last):
  File "rect.py", line 8, in <module>
    print(4 * r) 
TypeError: unsupported operand type(s) for *: 'int' and 'Rect'

In order to make the multiplication work in the other direction, one needs to implement the rmul method.

Operator overloading methods

  • mul
  • rmul
  • add
  • radd
  • iadd
  • lt
  • le
*    __mul__,  __rmul__
+    __add__, __radd__
+=   __iadd__
<    __lt__
<=   __le__
...

Declaring attributes (dataclasses)

  • Starting from 3.7 dataclasses
  • Typehints are required but not enforced!
from dataclasses import dataclass

@dataclass
class Point():
    x : float
    y : float
    name : str

from shapes import Point

p1 = Point(2, 3, 'left')
print(p1.x)    # 2
print(p1.y)    # 3
print(p1.name) # left

p1.x = 7       # 7
print(p1.x)

p1.color = 'blue'
print(p1.color)   # blue

p1.x = 'infinity' # infinity
print(p1.x)


Dataclasses and repr

  • __repr__ is implemented
from shapes import Point

p1 = Point(2, 3, 'left')
print(p1)    # Point(x=2, y=3, name='left')

Dataclasses and eq

  • __eq__ is automatically implemented
from shapes import Point

p1 = Point(2, 3, 'left')
print(p1.x)    # 2
print(p1.y)    # 3
print(p1.name) # left

p2 = Point(2, 3, 'left')
p3 = Point(2, 3, 'right')

print(p1 == p2)  # True
print(p1 == p3)  # False

eq are automatically implemented

Dataclasses create init and call post_init

  • __init__ is implemented and that's how the attributes are initialized
  • __post_init__ is called after __init__ to allow for further initializations
from dataclasses import dataclass

@dataclass
class Point():
    x : float
    y : float
    name : str

    def __post_init__(self):
        print(f"In post init: {self.name}")
from shapes import Point

p1 = Point(2, 3, 'left')

Dataclasses can provide default values to attributes

from dataclasses import dataclass

@dataclass
class Point():
    x : float = 0
    y : float = 0
    name : str = 'Nameless'
from shapes import Point

p1 = Point(2, 3, 'left')
print(p1)  # Point(x=2, y=3, name='left')

p2 = Point()
print(p2) # Point(x=0, y=0, name='Nameless')

p3 = Point( name = 'Good', x = 42)
print(p3) # Point(x=42, y=0, name='Good')

  • Attributes with default values must before attributes without default

Dataclasses and default factory

from dataclasses import dataclass, field

@dataclass
class Fruits():
    # names : list = []  # ValueError: mutable default <class 'list'> for field names is not allowed: use default_factory
    names : list = field(default_factory=lambda : [])


f1 = Fruits()
f1.names.append('Apple')
f1.names.append('Banana')
print(f1)      # Fruits(names=['Apple', 'Banana'])


f2 = Fruits(['Peach', 'Pear'])
print(f2)      # Fruits(names=['Peach', 'Pear'])

Read only (frozen) Dataclass

  • @dataclass(frozen = True) makes the class immutable
from dataclasses import dataclass

@dataclass(frozen = True)
class Point():
    x : float
    y : float
    name : str
from shapes import Point

p1 = Point(2, 3, 'left')
print(p1)           # Point(x=2, y=3, name='left')
# p1.x = 7          # dataclasses.FrozenInstanceError: cannot assign to field 'x'
# p1.color = 'blue' # dataclasses.FrozenInstanceError: cannot assign to field 'color'

Serialization of instances with pickle

  • pickle
import pickle

class aClass(object):
    def __init__(self, amount, name):
        self.amount = amount
        self.name = name


the_instance = aClass(42, "FooBar")

a = {
    "name": "Some Name",
    "address" : ['country', 'city', 'street'],
    'repr' : the_instance,
}

print(a)

pickle_string = pickle.dumps(a)

b = pickle.loads(pickle_string)

print(b)

print(b['repr'].amount)
print(b['repr'].name)

Class in function

def creator():
    class MyClass:
        def __init__(self):
            print('__init__ of MyClass')

    print('before creating instance')
    o = MyClass()
    print(o)
    print(o.__class__.__name__)

creator()

# before creating instance
# __init_ of MyClass
# <__main__.creator.<locals>.MyClass object at 0x7fa4d8d581c0>
# MyClass

# Cannot use it outside of the function:
# MyClass()  # NameError: name 'MyClass' is not defined

Exercise: rectangle

Take the Rect class in the shapes module. Implement rmul, but in that case multiply the width of the rectangle.

Implement the addition of two rectangles. I think this should be defined only if one of the sides is the same, but if you have an idea how to add two rectangualars of different sides, then go ahead, implement that.

Also implement all the comparision operators when comparing two rectangles, compare the area of the two. (like less-than) Do you need to implement all of them?

Exercise: SNMP numbers

  • SNMP numbers are strings consisting a series of integers separated by dots: 1.5.2, 3.7.11.2
  • Create a class that can hold such an snmp number. Make sure we can compare them with less-than (the comparision is pair-wise for each number until we find two numbers that are different. If one SNMP number is the prefix is the other then the shorter is "smaller").
  • Add a class-method, that can tell us how many SNMP numbers have been created.
  • Write a separate file to add unit-tests

Exercise: Implement a Gene inheritance model combining DNA

  • A class representing a person. It has an attribute called "genes" which is string of letters. Each character is a gene.
  • Implement the + operator on genes that will create a new "Person" and for the gene will select one randomly from each parent.
a = Person('ABC')
b = Person('DEF')

c = a + b
print(c.gene) # ABF

Exercise: imaginary numbers - complex numbers

Create a class that will represent imaginary numbers (x, y*i) and has methods to add and multiply two imaginary numbers.

The math:

z1 = (x1 + y1*i)
z2 = (x2 + y2*i)
z1+z2 = (x1 + x2  + (y1 + y2)*i)

z1*z2 = x1*y1 + x2*y2*i*i + x1*y2*i + x2*y1*i

Add operator overloading so we can really write code like:

z1 = Z(2, 3)
z2 = Z(4, 7)

zz = z1*z2
z = complex(2, 3)
print(z)
print(z.real)
print(z.imag)

imag = (-1) ** 0.5
print(imag)

i = complex(0, 1)
print(i)
print(i ** 2)
(2+3j)
2.0
3.0
(6.123233995736766e-17+1j)
1j
(-1+0j)

Solution: Rectangle

import copy
import shapes

class Rectangular(shapes.Rect):

    def __rmul__(self, other):
        o = int(other)
        new = copy.deepcopy(self)
        new.width *= o
        return new

    def area(self):
        return self.width * self.height

    def __eq__(self, other):
        return self.area() == other.area()

    def __add__(self, other):
        new = copy.deepcopy(self)
        if self.width == other.width:
            new.height += other.height
        elif self.height == other.height:
            new.width += other.width
        else:
            raise Exception('None of the sides are equal')
        return new



import shape2
import unittest

class TestRect(unittest.TestCase):

    def assertEqualSides(self, left, right):
        if isinstance(right, tuple):
            right = shape2.Rectangular(*right)

        if left.width != right.width:
            raise AssertionError('widths are different')
        if left.height != right.height:
            raise AssertionError('heights are different')

    def setUp(self):
        self.a = shape2.Rectangular(4, 10)
        self.b = shape2.Rectangular(2, 20)
        self.c = shape2.Rectangular(1, 30)
        self.d = shape2.Rectangular(4, 10)

    def test_sanity(self):
        self.assertEqualSides(self.a, self.a)
        self.assertEqualSides(self.a, self.d)
        try:
            self.assertEqualSides(self.a, self.b)
        except AssertionError as e:
            self.assertEqual(e.args[0], 'widths are different')

        try:
            self.assertEqualSides(self.a, shape2.Rectangular(4, 20))
        except AssertionError as e:
            self.assertEqual(e.args[0], 'heights are different')

        self.assertEqualSides(self.a, (4, 10))

    def test_str(self):
        self.assertEqual(str(self.a), 'Rect[4, 10]')
        self.assertEqual(str(self.b), 'Rect[2, 20]')
        self.assertEqual(str(self.c), 'Rect[1, 30]')

    def test_mul(self):
        self.assertEqual(str(self.a * 3), 'Rect[4, 30]')
        self.assertEqual(str(self.b * 7), 'Rect[2, 140]')

    def test_rmul(self):
        self.assertEqual(str(3 * self.a), 'Rect[12, 10]')
        self.assertEqualSides(3 * self.a, (12, 10))

    def test_area(self):
        self.assertEqual(self.a.area(), 40)
        self.assertEqual(self.b.area(), 40)
        self.assertEqual(self.c.area(), 30)

    def test_equal(self):
        self.assertEqual(self.a, self.d)
        self.assertEqual(self.a, self.b)

    def test_add(self):
        self.assertEqualSides(self.a + shape2.Rectangular(4, 20), (4, 30))




if __name__ == '__main__':
    unittest.main()

Solution: Implement a Gene inheritance model combining DNA

import random

class Person(object):
    def __init__(self, DNA):
        self.DNA = DNA

    def gene(self):
        return list(self.DNA)

    def print_genes(self):
        print(list(self.DNA))

    def __add__(self, other):
        DNA_father = self.gene()
        DNA_mother = other.gene()
        if len(DNA_father) != len(DNA_mother):
            raise Exception("Incompatible couple")

        DNA_childPosible_sequence = DNA_father + DNA_mother
        DNA_child = ""
        for i in range(len(self.gene())):
            DNA_child += random.choice([DNA_father[i], DNA_mother[i]])

        return Person(DNA_child)


a = Person("ABCD")
b = Person("1234")
c = a + b
print(c.DNA)

Instance Attribute

The attributes of the instance object can be set via 'self' from within the class.

class Person():
    name = 'Joseph'

    def __init__(self, given_name):
        self.name = given_name

    def show_class(self):
        return Person.name

    def show_instance(self):
        return self.name

print(Person.name)        # Joseph

Person.name = 'Classy'
print(Person.name)     # Classy
# print(Person.show_class()) # TypeError: show_class() missing 1 required positional argument: 'self'

x = Person('Joe')
print(x.name)             # Joe
print(Person.name)        # Classy
print(x.show_class())     # Classy
print(x.show_instance())  # Joe

Person.name = 'General'
print(x.name)             # Joe
print(Person.name)        # General
print(x.show_class())     # General
print(x.show_instance())  # Joe

x.name = 'Zorg'           # changing the instance attribute
print(x.name)             # Zorg
print(Person.name)        # General
print(x.show_class())     # General
print(x.show_instance())  # Zorg

Use Python @propery to fix bad interface (the bad interface)

  • @property

When we created the class the first time we wanted to have a field representing the age of a person. (For simplicity of the example we onlys store the years.)


class Person():
    def __init__(self, age):
        self.age = age

p = Person(19)
print(p.age)       # 19

p.age = p.age + 1
print(p.age)       # 20

Only after releasing it to the public have we noticed the problem. Age changes.

We would have been better off storing birthdate and if necessary calculating the age.

How can we fix this?

Use Python @propery to fix bad interface (first attempt)

This might have been a good solution, but now we cannot use this as a "fix" as this would change the public interface from p.age to p.age()

from datetime import datetime
class Person():
    def __init__(self, years):
        self.set_birthyear(years)

    def get_birthyear(self):
        return datetime.now().year - self._birthyear

    def set_birthyear(self, years):
        self._birthyear = datetime.now().year - years

    def age(self, years=None):
        if (years):
            self.set_birthyear(years)
        else:
            return self.get_birthyear()



p = Person(19)
print(p.age())       # 19

p.age(p.age() + 1)
print(p.age())       # 20

Use Python @propery to fix bad API

  • property
property(fget=None, fset=None, fdel=None, doc=None)
from datetime import datetime
class Person():
    def __init__(self, years):
        self.age =  years

    def get_birthyear(self):
        return datetime.now().year - self.birthyear

    def set_birthyear(self, years):
        self.birthyear = datetime.now().year - years

    age = property(get_birthyear, set_birthyear)

p = Person(19)
print(p.age)       # 19

p.age = p.age + 1
print(p.age)       # 20

p.birthyear = 1992
print(p.age)       # 28
   # warning: this will be different if you run the example in a year different from 2020 :)

Use Python @propery decorator to fix bad API

  • @property
from datetime import datetime
class Person():
    def __init__(self, years):
        self.age =  years

    # creates "getter"
    @property
    def age(self):
        return datetime.now().year - self.birthyear

    # creates "setter"
    @age.setter
    def age(self, years):
        self.birthyear = datetime.now().year - years

p = Person(19)
print(p.age)       # 19

p.age = p.age + 1
print(p.age)       # 20


p.birthyear = 1992
print(p.age)       # 28
   # warning: this will be different if you run the example in a year different from 2020 :)

Use Python @propery for value validation

  • @property
from datetime import datetime
class Person():
    def __init__(self, years):
        self.age =  years

    @property
    def age(self):
        return datetime.now().year - self.birthyear

    @age.setter
    def age(self, years):
        if years < 0:
            raise ValueError("Age cannot be negative")
        self.birthyear = datetime.now().year - years

from person5 import Person

p = Person(19)
print(p.age)       # 19

p.age = p.age + 1
print(p.age)       # 20

p.birthyear = 1992
print(p.age)       # 28
   # warning: this will be different if you run the example in a year different from 2020 :)
from person5 import Person

print("Hello")

p = Person(-1)
Hello
Traceback (most recent call last):
  File "person5_bad_init.py", line 5, in <module>
    p = Person(-1)
  File "/home/gabor/work/slides/python-programming/examples/classes/person/person5.py", line 4, in __init__
    self.age =  years
  File "/home/gabor/work/slides/python-programming/examples/classes/person/person5.py", line 13, in age
    raise ValueError("Age cannot be negative")
ValueError: Age cannot be negative
Hello
10
Traceback (most recent call last):
  File "person5_bad_setter.py", line 7, in <module>
    p.age = -1
  File "/home/gabor/work/slides/python-programming/examples/classes/person/person5.py", line 13, in age
    raise ValueError("Age cannot be negative")
ValueError: Age cannot be negative

Other

Old and probably bad examples.

Inheritance - super

We can also call super() passing a different class name

class Point():
    def __init__(self, x, y):
        print('__init__ of point')
        self.x = x
        self.y = y

class Circle(Point):
    def __init__(self, x, y, r):
        print('__init__ of circle')
        super().__init__(x, y)
        self.r = r

class Ball(Circle):
    def __init__(self, x, y, r, z):
        print('__init__ of ball')
        #super(Circle, self).__init__(x, y) # r
        Point.__init__(self, x, y) # r
        self.z = z


b = Ball(2, 3, 10, 7)
print(b)

# __init__ of ball
# __init__ of point
# <__main__.Ball object at 0x10a26f190>

Inheritance - super - other class

We cannot pass any class name to super()

class Point:
    def __init__(self, x, y):
        print('__init__ of point')
        self.x = x
        self.y = y

class Circle(Point):
    def __init__(self, x, y, r):
        print('__init__ of circle')
        super(Circle, self).__init__(x, y)
        self.r = r

class Ball(Circle):
    def __init__(self, x, y, r, z):
        print('__init__ of ball')
        super(Zero, self).__init__(x, y)
        self.z = z

class Zero:
    def __init__(self, x, y):
        print('really?')
    pass


b = Ball(2, 3, 10, 7)
print(b)

# __init__ of circle
# Traceback (most recent call last):
#   File "bad_shapes.py", line 25, in <module>
#     b = Ball(2, 3, 10, 7)
#   File "bad_shapes.py", line 16, in __init__
#     super(Zero, self).__init__(x, y)
# TypeError: super(type, obj): obj must be an instance or subtype of type

Abstract Base Class without abc

Only works in Python 2?

import inspect

class Base():
    def __init__(self, *args, **kwargs):
        if self.__class__.__name__ == 'Base':
            raise Exception('You are required to subclass the {} class'
                .format('Base'))

        methods = set([ x[0] for x in
            inspect.getmembers(self.__class__, predicate=inspect.ismethod)])
        required = set(['foo', 'bar'])
        if not required.issubset( methods ):
            missing = required - methods
            raise Exception("Requried method '{}' is not implemented in '{}'"
                .format(', '.join(missing), self.__class__.__name__))


class Real(Base):
    def foo(self):
        print('foo in Real')
    def bar(self):
        print('bar in Real')
    def other(self):
        pass

class Fake(Base):
# user can hide the __init__ method of the parent class:
#    def __init__(self):
#        pass
    def foo(self):
        print('foo in Fake')

r = Real()
#b = Base()  # You are required to subclass the Base class
#f = Fake()  # Requried method 'bar' is not implemented in class 'Fake'

Abstract Base Class with abc Python 2 ?

  • abc
from abc import ABCMeta, abstractmethod

#class Base(metaclass = ABCMet):
class Base():
    __metaclass__ = ABCMeta

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass


class Real(Base):
    def foo(self):
        print('foo in Real')
    def bar(self):
        print('bar in Real')
    def other(self):
        pass

class Fake(Base):
    def foo(self):
        print('foo in Fake')

r = Real()
f = Fake()
   # TypeError: Can't instantiate abstract class Fake with abstract methods bar

Abstract Base Class with metaclass

  • metaclass
import inspect
class MyABC(type):
    def __init__(class_object, *args):
        #print('Meta.__init__')
        #print(class_object)
        #print(args)
            # ('Base',
            # (<type 'object'>,),
            # {
            #   '__required_methods__': ['foo', 'bar'],
            #   '__module__': '__main__',
            #   '__metaclass__': <class '__main__.MyABC'>
            # })
#        attr = dict(args)
        if not '__metaclass__' in args[2]:
            return

        if not '__required_methods__' in args[2]:
             raise Exception("No __required_methods__")
        name = args[0]
        required_methods = set(args[2]['__required_methods__'])
        def my_init(self, *args, **kwargs):
            if self.__class__.__name__ == name:
                raise Exception("You are required to subclass the '{}' class"
                    .format(name))

            #print("my_init")
            methods = set([ x[0] for x in
                inspect.getmembers(self.__class__, predicate=inspect.ismethod)])
            if not required_methods.issubset( methods ):
                missing = required_methods - methods
                raise Exception("Requried method '{}' is not implemented in '{}'"
                    .format(', '.join(missing), self.__class__.__name__))

        class_object.__init__ = my_init


class Base(object):
    __metaclass__ = MyABC
    __required_methods__ = ['foo', 'bar']

# b = Base() # Exception: You are required to subclass the 'Base' class

class Real(Base):
    def foo():
        pass
    def bar():
        pass

r = Real()

class Fake(Base):
    def foo():
        pass

#f = Fake() # Exception: Requried method 'bar' is not implemented in class 'Fake'

class UnFake(Fake):
    def bar():
        pass

uf = UnFake()

Abstract Base Class without ABC

class NotImplementedError(Exception):
    pass

class Base():
    def foo(self):
        raise NotImplementedError()

    def bar(self):
        raise NotImplementedError()

class Real(Base):
    def foo(self):
        print('foo in Real')
    def bar(self):
        print('bar in Real')
    def other(self):
        pass

class Fake(Base):
    def foo(self):
        print('foo in Fake')

r = Real()
r.foo()
r.bar()
f = Fake()
f.foo()
f.bar()
foo in Real
bar in Real
foo in Fake
Traceback (most recent call last):
  File "no_abc.py", line 28, in <module>
    f.bar()    # NotImplementedError
  File "no_abc.py", line 9, in bar
    raise NotImplementedError()
__main__.NotImplementedError