Coursera

Classes and subclasses

In this notebook, I will show you the basics of classes and subclasses in Python. As you’ve seen in the lectures from this week, Trax uses layer classes as building blocks for deep learning models, so it is important to understand how classes and subclasses behave in order to be able to build custom layers when needed.

By completing this notebook, you will:

Part 1: Parameters, methods and instances

First, let’s define a class My_Class.

class My_Class: #Definition of My_class
    x = None    

My_Class has one parameter x without any value. You can think of parameters as the variables that every object assigned to a class will have. So, at this point, any object of class My_Class would have a variable x equal to None. To check this, I’ll create two instances of that class and get the value of x for both of them.

instance_a= My_Class() #To create an instance from class "My_Class" you have to call "My_Class"
instance_b= My_Class()
print('Parameter x of instance_a: ' + str(instance_a.x)) #To get a parameter 'x' from an instance 'a', write 'a.x'
print('Parameter x of instance_b: ' + str(instance_b.x))
Parameter x of instance_a: None
Parameter x of instance_b: None

For an existing instance you can assign new values for any of its parameters. In the next cell, assign a value of 5 to the parameter x of instance_a.

instance_a.x = 5
print('Parameter x of instance_a: ' + str(instance_a.x))
Parameter x of instance_a: 5

1.1 The __init__ method

When you want to assign values to the parameters of your class when an instance is created, it is necessary to define a special method: __init__. The __init__ method is called when you create an instance of a class. It can have multiple arguments to initialize the paramenters of your instance. In the next cell I will define My_Class with an __init__ method that takes the instance (self) and an argument y as inputs.

class My_Class: 
    def __init__(self, y): # The __init__ method takes as input the instance to be initialized and a variable y
        self.x = y         # Sets parameter x to be equal to y

In this case, the parameter x of an instance from My_Class would take the value of an argument y. The argument self is used to pass information from the instance being created to the method __init__. In the next cell, create an instance instance_c, with x equal to 10.

instance_c = My_Class(10)
print('Parameter x of instance_c: ' + str(instance_c.x))
Parameter x of instance_c: 10

Note that in this case, you had to pass the argument y from the __init__ method to create an instance of My_Class.

1.2 The __call__ method

Another important method is the __call__ method. It is performed whenever you call an initialized instance of a class. It can have multiple arguments and you can define it to do whatever you want like

In the next cell, I’ll define My_Class with the same __init__ method as before and with a __call__ method that adds z to parameter x and prints the result.

class My_Class: 
    def __init__(self, y): # The __init__ method takes as input the instance to be initialized and a variable y
        self.x = y         # Sets parameter x to be equal to y
    def __call__(self, z): # __call__ method with self and z as arguments
        self.x += z        # Adds z to parameter x when called 
        print(self.x)

Let’s create instance_d with x equal to 5.

instance_d = My_Class(5)

And now, see what happens when instance_d is called with argument 10.

instance_d(10)
15

Now, you are ready to complete the following cell so any instance from My_Class:

class My_Class: 
    def __init__(self, y, z): #Initialization of x_1 and x_2 with arguments y and z
        self.x_1 = y
        self.x_2 = z
        
    def __call__(self):       #When called, adds the values of parameters x_1 and x_2, prints and returns the result 
        result = self.x_1 + self.x_2 
        print("Addition of {} and {} is {}".format(self.x_1,self.x_2,result))
        
        return result

Run the next cell to check your implementation. If everything is correct, you shouldn’t get any errors.

instance_e = My_Class(10,15)
def test_class_definition():
    
    assert instance_e.x_1 == 10, "Check the value assigned to x_1"
    assert instance_e.x_2 == 15, "Check the value assigned to x_2"
    assert instance_e() == 25, "Check the __call__ method"
    
    print("\033[92mAll tests passed!")
    
test_class_definition()
Addition of 10 and 15 is 25
All tests passed!

1.3 Custom methods

In addition to the __init__ and __call__ methods, your classes can have custom-built methods to do whatever you want when called. To define a custom method, you have to indicate its input arguments, the instructions that you want it to perform and the values to return (if any). In the next cell, My_Class is defined with my_method that multiplies the values of x_1 and x_2, sums that product with an input w, and returns the result.

class My_Class: 
    def __init__(self, y, z): #Initialization of x_1 and x_2 with arguments y and z
        self.x_1 = y
        self.x_2 = z
    def __call__(self):       #Performs an operation with x_1 and x_2, and returns the result
        a = self.x_1 - 2*self.x_2 
        return a
    def my_method(self, w):   #Multiplies x_1 and x_2, adds argument w and returns the result
        result = self.x_1*self.x_2 + w
        return result

Create an instance instance_f of My_Class with any integer values that you want for x_1 and x_2. For that instance, see the result of calling My_method, with an argument w equal to 16.

instance_f = My_Class(1,10)
print("Output of my_method:",instance_f.my_method(16))
Output of my_method: 26

