Help support the author by donating or purchasing a copy of the book (not available yet)



Chapter 19 - OOP: Method Types & Access Modifiers

19.1 - What are instance methods?

By the end of this chapter you should be ready for your first project!

We have already met instance methods. They are the type of methods we have been working with in regards to object oriented programming until this point.

Instance methods are the most common type of methods in classes. They are called instance methods as they act on specific instances of a class. They can access the data attributes that are unique to that class.

For example, if we have a Person class, then each instance of a person may have a unique name, age etc. Instance methods have self as the first parameter and this allows us to go through self to access data attributes unique to that instance and also other methods that may reside within our class.

I'm not going to provide an example for instance methods as we've seen them time and time again!

19.2 - What are class methods?

Class methods are the second type of OOP method we can have. A class method knows about it's class. They can't access data that is specific to an instance, but they can call other methods.

Class methods have limited access to methods within the class and can modify class specific details such as class variables which we will see in a moment. This allows us to modify the class's state which will be applied across all instances of the class. They are one of the more confusing method types. Class methods are also permanently bound to its class.

We invoke class methods through an instance or through a class. Unlike instance methods whose first parameter is self, with class methods, it's first parameter is not an object, but the class itself. This first parameter is called cls. We use a decorator to mark a method as a class method. The decorator is @classmethod.

Let's look at an example by going back to our Time class. We want a function that can convert seconds to 24 hour time with hours, minutes, and seconds. This method should be bound to the class of Time rather than a specific instance.

class Time:
    def __init__(self, hours=0, minutes=0, seconds=0):
        self.hours = hours
        self.minutes = minutes
        self.seconds = seconds
        
    def __str__(self):
        return "The time is {:02}:{:02}:{:02}".format(self.hours,
                                                     self.minutes,
                                                     self.seconds)
    
   # OTHER METHODS CAN GO HERE...
    
    @classmethod
    def seconds_to_time(cls, s):
        minutes, seconds = divmod(s, 60)
        hours, minutes = divmod(minutes, 60)
        extra, hours = divmod(hours, 24)
        return cls(hours, minutes, seconds)

For this example, I've cut out other instance methods that we don't care about for now. The seconds to time class method takes the class as the first parameter and some number of seconds, s, as its second parameter.

A side note on the divmod() function: The divmod() function takes two arguments, the first is the number of seconds we want to convert to 24 hour time, the second will be divided into it (60 in this case). This returns a tuple in which the first element is the number of times the second parameter divided into the first, and the second element of the tuple is the remainder after that division. For example, if we called divmod(180, 60), we'd get back a tuple of (3, 0) in which we use multiple assignment to assign the 3 to the minutes and 0 to the seconds which is what we expect as 180 seconds is 3 minutes.

Back to our class method. When we have calculated our hours, minutes and seconds, we return cls(hours, minutes, seconds). cls will be replaced with Time in this case. So really we're returning a new Time object.

To see this in action:

>>> from my_time import Time
>>> t = Time.seconds_to_time(11982)
>>> print(t)
'The time is 03:19:42'

I also mentioned class variables earlier. Much like before, class variables are bound to a class rather than a specific instance. We can use a class method to check the number of times an instance of the time class has been created for example.

With convention, class variables are usually all uppercase for the variable name. Let's modify our time class above to add a class method called COUNT which will count the number of times an instance of the class has been created.

class Time:
    
    COUNT = 0
    
    def __init__(self, hours=0, minutes=0, seconds=0):
        self.hours = hours
        self.minutes = minutes
        self.seconds = seconds
        Time.COUNT += 1
        
    def __str__(self):
        return "The time is {:02}:{:02}:{:02}".format(self.hours,
                                                     self.minutes,
                                                     self.seconds)
    
   # OTHER METHODS CAN GO HERE...
    
    @classmethod
    def seconds_to_time(cls, s):
        minutes, seconds = divmod(s, 60)
        hours, minutes = divmod(minutes, 60)
        extra, hours = divmod(hours, 24)
        return cls(hours, minutes, seconds)

Note how we have a new COUNT variable just before our constructor and inside our constructor, after initializing all the attributes, we increase the COUNT class variable by 1. Now we can see how many times instance of our class has been created.

>>> from my_time import Time
>>> t1 = Time(23, 11, 37)
>>> t2 = Time()
>>> Time.COUNT
2
>>> t3 = Time(12, 34, 54)
>>> Time.COUNT
3

It may be tricky trying to spot when to use these. As a general rule of thumb, if a method can be invoked (and makes sense to) in the absence of an instance, then it seems that, that method is a good candidate for a class method.

19.3 - What are static methods?

Finally, we have a third method type that is called a static method. These are not as confusing as class methods. You can think of static methods just like ordinary functions. Python will not automatically supply any extra arguments when a static method is invoked. Unlike self and cls with instance and class methods.

Static methods are methods that are related to the class in some way, but don't need to access any class specific data and don't need an instance to be invoked. You can simply call them as pleased.

In general, static methods don't know anything about class state. They are methods that act more as utilities.

We use the @staticmethod decorator to specify a static method. We will add a static method to our Time class that will validate the time for us, checking that the hours are within the range of 0 and 23 and our minutes and seconds are within the range of 0 and 59.

class Time:
    
    COUNT = 0
    
    def __init__(self, hours=0, minutes=0, seconds=0):
        self.hours = hours
        self.minutes = minutes
        self.seconds = seconds
        Time.COUNT += 1
        
    def __str__(self):
        return "The time is {:02}:{:02}:{:02}".format(self.hours,
                                                     self.minutes,
                                                     self.seconds)
    
   # OTHER METHODS CAN GO HERE...
    
    @classmethod
    def seconds_to_time(cls, s):
        minutes, seconds = divmod(s, 60)
        hours, minutes = divmod(minutes, 60)
        extra, hours = divmod(hours, 24)
        return cls(hours, minutes, seconds)
    
    
    @staticmethod
    def validate(hours, minutes, seconds):
        return 0 <= hours <= 23 and 0 <= minutes <= 59 and 0<= seconds <= 59

This static method is simply a utility that allows us to verify that times are correct. We may use this to stop a user from creating a time such as 35:88:14 as that wouldn't make any logical sense.

>>> from my_time import Time
>>> Time.validate(5, 23, 44)
True
>>> Time.validate(25, 34, 63)
False
>>> t = Time(22, 45, 23)
>>> t.validate(t.hours, t.minutes, t.seconds)
True

It may also be tricky to spot when to use static methods, but again, if you find that you have a function that may be useful but it won't be applicable to any other class yet at the same time it doesn't need to be bound to a instance and it doesn't need to access or modify any class data then it is a good candidate for a static method.

19.4 - Public, Private & Protected attributes

In object oriented programming, the idea of access modifiers is important. It helps us implement the idea of Encapsulation (information hiding). Access modifiers tell compilers which other classes should have access to data attributes and methods that are defined within classes. This stops outside classes from calling methods or accessing data from within another class, or making those data attributes and methods public, in which case all other classes can call and access them.

Access modifiers are used to make code more robust and secure by limiting access to variables that don't need to be accessed by every (or maybe they do in which case they're public).

Until now we have been using public attributes and methods. Classes can call the methods and access the variables in other classes.

In Python, there is no strict checking for access modifiers, in fact they don't exist but Python coders have adopted a convention to overcome this.

The following attributes are public:

def __init__(self, hours, minutes, seconds):
    self.hours = hours
    self.minutes = minutes
    self.seconds = seconds

By convention, these are accessible by anyone (by anyone I mean other classes).

To "make" these variables private we add a double underscore in-front of the variable name. For an attribute or method to be private, means that only the class they are contained in can call and access them. Nothing on the outside. In the Java language, if you had two classes and tried to call a private method from another class you would get an error.

def __init__(self, hours, minutes, seconds):
    self.__hours = hours
    self.__minutes = minutes
    self.__seconds = seconds

The third access modifier I'll talk about is protected. This is the same as private except all subclasses can also access the methods and member variables. I'll talk about subclasses in the next chapter so don't worry about that now, but by convention, we use a single underscore before the variable name.

def __init__(self, hours, minutes, seconds):
    self._hours = hours
    self._minutes = minutes
    self._seconds = seconds

19.5 - Exercises

Question 1

Write a Python program that contains a class called Person. An instance of a Person should have name and age attributes. You should be able to print an instance of the class which, for example, should output John is 28 years old.

Your class should have a method called fromBirthYear() which as parameters should take an age and a birth year. For example, fromBirthYear('John', 1990) should return an instance of Person whose age is 29 (at the time of writing this book, 2019).

Running your program should produce the following:

>>> from my_person import Person
>>> john = Person("John", 28)
>>> print(john)
"John is 28 years old"
>>> adam = Person.fromBirthYear("Adam", 1990)
>>> print(adam)
"Adam is 29 years old"

Think carefully about what type of method to use.

Question 2

Write a Student class that has initializes a student with 3 attributes: name, grades and student_number. Grades should be a dictionary of modules to grades. The student number of the first student instance should be 19343553. The student number of the second student instance should be 19343554 and so on. You should have three methods, add_module() which takes one parameter, a module name, and initialises the grade to 0. The second method should be, update_module() which takes two parameters, the module name and the grade for the module. The final method should allow the user to print a student.

Think carefully about how you will implement the increasing student number.

Running your program should give the following output:

>>> from my_student import Student
>>> john = Student("John")
>>> john.add_module("Python")
>>> john.update_module("Python", 88)
>>> print(john)
Name: John
Student Number: 19343553
Grades:
Python 88
>>> adam = Student("Adam")
>>> adam.add_module("Java")
>>> adam.update_module("Java", 60)
>>> print(adam)
Name: Adam
Student Number: 19343554
Grades:
Java: 60

Question 3

Modify the Student class to include a method that will validate grades. A grade should be between 0 and 100.

Think carefully about the type of method this should be.

>>>from my_student import Student
>>> john = Student("John")
>>> john.add_module("Python")
>>> john.isValidGrade(88)
True
>>> Student.isValidGrade(101)
False
>>> john.update_module("Python", 88)
>>> print(john)
Name: John
Student Number: 19343553
Grades:
Python 88

Project

You are tasked with creating a network simulation application in which two parties exist. We'll call these parties the Sender and Receiver. There should also exist a process that continuously checks if the Sender has any packets of data to send, if it does, then the process should deliver them to the Receiver. The Sender should periodically create data packets to be sent (These can be randomly generated strings). The Receiver should periodically check if it has received any packets of data, if it has, then it should print them, then delete them.

Hint: You should think carefully about what classes are needed and what methods they should contain. Something called a 'buffer' may be helpful here for your process that moves packets between Sender and Receiver. Read up about buffers and what they. Think carefully about what Python built-in types might be able to implement a buffer.



Help support the author by donating or purchasing a copy of the book (not available yet)



Previous Chapter - Next Chapter