Classes#

Classic Classes#

Classes as we all known are a powerful Python feature. Digital Ocean has tutorials that explain how they work, in particular inheritance. Let's demonstrate with the following based on Digital Ocean's knowhow.

Starting with a Class called Fish that has the basic attributes of the family.:

class Fish:
    def __init__(self, first_name, last_name="Fish",
             skeleton="bone", eyelids=False):
        self.first_name = first_name
        self.last_name = last_name
        self.skeleton = skeleton
        self.eyelids = eyelids

    def swim(self):
        print("The fish is swimming.")

    def swim_backwards(self):
        print("The fish can swim backwards.")

The Fish class can be used directly, as in the class Pike.:

class Pike(Fish):
    pass

We can extend Fish class with a child class Clownfish.:

class Clownfish(Fish):

    def live_with_anemone(self):
        print("The clownfish is coexisting with sea anemone.")

One can override the many of the variables in the parent class, as with Shark

class Shark(Fish):
    def __init__(self, first_name, last_name="Shark", skeleton="cartilage", eyelids=True):
        self.first_name = first_name
        self.last_name = last_name
        self.skeleton = skeleton
        self.eyelids = eyelids
    def swim_backwards(self):
        print("The shark cannot swim backwards, but can sink backwards.")

If we need only to override a part of the parent class then the method using super can be used as shown with the Trout class.:

class Trout(Fish):
    def __init__(self, water = "freshwater"):
        self.water = water
        super().__init__(self)

These need to be tested and the output recorded. First test the Pike:

percy = Pike("Percy")
print(percy.first_name + " " + percy.last_name)
print(percy.skeleton)
print(percy.eyelids)
percy.swim()
percy.swim_backwards()

output:

Percy Fish
bone
False
The fish is swimming.
The fish can swim backwards.

Now the Clownfish:

casey = Clownfish("Casey")
print(casey.first_name + " " + casey.last_name)
casey.swim()
casey.live_with_anemone()

its output:

Casey Fish
The fish is swimming.
The clownfish is coexisting with sea anemone.

If we tried:

percy.live_with_anemone()
AttributeError: 'Pike' object has no attribute 'live_with_anemone'

Onto the Shark:

sammy = Shark("Sammy")
print(sammy.first_name + " " + sammy.last_name)
sammy.swim()
sammy.swim_backwards()
print(sammy.eyelids)
print(sammy.skeleton)

The answers should mostly differ from the Pike results:

Sammy Shark
The fish is swimming.
The shark cannot swim backwards, but can sink backwards.
True
cartilage

Lastly the Trout:

terry = Trout()

# Initialize first name
terry.first_name = "Terry"

# Use parent __init__() through super()
print(terry.first_name + " " + terry.last_name)
print(terry.eyelids)

# Use child __init__() override
print(terry.water)

# Use parent swim() method
terry.swim()

terry.swim_backwards()

giving the output:

Terry Fish
False
freshwater
The fish is swimming.
The fish can swim backwards.

All should work as expected, but it should be pointed out that when variables are initiated that they are first created as an attribute, which may or not have a default value, in which case they are pushed out of sequence to the end, then in the body of the initialisation each attribute is attributed to a clone of itself - the variable has been created in triplicate. Since Python 3.7 there has been the builtin ability to simplify this using Dataclasses.

Show/Hide Code test_class_inheritance_normal.py

class Fish:
    def __init__(self, first_name, last_name="Fish",
                 skeleton="bone", eyelids=False):
        self.first_name = first_name
        self.last_name = last_name
        self.skeleton = skeleton
        self.eyelids = eyelids

    def swim(self):
        print("The fish is swimming.")

    def swim_backwards(self):
        print("The fish can swim backwards.")

class Pike(Fish):
    pass

class Clownfish(Fish):
    def live_with_anemone(self):
        print("The clownfish is coexisting with sea anemone.")

class Shark(Fish):
    def __init__(self, first_name, last_name="Shark",
                 skeleton="cartilage", eyelids=True):
        self.first_name = first_name
        self.last_name = last_name
        self.skeleton = skeleton
        self.eyelids = eyelids

    def swim_backwards(self):
        print("The shark cannot swim backwards, but can sink backwards.")

class Trout(Fish):
    def __init__(self, water = "freshwater"):
        self.water = water
        super().__init__(self)

percy = Pike("Percy")
print(percy.first_name + " " + percy.last_name)
print(percy.skeleton)
print(percy.eyelids)
percy.swim()
percy.swim_backwards()

print()
casey = Clownfish("Casey")
print(casey.first_name + " " + casey.last_name)
casey.swim()
casey.live_with_anemone()

print()
# percy.live_with_anemone()