As you can corroborate in the previous cell, to call a custom method m, with arguments args, for an instance i you must write i.m(args). With that in mind, methods can call others within a class. In the following cell, try to define new_method which calls my_method with v as input argument. Try to do this on your own in the cell given below.

class My_Class: 
    def __init__(self, y, z):
        ### START CODE HERE           #Initialization of x_1 and x_2 with arguments y and z
        self.x_1 = y
        self.x_2 = z
    def __call__(self):               #Performs an operation with x_1 and x_2, and returns the result
        a = self.x_1 - 2*self.x_2 
        return a
    def my_method(self, w):           #Multiplies x_1 and x_2, adds argument w and returns the result
        b = self.x_1 * self.x_2 + w
        return b
    def new_method(self, v):          #Calls My_method with argument v
        result = self.my_method(v)
        ### END CODE HERE ### 
        return result

SPOILER ALERT Solution:

# hidden-cell
class My_Class: 
    def __init__(self, y, z):      #Initialization of x_1 and x_2 with arguments y and z
        self.x_1 = y
        self.x_2 = z
    def __call__(self):            #Performs an operation with x_1 and x_2, and returns the result
        a = self.x_1 - 2*self.x_2 
        return a
    def my_method(self, w):        #Multiplies x_1 and x_2, adds argument w and returns the result
        b = self.x_1*self.x_2 + w
        return b
    def new_method(self, v):       #Calls My_method with argument v
        result = self.my_method(v)
        return result
instance_g = My_Class(1,10)
print("Output of my_method:",instance_g.my_method(16))
print("Output of new_method:",instance_g.new_method(16))
Output of my_method: 26
Output of new_method: 26

Part 2: Subclasses and Inheritance

Trax uses classes and subclasses to define layers. The base class in Trax is layer, which means that every layer from a deep learning model is defined as a subclass of the layer class. In this part of the notebook, you are going to see how subclasses work. To define a subclass sub from class super, you have to write class sub(super): and define any method and parameter that you want for your subclass. In the next cell, I define sub_c as a subclass of My_Class with only one method (additional_method).

class sub_c(My_Class):           #Subclass sub_c from My_class
    def additional_method(self): #Prints the value of parameter x_1
        print(self.x_1)

2.1 Inheritance

When you define a subclass sub, every method and parameter is inherited from super class, including the __init__ and __call__ methods. This means that any instance from sub can use the methods defined in super. Run the following cell and see for yourself.

instance_sub_a = sub_c(1,10)
print('Parameter x_1 of instance_sub_a: ' + str(instance_sub_a.x_1))
print('Parameter x_2 of instance_sub_a: ' + str(instance_sub_a.x_2))
print("Output of my_method of instance_sub_a:",instance_sub_a.my_method(16))

Parameter x_1 of instance_sub_a: 1
Parameter x_2 of instance_sub_a: 10
Output of my_method of instance_sub_a: 26

As you can see, sub_c does not have an initialization method __init__, it is inherited from My_class. However, you can overwrite any method you want by defining it again in the subclass. For instance, in the next cell define a class sub_c with a redefined my_Method that multiplies x_1 and x_2 but does not add any additional argument.

class sub_c(My_Class):           #Subclass sub_c from My_class
    def my_method(self):         #Multiplies x_1 and x_2 and returns the result
        b = self.x_1*self.x_2 
        
        return b

To check your implementation run the following cell.

test = sub_c(3,10)
assert test.my_method() == 30, "The method my_method should return the product between x_1 and x_2"

print("Output of overridden my_method of test:",test.my_method()) #notice we didn't pass any parameter to call my_method
print("Output of overridden my_method of test:",test.my_method(16)) #try to see what happens if you call it with 1 argument
Output of overridden my_method of test: 30



---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-19-79bf2dc424cf> in <module>
      3 
      4 print("Output of overridden my_method of test:",test.my_method()) #notice we didn't pass any parameter to call my_method
----> 5 print("Output of overridden my_method of test:",test.my_method(16)) #try to see what happens if you call it with 1 argument


TypeError: my_method() takes 1 positional argument but 2 were given

In the next cell, two instances are created, one of My_Class and another one of sub_c. The instances are initialized with equal x_1 and x_2 parameters.

y,z= 1,10
instance_sub_a = sub_c(y,z)
instance_a = My_Class(y,z)
print('My_method for an instance of sub_c returns: ' + str(instance_sub_a.my_method()))
print('My_method for an instance of My_Class returns: ' + str(instance_a.my_method(10)))
My_method for an instance of sub_c returns: 10
My_method for an instance of My_Class returns: 20

As you can see, even though sub_c is a subclass from My_Class and both instances are initialized with the same values, My_method returns different results for each instance because you overwrote My_method for sub_c.

Congratulations! You just reviewed the basics behind classes and subclasses. Now you can define your own classes and subclasses, work with instances and overwrite inherited methods. The concepts within this notebook are more than enough to understand how layers in Trax work.