Python object-oriented advanced-features tutorial

The three cornerstones of oop are encapsulation, inheritance, and polymorphism, but Python's OOP is much more than these. It also provides a series of lightweight yet powerful advanced-features, which can help us build more flexible, reusable programs that comply with design patterns. This article will explain them one by one in the order most friendly to the learning curve: multiple inheritance, custom classes, attribute control, abstract base classes, and finally a taste of the most obscure metaclass.


1. Multiple inheritance

💡 Basic Concepts

Multiple inheritance allows a subclass to inherit properties and methods from multiple direct parent classes (base classes) to achieve multi-dimensional code reuse:

# 两个独立的父类
class Father:
    def cook_meat(self):
        print("做红烧肉")

class Mother:
    def cook_vegetable(self):
        print("做青菜沙拉")

# 子类同时继承 Father 和 Mother
class Child(Father, Mother):
    pass

child = Child()
child.cook_meat()      # 继承自 Father
child.cook_vegetable() # 继承自 Mother

🔍 Method parsing order (MRO)

If multiple parent classes or even higher-level ancestor classes have methods with the same name, Python will use the C3 linearization algorithm to determine the calling order to avoid confusion. pass类名.__mro__or类名.mro()You can view this sequence chain:

# 查看 Child 的 MRO(顺序:当前类 → 父类依次 → object)
print(Child.__mro__)
# (<class '__main__.Child'>, <class '__main__.Father'>, <class '__main__.Mother'>, <class 'object'>)

⚠️ Best Practices

Although multiple inheritance is flexible, it can easily introduce complexity. It is recommended to follow three principles:

  1. Try to avoid diamond inheritance (two parent classes share the same top-level ancestor class);
  2. When inheritance must be used, use it firstsuper()Instead of hard-coding the parent class name to call the method;
  3. When complexity increases, use composition instead of inheritance (use instances of other classes as properties).

2. Customized class (magic method)

💡 What is a magic method?

Magic methods are ** in Python classes marked with double underscores__Built-in methods for Beginning and Ending**. You don't need to call them manually, the interpreter will be triggered automatically when a specific operation occurs. The most familiar example is__init__——Constructor that is automatically executed when creating an instance.

By implementing these magic methods, we can make instances of custom classes behave like Python native types (such as making vectors support+len(), subscript access), greatly improving the readability and consistency of the code.

🧪 Complete example: Customize a two-dimensional vector class

class Vector2D:
    def __init__(self, x: int, y: int):
        """构造函数:创建实例时自动初始化坐标"""
        self.x = x
        self.y = y

    def __add__(self, other: "Vector2D") -> "Vector2D":
        """加法魔术方法:对应 v1 + v2"""
        if not isinstance(other, Vector2D):
            raise TypeError("只能和 Vector2D 类型相加")
        return Vector2D(self.x + other.x, self.y + other.y)

    def __str__(self) -> str:
        """给用户看的字符串表示:对应 print(v) 或 str(v)"""
        return f"二维向量(x={self.x}, y={self.y})"

    def __repr__(self) -> str:
        """给开发者看的官方字符串表示:对应 repr(v) 或调试时的输出"""
        return f"Vector2D({self.x}, {self.y})"

    def __len__(self) -> int:
        """长度魔术方法:对应 len(v)(这里我们返回它的维度)"""
        return 2

    def __getitem__(self, index: int) -> int:
        """索引访问魔术方法:对应 v[0], v[1]"""
        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        else:
            raise IndexError("Vector2D 仅支持索引 0 和 1")

# 测试
v1 = Vector2D(2, 3)
v2 = Vector2D(4, 5)
print(v1 + v2)        # 用户友好的输出
print(repr(v1))       # 开发者友好的输出
print(len(v1))        # 维度 2
print(v1[0], v1[1])   # 索引访问

📋 Cheat Sheet for the Most Common Magic Methods

Method nameFunction descriptionTrigger conditions
__init__Constructorobj = 类名(...)
__str__A string displayed to the userprint(obj) / str(obj)
__repr__Official string for developersrepr(obj)/DebugPanel
__len__Define lengthlen(obj)
__getitem__Index/slice accessobj[i] / obj[a:b]
__setitem__Index/slice assignmentobj[i] = val
__iter__Define iterable objectsfor x in obj / iter(obj)
__call__Make the instance callable like a functionobj(...)

3. Attribute access control

Python has no strictprivate / publickeyword, but provides two ways to finely control attribute access: Let’s talk about the more commonly used and simpler ones first.@propertyDecorator, let’s talk about the underlying descriptor protocol.

3.1 @property decorator (most commonly used)