print()
sammy = Shark("Sammy")
print(sammy.first_name + " " + sammy.last_name)
sammy.swim()
sammy.swim_backwards()
print(sammy.eyelids)
print(sammy.skeleton)

print()
terry = Trout()

# Initialize first name
terry.first_name = "Terry"

# Use parent __init__() through super()
print(terry.first_name + " " + terry.last_name)
print(terry.eyelids)

# Use child __init__() override
print(terry.water)

# Use parent swim() method
terry.swim()

'''
Percy Fish
bone
False
The fish is swimming.
The fish can swim backwards.

Casey Fish
The fish is swimming.
The clownfish is coexisting with sea anemone.

AttributeError: 'Pike' object has no attribute 'live_with_anemone'

Sammy Shark
The fish is swimming.
The shark cannot swim backwards, but can sink backwards.
True
cartilage

Terry Fish
False
freshwater
The fish is swimming.
'''

Dataclass#

Rebuild the classic classes as dataclasses. In particular see whether inheritance is affected, if so how can it be improved. As above begin with Fish class. This is rewritten, not forgetting the dataclass decorator:

from dataclasses import dataclass

@dataclass
class Fish:
    first_name:str
    last_name:str = "Fish"
    skeleton:str = "bone"
    eyelids:bool = False

    def swim(self):
        print("The fish is swimming.")

    def swim_backwards(self):
        print("The fish can swim backwards.")

Initialisation for Fish falls away, but the two methods remain unaltered, with their reference to self (as before). The variables all come complete with their preferred type. The Pike class can be imported unchanged:

class Pike(Fish):
    pass

let's see how it reacts:

percy = Pike("Percy")
print(percy.first_name + " " + percy.last_name)
print(percy.skeleton)
print(percy.eyelids)
percy.swim()
percy.swim_backwards()

output:

Percy Fish
bone
False
The fish is swimming.
The fish can swim backwards.

exactly as before. Add the Clownfish with its tests, also unchanged:

class Clownfish(Fish):

    def live_with_anemone(self):
        print("The clownfish is coexisting with sea anemone.")

Clownfish tests:

casey = Clownfish("Casey")
print(casey.first_name + " " + casey.last_name)
casey.swim()
casey.live_with_anemone()

once again correct:

Casey Fish
The fish is swimming.
The clownfish is coexisting with sea anemone.

Test anemones with Pike:

percy.live_with_anemone()

output as before:

AttributeError: 'Pike' object has no attribute 'live_with_anemone'

Onto the shark which needs a decorator but can be simplified, much as before for fish:

@dataclass
class Shark(Fish):
    first_name:str
    last_name:str = "Shark"
    skeleton:str = "cartilage"
    eyelids:bool = True

def swim_backwards(self):
    print("The shark cannot swim backwards, but can sink backwards.")

Many of the variables change, the swim method remains unaltered, (it is automatically included when we write Shark(Fish)), the other method was modifified in the new Shark class:

sammy = Shark("Sammy")
print(sammy.first_name + " " + sammy.last_name)
sammy.swim()
sammy.swim_backwards()
print(sammy.eyelids)
print(sammy.skeleton)

Lastly we come to the Trout class, which with its addition of water and its super construct will need a dataclass decorator:

@dataclass
class Trout(Fish):
    water:str = "freshwater"

Run our tests as before:

terry = Trout()

caused an error:

TypeError: Trout.__init__() missing 1 required positional argument: 'first_name'

This means we cannot use terry.first_name = "Terry", try terry = Trout("Terry")

# Initialize first name
terry = Trout("Terry")

# Use parent values for last name and eyelids
print(terry.first_name + " " + terry.last_name)
print(terry.eyelids)

# Use new variable and its value
print(terry.water)

# Use parent swim() method
terry.swim()

# Use parent swim_backwards() method
terry.swim_backwards()

The outcome is:

Terry Fish
False
freshwater
The fish is swimming.
The fish can swim backwards.

Apart from the slight hiccup when starting up the Trout class without a firstname all went well. It was lucky that our classes were rather simple.

Show/Hide Code test_class_inheritance_dc.py

from dataclasses import dataclass, field

@dataclass
class Fish:
    first_name:str
    last_name:str = "Fish"
    skeleton:str = "bone"
    eyelids:bool = False

    def swim(self):
        print("The fish is swimming.")

    def swim_backwards(self):
        print("The fish can swim backwards.")

class Pike(Fish):
    pass

class Clownfish(Fish):

    def live_with_anemone(self):
        print("The clownfish is coexisting with sea anemone.")

@dataclass
class Shark(Fish):
    first_name:str
    last_name:str = "Shark"
    skeleton:str = "cartilage"
    eyelids:bool = True

    def swim_backwards(self):
        print("The shark cannot swim backwards, but can sink backwards.")

