sec04 - Practical Class Design: Techniques for Building Safe and Scalable Classes
スポンサーリンク

Techniques for Mastering Classes

In this lesson, we’ll learn how to design Python classes in a safer and more extensible way. We’ll go through special methods, encapsulation, the @property decorator, and how to use constructors and destructors.

Special (Magic) Methods

Basics of Special Methods

In Python, there’s a mechanism that allows your custom classes to work naturally with built-in operations like print() or ==. These are called special methods (or magic methods).
By defining __str__, you can make print(car) display a readable string, and by defining __eq__, you can compare two objects like car1 == car2.

class Car:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model

    def __str__(self):
        return f'{self.brand} {self.model}'

    def __eq__(self, other):
        return self.brand == other.brand and self.model == other.model

Example Output

car1 = Car('Toyota', 'Corolla')
car2 = Car('Toyota', 'Corolla')
car3 = Car('Tesla', 'Model 3')

print(car1)         # Toyota Corolla
print(car3)         # Tesla Model 3
print(car1 == car2) # True
print(car1 == car3) # False

Common Types of Special Methods

Special methods used in this example:

  • __str__: Defines the string representation shown by print()
  • __eq__: Defines comparison using the == operator

Other frequently used special methods include:

  • __repr__: Used by repr() or the interactive console
  • __add__, __sub__: Arithmetic operators
  • __len__: Works with len()
  • __getitem__, __setitem__: Enable index operations
  • __iter__, __next__: Enable iteration

See the full list of special methods in the official documentation: https://docs.python.org/3.14/reference/datamodel.html#emulating-callable-objects

スポンサーリンク

Encapsulation and Access Control

Some data inside a class should not be accessed directly from outside. By adding _ or __ to a variable name, you can control its accessibility.

  • _speed is treated as “for internal use” and should not be modified directly.
  • __engine_on is hidden and cannot be accessed directly; it should only be changed via start_engine or stop_engine.

This ensures safe management of engine states and speed within the Car class.

class Car:
    def __init__(self, speed):
        self._speed = speed       # Internal use only
        self.__engine_on = False  # Hidden variable

    def start_engine(self):
        self.__engine_on = True
        print(f'Started the engine of {self.brand} {self.model}')

    def stop_engine(self):
        self.__engine_on = False
        print(f'Stopped the engine of {self.brand} {self.model}')


my_car = Car(50)
print(my_car._speed)       # 50 (Accessible, but not recommended)
print(my_car.__engine_on)  # Raises AttributeError: 'Car' object has no attribute '__engine_on'

Safely Managing Attributes with @property

Using @property allows you to unify the “getter” and “setter” operations for a variable under the same name. For example, you can retrieve and assign values as if handling a variable named speed. Unlike regular variables, you can implement checks during assignment, such as preventing negative values. This enables safe manipulation without directly touching the _speed variable.

  • Getter method
    • Add @property before the method definition.
    • The getter method returns the value.
  • Setter method
    • Add @methodname.setter (in this case, @speed.setter).
    • Use the same method name as the getter.
    • The assigned value is passed as an argument.
    • You can add checks (e.g., raising an error if the value is invalid).
class Car:
    def __init__(self, brand, model, speed):
        self.brand = brand
        self.model = model
        self._speed = speed  # Internal variable

    @property
    def speed(self):
        # Getter method for retrieving the value
        return self._speed

    @speed.setter
    def speed(self, value):
        # Setter method for assigning a value
        # Raise an error if the value is negative
        if value < 0:
            raise ValueError('Speed must be zero or higher.')
        else:
            self._speed = value

Example Execution

car = Car('Toyota', 'Corolla', 50)
print(car.speed)  # 50

car.speed = 80
print(car.speed)  # 80

car.speed = -10   # ValueError: Speed must be zero or higher.

Constructors and Destructors

A constructor (__init__) is automatically called when an instance is created and is used for initialization or setup. A destructor (__del__) is automatically called when an instance is destroyed and is used for cleanup or resource release.

For example, in a network class, __init__ might prepare a connection, while __del__ safely closes it.

class NetworkController:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        print(f'Preparing connection to {self.host}:{self.port} (constructor)')

    def __del__(self):
        print(f'Safely disconnected from {self.host}:{self.port} (destructor)')


nc = NetworkController('192.168.1.10', 8080)
del nc

Example Output

Preparing connection to 192.168.1.10:8080 (constructor)
Safely disconnected from 192.168.1.10:8080 (destructor)

The destructor is executed either when you explicitly delete an object using the del statement or when Python automatically deletes the object after a function finishes. Even if you comment out the del nc line, the destructor will still run when the object goes out of scope.

Practical Example: Safe and Extensible Class Design

By handling shared behavior through methods, managing attributes safely with @property, and providing a clear display using __str__, you can design classes that are both safe and extensible.

class Car:
    def __init__(self, brand, model, speed):
        self.brand = brand
        self.model = model
        self._speed = speed
        self.__engine_on = False

    def start_engine(self):
        self.__engine_on = True

    def stop_engine(self):
        self.__engine_on = False

    @property
    def speed(self):
        return self._speed

    @speed.setter
    def speed(self, value):
        # Raise an error if the value is negative
        if value < 0:
            raise ValueError('Speed must be zero or higher.')
        else:
            self._speed = value

    def __str__(self):
        return f'{self.brand} {self.model} ({self._speed}km/h)'
スポンサーリンク