Classes

We are going to use the following code (a very much overused example, but it is overused for a reason) to help us define some terminology.

import math

class Pet:

    domesticated = True

    def __init__(self, name, age, gender, height, weight, is_fixed=False):
        self.name = name
        self.age = age
        self.gender = gender
        self.height = height
        self.weight = weight
        self.is_fixed = is_fixed

    def sound(self):
        return(f'Hello there, my name is {self.name}, and I\'m a pet.')

    @staticmethod
    def calculate_bmi(height, weight):
        return(weight / math.pow(height, 2) * 703)

    def is_overweight(self):
        return(self.calculate_bmi(self.height, self.weight) > 24)

    @classmethod
    def from_dict(cls, d):

        print("In from_dict, and cls is:", cls)

        is_fixed = False
        if d.get("name"):
            name = d.get("name")
        if d.get("age"):
            age = d.get("age")
        if d.get("gender"):
            gender = d.get("gender")
        if d.get("height"):
            height = d.get("height")
        if d.get("weight"):
            weight = d.get("weight")
        if d.get("is_fixed"):
            is_fixed = d.get("is_fixed")

        return cls(name, age, gender, height, weight, is_fixed)


class Dog(Pet):

    def sound(self):
        return(f'Ruff ruff!')

Pet is a class. You can create an instance of a Pet class, or a Pet object, like this.

my_pet = Pet("Ziva", .25, "Female", .5, 10, True)

# or
my_pet = Pet(name="Ziva", age=.25, gender="Female", height=.5, weight=10, is_fixed=True)

age, name, gender, height, weight, is_fixed are instance attributes. They can be accessed via an instance of the Pet class using the "." syntax.

print(my_pet.name)
Ziva
print(my_pet.age)
0.25

domesticated is a class attribute. It can be accessed via an instance of the class or from the class itself.

print(my_pet.domesticated) # via instance of the Pet class
True
print(Pet.domesticated) # via the class itself
True

sound and is_overweight are methods of the Pet class. You can call the methods using the "." syntax. The first argument to the methods are self. self is simply my_pet in this case. These methods accept self so that it can modify the instance’s state or instance attributes. When a method is called using the "." syntax, self is automatically passed as the first argument.

print(my_pet.sound()) # What a weird sound for a Pet to make
Hello there, my name is Ziva, and I'm a pet.
print(my_pet.is_overweight()) # oh no, we need more walks
True

calculate_bmi is a static method of the Pet class. Static methods are methods that don’t take either self or the class as arguments. A static method does not modify individual object’s states (like my_pet) or the class’s state (like Pet.domesticated). In addition, when a static method is called using the "." syntax, self is not passed as the first argument. Generally, a static method may be appropriate if the method is loosely coupled to the object.

print(Pet.calculate_bmi(50, 120))
33.744
print(my_pet.calculate_bmi(50, 120))
33.774
# this will NOT work, calculate_bmi is not passed `self`
print(my_pet.calculate_bmi())
Error in py_call_impl(callable, dots$args, dots$keywords): TypeError: calculate_bmi() missing 2 required positional arguments: 'height' and 'weight'

Detailed traceback:
File "<string>", line 1, in <module>

from_dict is a class method of the Pet class. Similarly to how regular methods accept self as the first argument, and are automatically passed self as the first argument when called, class method's accept cls as the first argument, and are automatically passed cls as the first argument when called. cls is simply the class, which is Pet in this case.

d = {"name": "Ziva", "age": .25, "gender": "Female", "height": .5, "weight": 10, "is_fixed": True}

# Pet and cls are the same:
print(Pet)
<class '__main__.Pet'>
Pet.from_dict(d)

# Our from_dict class method is used to create a pet from a dict rather than the normal method:
In from_dict, and cls is: <class '__main__.Pet'>
<__main__.Pet object at 0x7ff1a2eb3520>
my_pet = Pet("Ziva", .25, "Female", .5, 10, True)

# is the same as:
my_pet = Pet.from_dict(d)
In from_dict, and cls is: <class '__main__.Pet'>

Dog is a class. Pet is a parent class because Dog inherits from it. Dog is a child class because it inherits from Pet. You can create an instance of a Dog class, like this.

my_dog = Dog("Ziva", .25, "Female", .5, 10, True)

# this is the sound of a pet
print(my_pet.sound())

# but a dog, a dog sounds like this:
Hello there, my name is Ziva, and I'm a pet.
print(my_dog.sound())

# We said our dog inherits from the pet, what does this mean?
# It means we can do this with our Dog, even though we did
# not explictly define this function or the class attribute for
# Dog. Dog inherits this from its parent, Pet. Very cool!
Ruff ruff!
print(my_dog.is_overweight())
True
print(my_dog.domesticated)
True

What’s up with this funky __init__ function? This is called a dunder method. Dunder methods are a special set of predefined methods that you can use to make your classes even better. They all start and end with double underscores. Note that although they look odd, they are just functions and can be called just like any other function. We will explore dunder methods as a part of this project! If you are antsy to read more before doing, here is an article to read.

__init__ is the special constructor dunder method. Just like regular methods, the first argument is self, and each following argument are the arguments you would actually feed the class in order to create the object. For example:

my_dog = Dog("Ziva", .25, "Female", .5, 10, True)

my_dog is a Dog and a Dog inherits the __init__ method of the Pet class.

def __init__(self, name, age, gender, height, weight, is_fixed=False):
    self.name = name
    self.age = age
    self.gender = gender
    self.height = height
    self.weight = weight
    self.is_fixed = is_fixed

As you can see, this method is very boring. It takes the first argument, self, and sets its instance attributes: name, age, gender, height, weight, and is_fixed to the provided values.

my_dog = Dog("Ziva", .25, "Female", .5, 10, True)
# so this is what the inside of the `__init__` method looks like when
# we make `my_dog`
def __init__(self, name, age, gender, height, weight, is_fixed=False):
    # self.name is None, name is "Ziva"
    self.name = name

    # self.age is None, age is .25
    self.age = age

    # self.gender is None, gender is "Female"
    self.gender = gender

    # self.height is None, height is .5
    self.height = height

    # self.weight is None, weight is 10
    self.weight = weight

    # self.is_fixed is None, and is_fixed is True
    self.is_fixed = is_fixed

You could verify that all the instance attributes: self.name, …​, self.is_fixed, have properly set values:

print(my_dog.age)
0.25
print(my_dog.name)

# if we didn't have the __init__ method, the result of the my_dog.age, and my_dog.name would be None
Ziva

Believe it or not, classes can inherit from mulitple parent classes, who can also be children of multiple parent classes. It doesn’t take a lot to end up with an extremely confusing and hard to navigate code base. Composition is another programming principle that has better flexibility and often ends up producing easier to maintain code.

Resources

A great introduction to classes in Python.

A good resource for the basics.

A nice article explaining classes and object oriented programming.

A great explanation of the differences between the types of methods.