@dataclass
class Trout(Fish):
    water:str = "freshwater"

percy = Pike("Percy")
print(percy.first_name + " " + percy.last_name)
print(percy.skeleton)
print(percy.eyelids)
percy.swim()
percy.swim_backwards()
print()

casey = Clownfish("Casey")
print(casey.first_name + " " + casey.last_name)
casey.swim()
casey.live_with_anemone()

print()

# percy.live_with_anemone()
print()

sammy = Shark("Sammy")
print(sammy.first_name + " " + sammy.last_name)
sammy.swim()
sammy.swim_backwards()
print(sammy.eyelids)
print(sammy.skeleton)

print()

#terry = Trout()
# this raises an error

# Initialize first name
terry = Trout("Terry")

# Use parent values for last name and eyelids
print(terry.first_name + " " + terry.last_name)
print(terry.eyelids)

# Use new variable and its value
print(terry.water)

# Use parent swim() method
terry.swim()

# Use parent swim_backwards() method
terry.swim_backwards()

Attrs#

The existing scripts used for the PIL dimensions had used the older version attr, this is accessible with the newer version attrs, so until they are officially retired they can stay in use. The newer version is sleeker, so will be used for this exercise. Just as with the dataclass we only need to change those classes which contain initialisation.

Apart from the imports and the attrs decorator define it is the same as used for the dataclass, so type hints included. Copy all the dataclass classes into a new file, change the imports and decorator, so when starting the new Fish class will look like:

from attrs import define

@attrs
class Fish:
    first_name:str
    last_name:str = "Fish"
    skeleton:str = "bone"
    eyelids:bool = False

    def swim(self):
        print("The fish is swimming.")

    def swim_backwards(self):
        print("The fish can swim backwards.")

.....

and copy all the other classes, remembering the decorator changes for shark and trout. Include all the queries as well as the original Trout startup - just as with dataclass we had the slight hiccup. Place the Pike query with anemone at the end, to check that it can't be done.

Both methods (dataclass and attrs) work well, there are some advantages to both, but provided there is no restriction on using and installing third party Python programs there seems to be an advantage with attrs.

Show/Hide Code test_class_inheritance_attrs.py

from attrs import define, fields

@define
class Fish:
    first_name:str
    last_name:str = "Fish"
    skeleton:str = "bone"
    eyelids:bool = False

    def swim(self):
        print("The fish is swimming.")

    def swim_backwards(self):
        print("The fish can swim backwards.")

class Pike(Fish):
    pass

class Clownfish(Fish):

    def live_with_anemone(self):
        print("The clownfish is coexisting with sea anemone.")

@define
class Shark(Fish):
    first_name:str
    last_name:str = "Shark"
    skeleton:str = "cartilage"
    eyelids:bool = True

    def swim_backwards(self):
        print("The shark cannot swim backwards, but can sink backwards.")

@define
class Trout(Fish):
    water:str = "freshwater"

percy = Pike("Percy")
print(percy.first_name + " " + percy.last_name)
print(percy.skeleton)
print(percy.eyelids)
percy.swim()
percy.swim_backwards()

casey = Clownfish("Casey")
print(casey.first_name + " " + casey.last_name)
casey.swim()
casey.live_with_anemone()

print()

sammy = Shark("Sammy")
print(sammy.first_name + " " + sammy.last_name)
sammy.swim()
sammy.swim_backwards()
print(sammy.eyelids)
print(sammy.skeleton)

print()

#terry = Trout()
# caused an error, so terry.first_name = "Terry" cannot be used

# Initialize Trout supplying first_name, which is positional
terry = Trout("Terry")

# Use parent values for last name and eyelids
print(terry.first_name + " " + terry.last_name)
print(terry.eyelids)

# Use new variable and its value
print(terry.water)

# Use parent swim() method
terry.swim()

# Use parent swim_backwards() method
terry.swim_backwards()

print()

percy.live_with_anemone()
# AttributeError: 'Pike' object has no attribute 'live_with_anemone'

Applying dataclass and attrs#

Dataclasses and attrs work well where there is a lot of common data being changed and accessed. As seen above all valueless attributes (positional) may need to be given values immediately when the class is first called, but this is similar to the behaviour of normal classes. Build a dataclass (attrs) up from scratch rather than converting existing classes.

When converting classes, simple classes are better suited for dataclasses or attrs than those requiring unusual logic tucked away in a super class, normal inheritance rules can work, but as with everything test thoroughly. Classes with few if any values requiring to be initialised are not suited for dataclasses or attrs and the reasons to use them diminish if special constructions are required - whenever the class's methods are called the initial call is replaced by __post_init__ or the __attrs_post_init__ construct.