@propertyYou can disguise the ** method as a read-only attribute ** and also cooperate with@属性名.setter / @属性名.deleterImplement validation or additional logic when assigning and deleting:

class Circle:
    def __init__(self, radius: float):
        # 单下划线表示“建议不要直接访问”的内部属性
        self._radius = radius

    @property
    def radius(self) -> float:
        """把 radius 方法伪装成只读属性"""
        return self._radius

    @radius.setter
    def radius(self, value: float) -> None:
        """赋值时验证半径非负"""
        if not isinstance(value, (int, float)):
            raise TypeError("半径必须是数字")
        if value < 0:
            raise ValueError("半径不能为负数")
        self._radius = value

    @property
    def area(self) -> float:
        """只读的计算属性:每次访问都会重新计算"""
        return 3.14159 * self._radius ** 2

# 测试
c = Circle(5)
print(c.radius)  # 像访问属性一样,不用加括号
print(c.area)    # 只读计算属性
c.radius = 10    # 触发 setter 验证
print(c.area)    # 面积自动更新

3.2 Descriptor protocol (underlying mechanism)

If multiple classes need to reuse the same attribute access logic (for example, age in all classes must be greater than 0), you can use the descriptor protocol. A descriptor is an implementation__get__ / __set__ / __delete__A class with at least one method in:

# 可复用的年龄描述符
class PositiveAge:
    def __get__(self, instance, owner):
        """获取属性时调用"""
        return instance._age

    def __set__(self, instance, value):
        """赋值时验证"""
        if not isinstance(value, int) or value <= 0 or value > 150:
            raise ValueError("年龄必须是 1-150 的整数")
        instance._age = value

# 多个类都可以使用同一个描述符
class Student:
    age = PositiveAge()  # 描述符必须绑定为类属性
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age  # 触发 PositiveAge.__set__

class Teacher:
    age = PositiveAge()
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

# 测试
s = Student("小明", 18)
print(s.age)          # -> 18
t = Teacher("李老师", 35)
print(t.age)          # -> 35
# t.age = -1          # 会抛出 ValueError

4. Abstract base class (ABC)

💡 Why use abstract base class?

The core function of Abstract Base Class (ABC) is to define a unified interface specification for a group of subclasses. It can force all direct subclasses to implement a specific abstract method, otherwise the subclass cannot be instantiated. This effectively avoids runtime errors caused by missing core functionality.

🧪 Example: Abstract base class defining shapes

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        """所有形状必须实现的面积计算方法"""
        pass

    @abstractmethod
    def perimeter(self) -> float:
        """所有形状必须实现的周长计算方法"""
        pass

# 子类必须实现所有抽象方法,否则会报错
class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

# 测试
# shape = Shape()       # 直接实例化抽象基类会报错
rect = Rectangle(3, 4)
print(f"矩形面积:{rect.area()}")
print(f"矩形周长:{rect.perimeter()}")

5. Metaclass (just a taste of it)

💡 What is a metaclass?

Everything is an object in Python, and classes themselves are also objects! The class that creates the class is the metaclass. Python’s default metaclass istype, through custom metaclasses, we can control the class creation process, such as automatically registering subclasses, verifying class attributes, dynamically adding methods, etc.

🧪 Example: Using metaclass to implement singleton pattern

The singleton pattern ensures that there is only one instance of a class in the entire program. Custom metaclasses are one of the most elegant implementations in Python:

class SingletonMeta(type):
    # 存储每个类的唯一实例
    _instances = {}

    # 当调用类创建实例时,__call__ 会被自动触发
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            # 第一次调用,通过 type 的 __call__ 真正创建实例
            cls._instances[cls] = super().__call__(*args, **kwargs)
        # 之后直接返回已缓存的实例
        return cls._instances[cls]

# 子类指定 metaclass=SingletonMeta 即可
class Singleton(metaclass=SingletonMeta):
    pass

# 测试
a = Singleton()
b = Singleton()
print(a is b)  # 输出 True,说明是同一个实例

Summarize

Python's OOP advanced-features provide great flexibility, but use them as needed and avoid over-engineering:

  1. Multiple inheritance: multi-dimensional reuse, but giving priority to combination;
  2. Customized classes: Use magic methods to integrate custom classes into the Python ecosystem;
  3. Attribute Control: for a single class@property, descriptors for multiple types of multiplexing logic;
  4. Abstract base class: Defines interface specifications and forces subclasses to implement core functions;
  5. Metaclass: Control the creation process of classes and implement advanced modes (do not touch unless necessary).

After mastering these features, you will be able to use design patterns more freely to write maintainable and extensible Python applications.