Last week we looked at several example projects and the classes we might use to implement them.
I would like to keep track of all the items in the laboratory so I can easily find them the next time I need them. Both equipment and consumables would be tracked. We have multiple rooms, and items can be on shelves, in refrigerators, in freezers, etc. Items can also be in boxes containing other items in all these places.
The words in bold would all be good ideas to turn into classes. Now we know some of the classes we will need we can start to think about what each of these classes should do, what the methods will be. Let's consider the consumables class:
For consumables we will need to manage their use so there should be an initial quantity and a quantity remaining that is updated every time we use some. We want to make sure that temperature sensitive consumables are always stored at the correct temperature, and that flammables are stored in a flammables cabinet etc.
The consumable class will need a number of attributes:
The consumable class will need methods to:
The consumable class might interact with the shelf, refrigerator, freezer, and/or box classes.
Reading back through our description of consumables there is reference to a flammables cabinet that was not mentioned in our initial description of the problem. This is an iterative design process so we should go back and add a flammables cabinet class.
If we expand our list to all the classes we plan to use we get the following:
Although this is a long list careful examination reveals that there is a lot of repetition.
Items and equipment are identical and consumables is similar, adding several extra attributes and methods.
Rooms, shelves, refrigerators, freezers, boxes and flammables cabinet are all similar, only differing in the occasional attribute.
Our three main groups are:
So much duplication is problematic, it is diffcult to maintain and subject to greater risk of bugs.
There is a better way - we can create a generic class with the shared functionality and then inherit from it when we create the other classes.
For example an Item class would contain the basic attributes and methods. The Equipment class could then inherit from this class without modification. The Consumable class would also inherit from Item and only add the extra attributes and methods uniquely need by the Consumable class.
In [ ]:
class Item(object):
def __init__(self, name, description, location):
self.name = name
self.description = description
self.location = location
def update_location(self, new_location):
pass
class Equipment(Item):
pass
class Consumable(Item):
def __init__(self, name, description, location, initial_quantity, current_quantity, storage_temp, flammability):
self.name = name
self.description = description
self.location = location
self.initial_quantity = initial_quantity
self.current_quantity = current_quantity
self.flammability = flammability
def update_quantity_remaining(self, amount):
pass
There is one other situation we should consider. Occasionally we will want a class of a particular type to always implement a particular method even though we are unable to implement that method in our parent class. We need some way of raising an error when the parent class is inherited and the method is not implemented.
As a simple example consider a class representing length. We might create classes for meters, miles, feet, etc. Keeping the original units when performing operations (adding, subtracting, etc) would prevent rounding errors but each class would need custom logic.
Returning to our laboratory inventory system one way we can implement this is below:
In [2]:
class Item(object):
def safely_stored(self):
raise NotImplementedError('override in subclass')
class Consumable(Item):
def safely_stored(self):
return True
In [3]:
a = Item()
In [4]:
a.safely_stored()
In [5]:
b = Consumable()
In [6]:
b.safely_stored()
Out[6]:
A disadvantage with this approach is we only see the error message when we call the method. The error is in the way we implemented the class so it would be more intuitive to get an error earlier, when we first create the object.
This can be achieved using the abstract method decorator.
In [7]:
from abc import ABCMeta, abstractmethod
class Item(metaclass=ABCMeta):
@abstractmethod
def safely_stored(self):
pass
class Consumable(Item):
def safely_stored(self):
return True
In [8]:
a = Item()
In [9]:
b = Consumable()
b.safely_stored()
Out[9]:
Either of these approaches work well for adding new methods or completely changing the behaviour of a method. Often we only need to make a more subtle change. In this situation it can be useful to call a method from a parent class while only implementing our new functionality in the child class.
There are two approaches for this.
In [1]:
class A(object):
def a(self):
print('a in class A')
class B(A):
def a(self):
A.a(self)
print('b in class B')
a = A()
a.a()
b = B()
b.a()
In [2]:
class A(object):
def a(self):
print('a in class A')
class B(A):
def a(self):
super().a()
print('b in class B')
a = A()
a.a()
b = B()
b.a()
Using super() is usually the best approach, the reasons for this are covered in detail in this blog post
We are not limited to inheriting from a single class. It is possible to merge functionality from multiple different classes simply by inheriting from them.
When inheriting from multiple classes that contain a method or attribute with the same name there is a particular order in which the names are resolved.
In [18]:
class A(object):
def a(self):
print('A-a')
class A2(object):
def a(self):
print('A2-a')
class B(A, A2):
pass
a = A()
a.a()
a2 = A2()
a2.a()
b = B()
b.a()
In [21]:
class A(object):
def a(self):
print('A-a')
class A2(object):
def a(self):
print('A2-a')
class B(A):
pass
class C(B, A2):
pass
a = A()
a.a()
a2 = A2()
a2.a()
c = C()
c.a()
A simple rule-of-thumb is that search is depth first. The details are a little more complicated.
In [11]:
isinstance(a, Item)
Out[11]:
In [12]:
isinstance(b, Consumable)
Out[12]:
In [13]:
isinstance(b, Item)
Out[13]:
In [14]:
isinstance(a, Consumable)
Out[14]:
A popular alternative in python is duck typing, an approach named after the idea that,
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
What this means for programming is that instead of checking for a particular class, instead the methods and attributes that are actually needed are checked for.
Object oriented programming and particularly inheritance is commonly used for creating GUIs. There are a large number of different frameworks supporting building GUIs. The following are particularly relevant:
conda install pyqtgraph
In [ ]: