Advanced Object-Oriented Programming in Python

by Pyrastra Team
Advanced Object-Oriented Programming in Python

In the previous section, we explained some basic knowledge of Python object-oriented programming. In this section, we continue to discuss content related to object-oriented programming.

Visibility and Property Decorators

In many object-oriented programming languages, object attributes are usually set as private or protected members. Simply put, these attributes are not allowed to be accessed directly. Object methods are usually public because public methods are the messages that objects can receive and the calling interfaces that objects expose to the outside world. This is the so-called access visibility. In Python, you can indicate the access visibility of attributes by adding prefix underscores to object attribute names. For example, you can use __name to represent a private attribute and _name to represent a protected attribute, as shown in the code below.

class Student:

    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    def study(self, course_name):
        print(f'{self.__name} is studying {course_name}.')


stu = Student('Wang Dachui', 20)
stu.study('Python Programming')
print(stu.__name)  # AttributeError: 'Student' object has no attribute '__name'

The last line of the code above will raise an AttributeError exception with the message: 'Student' object has no attribute '__name'. This shows that the attribute __name starting with __ is equivalent to being private and cannot be directly accessed outside the class, but it can be accessed through self.__name in the study method inside the class. It should be noted that most people who use Python don’t usually choose to make object attributes private or protected when defining classes. As a famous saying goes: “We are all consenting adults here”. Adults can be responsible for their own actions without needing Python itself to restrict access visibility. In fact, most programmers believe that openness is better than closure, and privatizing object attributes is not essential. Therefore, Python doesn’t make the strictest restrictions semantically. That is to say, if you want, you can still access the private attribute __name using stu._Student__name in the code above. Interested readers can try it themselves.

Dynamic Attributes

Python is a dynamic language. Wikipedia’s explanation of dynamic languages is: “Languages that can change their structure at runtime, for example, new functions, objects, or even code can be introduced, existing functions can be deleted, or other structural changes can occur”. Dynamic languages are very flexible. Currently popular Python and JavaScript are both dynamic languages. In addition, languages such as PHP and Ruby are also dynamic languages, while C, C++, and other languages are not dynamic languages.

In Python, we can dynamically add attributes to objects. This is a privilege of Python as a dynamically typed language, as shown in the code below. It should be reminded that object methods are essentially object attributes. If you send a message to an object that it cannot receive, the exception raised is still AttributeError.

class Student:

    def __init__(self, name, age):
        self.name = name
        self.age = age


stu = Student('Wang Dachui', 20)
stu.sex = 'Male'  # Dynamically add sex attribute to student object

If you don’t want to dynamically add attributes to objects when using them, you can use Python’s __slots__ magic. For the Student class, you can specify __slots__ = ('name', 'age') in the class, so that Student class objects can only have name and age attributes. If you try to dynamically add other attributes, an exception will be raised, as shown in the code below.

class Student:
    __slots__ = ('name', 'age')

    def __init__(self, name, age):
        self.name = name
        self.age = age


stu = Student('Wang Dachui', 20)
# AttributeError: 'Student' object has no attribute 'sex'
stu.sex = 'Male'

Static Methods and Class Methods

Previously, the methods we defined in classes were all object methods. In other words, these methods are all messages that objects can receive. In addition to object methods, classes can also have static methods and class methods. These two types of methods are messages sent to classes. There is no substantial difference between the two. In the object-oriented world, everything is an object. Each class we define is actually also an object, and static methods and class methods are messages sent to class objects. So what kind of messages are sent directly to class objects?

Let me give an example. Define a triangle class. Construct a triangle by passing in the lengths of three sides, and provide methods to calculate the perimeter and area. Calculating the perimeter and area are definitely methods of triangle objects, there’s no doubt about that. However, when creating a triangle object, the three side lengths passed in may not be able to construct a triangle. For this reason, we can first write a method to verify whether the given three side lengths can form a triangle. This method is obviously not an object method because the triangle object hasn’t been created yet when this method is called. We can design this type of method as a static method or class method. That is to say, this type of method is not a message sent to triangle objects, but a message sent to the triangle class, as shown in the code below.

class Triangle(object):
    """Triangle"""

    def __init__(self, a, b, c):
        """Initialization method"""
        self.a = a
        self.b = b
        self.c = c

    @staticmethod
    def is_valid(a, b, c):
        """Determine if three side lengths can form a triangle (static method)"""
        return a + b > c and b + c > a and a + c > b

    # @classmethod
    # def is_valid(cls, a, b, c):
    #     """Determine if three side lengths can form a triangle (class method)"""
    #     return a + b > c and b + c > a and a + c > b

    def perimeter(self):
        """Calculate perimeter"""
        return self.a + self.b + self.c

    def area(self):
        """Calculate area"""
        p = self.perimeter() / 2
        return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5

The code above uses the staticmethod decorator to declare that the is_valid method is a static method of the Triangle class. If you want to declare a class method, you can use the classmethod decorator (as shown in lines 15-18 of the code above). You can directly use the ClassName.method_name method to call static methods and class methods. The difference between the two is that the first parameter of a class method is the class object itself, while static methods don’t have this parameter. To summarize simply, object methods, class methods, and static methods can all be called through “ClassName.method_name”, the difference lies in whether the first parameter is a regular object, a class object, or there is no object receiving the message. Static methods can usually also be written directly as independent functions because they are not bound to specific objects.

Here’s a supplementary note: we can add a property decorator (a Python built-in type) to the methods for calculating triangle perimeter and area above. This way, the perimeter and area of the triangle class become two properties, no longer accessed by calling methods, but directly obtained by accessing properties with objects. The modified code is shown below.

class Triangle(object):
    """Triangle"""

    def __init__(self, a, b, c):
        """Initialization method"""
        self.a = a
        self.b = b
        self.c = c

    @staticmethod
    def is_valid(a, b, c):
        """Determine if three side lengths can form a triangle (static method)"""
        return a + b > c and b + c > a and a + c > b

    @property
    def perimeter(self):
        """Calculate perimeter"""
        return self.a + self.b + self.c

    @property
    def area(self):
        """Calculate area"""
        p = self.perimeter / 2
        return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5


t = Triangle(3, 4, 5)
print(f'Perimeter: {t.perimeter}')
print(f'Area: {t.area}')

Inheritance and Polymorphism

Object-oriented programming languages support creating new classes based on existing classes, thereby reducing duplicate code writing. The class that provides inheritance information is called the parent class (superclass, base class), and the class that receives inheritance information is called the child class (derived class, subclass). For example, if we define a student class and a teacher class, we’ll find they have a lot of duplicate code, and this duplicate code is all common attributes and behaviors of teachers and students as people. So in this situation, we should first define a person class, and then through inheritance, derive teacher and student classes from the person class, as shown in the code below.

class Person:
    """Person"""

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def eat(self):
        print(f'{self.name} is eating.')

    def sleep(self):
        print(f'{self.name} is sleeping.')


class Student(Person):
    """Student"""

    def __init__(self, name, age):
        super().__init__(name, age)

    def study(self, course_name):
        print(f'{self.name} is studying {course_name}.')


class Teacher(Person):
    """Teacher"""

    def __init__(self, name, age, title):
        super().__init__(name, age)
        self.title = title

    def teach(self, course_name):
        print(f'{self.name} {self.title} is teaching {course_name}.')


stu1 = Student('Bai Yuanfang', 21)
stu2 = Student('Di Renjie', 22)
tea1 = Teacher('Wu Zetian', 35, 'Associate Professor')
stu1.eat()
stu2.sleep()
tea1.eat()
stu1.study('Python Programming')
tea1.teach('Python Programming')
stu2.study('Introduction to Data Science')

The syntax for inheritance is to specify the current class’s parent class in parentheses after the class name when defining a class. If you don’t specify a parent class when defining a class, the default parent class is the object class. The object class is the top-level class in Python, which means all classes are its subclasses, either directly or indirectly inheriting from it. Python allows multiple inheritance, meaning a class can have one or more parent classes. We’ll have a more detailed discussion about multiple inheritance later. In the child class’s initialization method, we can call the parent class’s initialization method through super().__init__(). The super function is a Python built-in function specifically designed to get the parent object of the current object. From the code above, we can see that in addition to obtaining attributes and methods provided by the parent class through inheritance, child classes can also define their own unique attributes and methods. So child classes have more capabilities than parent classes. In actual development, we often use child class objects to replace parent class objects. This is a common behavior in object-oriented programming, also called the “Liskov Substitution Principle”.

After a child class inherits a parent class’s method, it can also override the method (re-implement the method). Different child classes can give different implementation versions of the same parent class method. Such methods will exhibit polymorphic behavior at runtime (calling the same method does different things). Polymorphism is the most essential part of object-oriented programming. Of course, it’s also the most difficult part for beginners to understand and flexibly apply. We’ll use dedicated examples in the next chapter to explain this knowledge point.

Summary

Python is a dynamically typed language. Objects in Python can dynamically add attributes. Object methods are actually also attributes, except that the attribute corresponds to a callable function. In the object-oriented world, everything is an object. The classes we define are also objects, so classes can also receive messages. The corresponding methods are class methods or static methods. Through inheritance, we can create new classes from existing classes, achieving code reuse of existing classes.