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
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 theobject
. - 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
- See cmath
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