Python.三.(三)--3. 面向对象三大特征

三、 函数、类与面向对象编程 (OOP)

  • 标题:
    1. 函数参数与高阶函数
    2. 装饰器与闭包
    3. 面向对象三大特征
    4. 类、对象与魔术方法
    5. 异常处理


介绍一下Python中的封装(Encapsulation)思想

封装 (Encapsulation) 在 Python 中的概念

封装是面向对象编程(OOP)的四大基本原则之一,其他三个是继承(Inheritance)、多态(Polymorphism)和抽象(Abstraction)。封装的核心思想是将对象的数据(属性)和行为(方法)打包在一起,并限制外界对它们的直接访问。通过封装,开发人员可以控制哪些数据可以从外部访问,哪些只能在类的内部使用。

Python 虽然不像一些其他面向对象的编程语言(如 Java、C++)那样严格地限制数据的访问,但它依然支持通过命名约定和访问控制来实现封装的概念。

封装的主要思想

封装主要涉及以下几个方面:

  1. 隐藏内部实现:对象的内部状态对外界不可见,外界只能通过公开的接口(即方法)访问或修改对象的状态。
  2. 保护对象的完整性:通过封装,类的设计者可以控制外部如何访问或修改内部数据,避免外部对内部数据进行非法的操作,确保对象的一致性和完整性。
  3. 提供安全的访问接口:通过定义类的公有方法(public methods),外部可以在不直接操作内部数据的情况下,安全地对对象进行操作。

Python 中的封装机制

在 Python 中,封装的实现主要依赖命名约定访问控制,Python 没有像某些编程语言那样提供明确的访问权限控制符(如 Java 的 publicprivateprotected),但它有一些约定俗成的规则来实现封装。

1. 公有成员 (Public Members)

在 Python 中,默认情况下,类的所有属性和方法都是公有的(public)。这意味着外部可以直接访问或修改这些属性和方法。例如:

class MyClass:
    def __init__(self, name):
        self.name = name  # 公有属性

    def greet(self):  # 公有方法
        return f"Hello, {self.name}"

# 使用
obj = MyClass("Alice")
print(obj.name)      # 直接访问公有属性
print(obj.greet())   # 调用公有方法

在这个例子中,name 属性和 greet() 方法都是公有的,外部可以直接访问它们。

2. 私有成员 (Private Members)

在 Python 中,使用双下划线 (__) 开头的属性或方法被认为是私有的,不能被类外部直接访问。这是通过名称重整(name mangling)实现的,Python 会在属性名前加上类名来避免外部访问它们。

class MyClass:
    def __init__(self, name):
        self.__name = name  # 私有属性

    def __private_method(self):  # 私有方法
        return f"Hello, {self.__name}"

    def public_method(self):
        return self.__private_method()  # 公有方法调用私有方法

# 使用
obj = MyClass("Alice")
# print(obj.__name)           # 会抛出 AttributeError
# print(obj.__private_method())  # 会抛出 AttributeError
print(obj.public_method())     # 可以通过公有方法间接访问私有方法

在这个例子中,__name 属性和 __private_method() 方法是私有的,外部无法直接访问它们。如果尝试访问,会报 AttributeError 错误。但是,可以通过类内部的公有方法来访问私有成员。

注意:虽然双下划线的属性和方法是"私有"的,但实际上 Python 只是对它们的名称进行了重整。你可以通过 _ClassName__attribute 的方式来访问它们,Python 并没有完全禁止访问。这种设计更多的是一种"约定"而不是强制的隐藏。

# 通过名称重整访问私有属性
print(obj._MyClass__name)  # 通过 name mangling 访问私有属性
3. 受保护成员 (Protected Members)

在 Python 中,使用单下划线 (_) 开头的属性或方法被认为是受保护的,这是一个弱封装的约定。受保护的成员不建议在类外部直接访问,但并没有强制限制,可以通过子类继承和扩展时访问。

class MyClass:
    def __init__(self, name):
        self._name = name  # 受保护属性

    def _protected_method(self):  # 受保护方法
        return f"Hello, {self._name}"

# 使用
obj = MyClass("Alice")
print(obj._name)            # 可以访问受保护属性,但不建议
print(obj._protected_method())  # 可以访问受保护方法,但不建议

受保护的成员可以在类外部访问,但一般在设计时,约定不应该直接访问这些成员,通常用于类内部或子类中。

4. 公有方法与私有属性的结合使用

一个常见的封装模式是将类的属性设置为私有,然后通过公有的方法(通常称为gettersetter方法)来控制外界如何访问或修改这些属性。这种方法允许对属性的访问进行更精细的控制,避免不当的操作。

class MyClass:
    def __init__(self, name):
        self.__name = name  # 私有属性

    def get_name(self):  # getter 方法
        return self.__name

    def set_name(self, new_name):  # setter 方法
        if isinstance(new_name, str):
            self.__name = new_name
        else:
            raise ValueError("Name must be a string")

# 使用
obj = MyClass("Alice")
print(obj.get_name())   # 通过 getter 访问私有属性
obj.set_name("Bob")     # 通过 setter 修改私有属性
print(obj.get_name())

通过这种设计,程序员可以确保只有经过验证的数据才能修改属性。比如在 set_name 方法中,我们检查输入是否为字符串,如果不是,则抛出异常。这种方式有效地保护了类的内部状态。

5. 属性装饰器 (@property) 的使用

Python 提供了 @property 装饰器来简化 getter 和 setter 方法的定义,允许我们像访问普通属性一样调用方法。这是一种更 Pythonic 的封装方式。

class MyClass:
    def __init__(self, name):
        self.__name = name  # 私有属性

    @property
    def name(self):  # getter 方法
        return self.__name

    @name.setter
    def name(self, new_name):  # setter 方法
        if isinstance(new_name, str):
            self.__name = new_name
        else:
            raise ValueError("Name must be a string")

# 使用
obj = MyClass("Alice")
print(obj.name)   # 通过属性访问
obj.name = "Bob"  # 修改属性
print(obj.name)

@property 允许你将方法包装成属性的形式,从而使类的使用更加直观,同时保持了封装性。

  • @property:将方法转化为属性,用于读取。
  • @name.setter:为属性定义赋值逻辑,用于写入。

🆕 5.1 @property.deleter 与只读属性

class Config:
    def __init__(self, db_url):
        self._db_url = db_url

    @property
    def db_url(self):
        """只读属性:外部可读取但不能直接修改"""
        return self._db_url

    @db_url.deleter
    def db_url(self):
        """删除属性时的清理逻辑"""
        print("Cleaning up database connection...")
        del self._db_url

config = Config("postgresql://localhost:5432/mydb")
print(config.db_url)       # ✅ 可读取
# config.db_url = "xxx"    # ❌ 没有 setter → AttributeError
del config.db_url          # ✅ 触发 deleter

🆕 5.2 只读计算属性模式

import time

class Circle:
    def __init__(self, radius):
        self.radius = radius  # 半径是可变的

    @property
    def area(self):
        """计算属性:每次读取时实时计算,不可赋值"""
        return 3.14159 * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

c = Circle(5)
print(c.area)        # 78.53975
c.radius = 10
print(c.area)        # 314.159(自动更新)
# c.area = 100       # ❌ 只读属性无法赋值
🆕 6. 描述符协议 (Descriptor Protocol) —— @property 的底层原理

@property 本质上是一个数据描述符 (Data Descriptor)。理解描述符协议是掌握 Python 封装高级用法的关键。

描述符协议:一个类如果实现了以下任意方法,就是一个描述符:

  • __get__(self, obj, objtype=None) → 用于属性访问
  • __set__(self, obj, value) → 用于属性赋值
  • __delete__(self, obj) → 用于属性删除 (del)
# 手写一个非负数校验描述符(等价于带校验的 property)
class NonNegative:
    """确保属性的值永远 >= 0"""
    def __init__(self, default=0):
        self.default = default
        self.private_name = None  # 将在 __set_name__ 中设置

    def __set_name__(self, owner, name):
        # Python 3.6+ 自动调用:告诉我们被赋给了哪个属性名
        self.private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, self.default)

    def __set__(self, obj, value):
        if value < 0:
            raise ValueError(f"{self.private_name} must be >= 0, got {value}")
        setattr(obj, self.private_name, value)


class Product:
    price = NonNegative(0)      # 使用描述符
    stock = NonNegative(0)      # 复用同一个描述符类

    def __init__(self, price, stock):
        self.price = price      # 触发 NonNegative.__set__
        self.stock = stock


p = Product(99.9, 50)
print(p.price)                 # 99.9 → 触发 NonNegative.__get__
# p.price = -1                # ❌ ValueError: _price must be >= 0

关键洞察

特性@property自定义描述符
复用性每个属性单独定义一个类可用于多个属性
适用场景单一属性的特殊逻辑跨多个属性的通用校验/转换逻辑
底层关系@property 就是一个描述符工厂描述符是更底层的原语

PyTorch 的 nn.Parameter、Django ORM 的 Field 都是描述符协议的实际应用。

🆕 7. __slots__ —— 内存优化与属性封禁

__slots__ 是 Python 提供的一个类级别优化选项,同时具有封装增强的作用。

class WithoutSlots:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class WithSlots:
    __slots__ = ('x', 'y')   # 声明允许的属性名

    def __init__(self, x, y):
        self.x = x
        self.y = y

# ---- 功能差异 ----
a = WithoutSlots(1, 2)
a.z = 999              # ✅ 可以随意添加新属性

b = WithSlots(1, 2)
b.z = 999              # ❌ AttributeError: 'WithSlots' object has no attribute 'z'

# ---- 内存差异 ----
import sys
print(sys.getsizeof(WithoutSlots(1, 2)))  # 通常 56 bytes (+ __dict__)
print(sys.getsizeof(WithSlots(1, 2)))     # 通常 32 bytes (无 __dict__)

__slots__ 的封装意义

  1. 防止拼写错误obj.nmae = 5 不会静默创建错误属性,而是立即报错
  2. 节省内存:省去 __dict__ 和 __weakref__,适合创建大量小对象的场景
  3. 加速属性访问: slots 使用 descriptor 而非 dict 查找,略快

⚠️ 继承注意事项

  • 子类不会自动继承父类的 __slots__,除非子类也定义了 __slots__
  • 如果父类没有定义 __slots__,子类定义了 __slots__ 则无效(因为 __dict__ 已存在)
  • 正确做法:父类和子类都定义 __slots__
class Base:
    __slots__ = ('x',)

class Derived(Base):
    __slots__ = ('y',)   # 子类额外声明的 slot

d = Derived()
d.x = 1   # ✅ 来自 Base
d.y = 2   # ✅ 来自 Derived
# d.z = 3 # ❌ AttributeError
🆕 8. 模块级封装

Python 的封装不止于类层面,模块(module) 也有自己的封装机制:

# ===== my_module.py =====

__all__ = ['public_func', 'PUBLIC_CONST']  # 显式导出 API

PUBLIC_CONST = 42

def public_func():
    return "I'm part of the public API"

def _internal_helper():
    """单下划线:约定为内部实现,from X import * 不会被导入"""
    return "Implementation detail"

__secret_var = "double underscore: 更强的私有暗示"

# ===== 使用方 =====
from my_module import *
print(public_func())     # ✅ 在 __all__ 中
# print(_internal_helper())  # ❌ import * 不会导入
# print(__secret_var)       # ❌ 名称已被重整为 _my_module__secret_var
机制作用域强制程度
_name (单下划线)from X import * 不导入弱约定(仍可直接 import)
__name (双下划线)名称重整为 _mod__name中等(需知道重整规则才能访问)
__all__控制 from X import * 的行为弱(不影响显式 import)
🆕 9. dataclass 与封装的最佳实践

Python 3.7+ 的 dataclass 结合 field() 可以写出简洁且封装良好的代码:

from dataclasses import dataclass, field

@dataclass
class ModelConfig:
    """AI 模型配置 —— dataclass 封装示例"""
    model_name: str
    learning_rate: float = 1e-3
    batch_size: int = 32
    _internal_cache: dict = field(default_factory=dict, repr=False, init=False)

    def __post_init__(self):
        """构造后的校验逻辑"""
        if self.learning_rate <= 0:
            raise ValueError("learning_rate must be positive")
        if not self.model_name:
            raise ValueError("model_name cannot be empty")

    @property
    def effective_steps_per_epoch(self):
        """计算属性:根据 batch_size 动态计算"""
        return 100000 // self.batch_size  # 假设训练集 100k 样本

config = ModelConfig("gpt-2", learning_rate=5e-4)
print(config)                    # ModelConfig(model_name='gpt-2', learning_rate=0.0005, ...)
print(config.effective_steps_per_epoch)  # 3125
# ModelConfig("", 1e-3)         # ❌ ValueError

dataclass 封装要点

  • init=False:不在 __init__ 中暴露(类似私有属性)
  • repr=False:不在 __repr__ 中显示(隐藏内部细节)
  • __post_init__:替代 setter 的校验逻辑
  • frozen=True:创建不可变对象(最强封装)
🆕 10. 反模式:过度封装 (Over-Encapsulation)

Python 哲学:“We are all consenting adults here.” —— 我们都是成年人,不需要保姆式的保护。

# ❌ 反模式:Java 风格的过度封装
class BadEncapsulation:
    def __init__(self, value):
        self.set_value(value)

    def get_value(self):
        return self._value

    def set_value(self, value):
        self._value = value

# ✅ Pythonic 写法:简单场景直接用公有属性
class GoodEncapsulation:
    def __init__(self, value):
        self.value = value  # 先保持简单

# 只有当需要在赋值时添加校验/副作用时才升级为 property
class WhenNeeded:
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, val):
        if val < 0:
            raise ValueError("must be non-negative")
        self._value = val

判断是否需要封装的自检清单

  1. 赋值时需要校验吗?→ 需要 → 用 @property
  2. 取值时需要计算/缓存吗?→ 需要 → 用 @property
  3. 需要控制外部能否删除该属性吗?→ 需要 → 加 @x.deleter
  4. 以上都不需要?→ 直接用公有属性,保持简单
🆕 11. AI/ML 框架实战:PyTorch nn.Module 的封装设计
import torch
import torch.nn as nn

class SimpleTransformerBlock(nn.Module):
    """展示 PyTorch 中如何运用封装思想"""

    def __init__(self, d_model: int, n_heads: int, dropout: float = 0.1):
        super().__init__()
        # 私有参数:不希望外部直接修改
        self._d_model = d_model
        self._n_heads = n_heads

        # 公有子模块:PyTorch 约定用 .layer_name 暴露
        self.attention = nn.MultiheadAttention(d_model, n_heads, dropout=dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, 4 * d_model),
            nn.GELU(),
            nn.Linear(4 * d_model, d_model),
        )
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    @property
    def head_dim(self) -> int:
        """计算属性:每个注意力头的维度"""
        return self._d_model // self._n_heads

    def forward(self, x, mask=None):
        # 内部实现细节被封装
        attn_out, _ = self.attention(x, x, x, attn_mask=mask)
        x = self.norm1(self.dropout(attn_out) + x)
        ffn_out = self.ffn(x)
        x = self.norm2(self.dropout(ffn_out) + x)
        return x

block = SimpleTransformerBlock(d_model=512, n_heads=8)
print(block.head_dim)  # 64 — 通过 property 安全访问计算属性
# block._d_model = 128  # ⚠️ 技术上可以,但违反封装约定

PyTorch 封装惯例总结

  • 构造参数 → 存储为私有 (_d_model)
  • 子模块 → 公有属性 (self.attention),便于外部访问和检查
  • 派生量 → @property 计算 (head_dim)
  • 核心逻辑 → forward() 方法,外部只需调用即可

封装的优势

  1. 提高代码的安全性

    • 封装隐藏了类的内部细节,防止外部对内部属性进行非法操作,减少了数据不一致或无效数据的风险。
  2. 提高代码的灵活性

    • 通过封装,可以灵活地修改类的内部实现,而无需修改类的外部使用代码。这种设计允许类的实现细节发生变化而不影响其接口,具有较高的扩展性。
  3. 更好的代码维护性

    • 封装使得代码更加模块化,每个类或模块只暴露必要的接口,减少了耦合性,增强了代码的可维护性。
  4. 控制属性访问

    • 通过 getter 和 setter 方法,可以控制对属性的访问和修改操作,确保类的内部状态始终有效。

🆕 5. 内存效率提升__slots__ 可显著降低大量小对象的内存占用。

🆕 6. 接口稳定性:模块级封装(__all__)保证公共 API 的向后兼容。

封装与其他 OOP 概念的关系

  • 封装与继承:封装可以结合继承一起使用,通过子类继承父类的公有方法和受保护的属性,封装性依然得以保持。
  • 封装与多态:封装和多态相辅相成,封装允许将实现隐藏,而多态允许对象在运行时决定具体调用的实现,使得代码的扩展性更强。
  • 🆕 封装与组合:良好的封装使得对象可以作为"黑盒子"被组合到更大的系统中,是组合优于继承的基础。

介绍一下Python中的继承(Inheritance)思想

继承是面向对象编程(OOP)的一个核心概念,它允许一个类(称为子类或派生类)从另一个类(称为父类或基类)继承属性和方法。子类可以继承父类的特性,并且可以在此基础上添加自己的新特性,从而实现代码的重用和扩展。Python 作为一门支持面向对象编程的语言,提供了强大的继承机制。

Python中继承的优势:

  1. 代码重用:子类可以直接使用父类已经定义的方法和属性,避免了重复编写相同的代码片段。
  2. 可扩展性:子类可以在不修改父类的情况下,添加新的属性和方法,从而使得代码更具可扩展性。这样可以在不影响父类的基础上,为程序添加新的功能。

一、继承的基本概念

1. 父类(基类)
  • 定义:被继承的类,提供基本的属性和方法。
  • 作用:作为子类的模板,子类可以继承父类的属性和方法。
2. 子类(派生类)
  • 定义:从父类继承而来的类,可以新增或重写父类的方法和属性。
  • 作用:在继承父类的基础上进行扩展或修改,实现特定的功能。
3. 继承的目的
  • 代码重用:避免重复编写相同的代码,提高开发效率。
  • 可扩展性:通过继承,子类可以扩展父类的功能。
  • 多态性:同一个方法在不同的类中可能有不同的实现,增强程序的灵活性。

二、Python 中的继承实现

1. 基本语法

在 Python 中,继承通过在类定义时指定父类来实现。

class 子类名(父类名):
    # 类的定义
2. 示例

父类:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        pass

子类:

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

使用子类:

dog = Dog("Buddy")
cat = Cat("Kitty")

print(dog.speak())  # 输出: Buddy says Woof!
print(cat.speak())  # 输出: Kitty says Meow!

三、继承的类型

1. 单继承
  • 定义:一个子类只继承一个父类。

  • 示例

    class Parent:
        pass
    
    class Child(Parent):
        pass
    
2. 多重继承
  • 定义:一个子类继承多个父类。

  • 语法

    class 子类名(父类1, 父类2, ...):
        pass
    
  • 示例

    class Flyable:
        def fly(self):
            return "I can fly!"
    
    class Swimmable:
        def swim(self):
            return "I can swim!"
    
    class Duck(Flyable, Swimmable):
        pass
    
    duck = Duck()
    print(duck.fly())   # 输出: I can fly!
    print(duck.swim())  # 输出: I can swim!
    
3. 多层继承
  • 定义:子类继承父类,父类再继承其父类,形成继承链。

  • 示例

    class GrandParent:
        pass
    
    class Parent(GrandParent):
        pass
    
    class Child(Parent):
        pass
    

四、方法重写(Override)

  • 定义:子类重新定义父类的同名方法,以实现不同的功能。
  • 作用:让子类能够根据需要修改或扩展父类的方法行为。
示例:
class Vehicle:
    def move(self):
        print("The vehicle is moving.")

class Car(Vehicle):
    def move(self):
        print("The car is driving on the road.")

vehicle = Vehicle()
car = Car()

vehicle.move()  # 输出: The vehicle is moving.
car.move()      # 输出: The car is driving on the road.

五、调用父类的方法

  • 使用 super() 函数:在子类中调用父类的方法或初始化父类。

  • 语法

    class 子类名(父类名):
        def 方法名(self, 参数):
            super().方法名(参数)
    
示例:
class Person:
    def __init__(self, name):
        self.name = name

class Employee(Person):
    def __init__(self, name, employee_id):
        super().__init__(name)  # 调用父类的构造函数
        self.employee_id = employee_id

employee = Employee("Alice", "E123")
print(employee.name)         # 输出: Alice
print(employee.employee_id)  # 输出: E123

六、继承中的特殊方法

1. __init__ 构造函数
  • 继承特性:子类的 __init__ 方法会覆盖父类的 __init__ 方法。
  • 注意:如果子类定义了 __init__ 方法,需要显式调用父类的 __init__ 方法来初始化父类的属性。

示例:

class Parent:
    def __init__(self):
        print("Parent init")

class Child(Parent):
    def __init__(self):
        super().__init__()  # 调用父类的构造函数
        print("Child init")

child = Child()
# 输出:
# Parent init
# Child init
2. __str__ 和 __repr__ 方法
  • 作用:定义对象的字符串表示形式。
  • 继承特性:子类可以重写这些方法,提供自定义的字符串表示。

示例:

class Animal:
    def __str__(self):
        return "This is an animal."

class Dog(Animal):
    def __str__(self):
        return "This is a dog."

dog = Dog()
print(dog)  # 输出: This is a dog.

七、继承的注意事项

1. 访问权限
  • Python 中不存在像 Java 或 C++ 那样的访问修饰符(public、private、protected)。

  • 以双下划线 __ 开头的属性或方法被视为私有成员,不能在子类中直接访问。

  • 示例

    class Parent:
        def __init__(self):
            self.__private_var = 42
    
    class Child(Parent):
        def get_private_var(self):
            return self.__private_var  # 这将引发 AttributeError
    
    child = Child()
    print(child.get_private_var())
    
2. 方法解析顺序(MRO)
  • 在多重继承中,Python 使用**方法解析顺序(Method Resolution Order, MRO)**来确定属性和方法的查找顺序。
  • 可以使用 类名.mro() 查看 MRO 列表。

示例:

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

print(D.mro())
# 输出: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

🆕 八、MRO 与 C3 线性化算法 —— 深入理解

MRO 不是"魔法",而是由 C3 线性化算法确定的。理解这个算法对于排查多重继承 bug 至关重要。

C3 算法核心规则

给定一个类 C 的继承列表 [C, (C1, C2, ..., Cn)],MRO 的计算遵循:

  1. C 的 MRO = [C] + merge(C1 的 MRO, C2 的 MRO, …, Cn 的 MRO, [C1, C2, ..., Cn])
  2. merge 操作:取每个列表的头部(第一个元素),如果该元素不在任何列表的尾部(除头部外的其余部分),则取出;否则跳过该列表,尝试下一个
图解:菱形继承的 MRO 计算
    A
   / \
  B   C
   \ /
    D(B, C)
class A:
    def who_am_i(self):
        return "A"

class B(A):
    def who_am_i(self):
        return "B"

class C(A):
    def who_am_i(self):
        return "C"

class D(B, C):
    pass

逐步计算 D 的 MRO

L[D] = D + merge(L[B], L[C], BC)
     = D + merge(BA, CA, BC)        ← L[B]=[B,A], L[C]=[C,A], BC=[B,C]

Step 1: 取 B(B 不在任何 tail 中)✅ → [D, B]
        merge(A, CA, C)             ← B 已取出,剩余

Step 2: 取 A → A 在 [C, A] 的 tail 中 ❌ 跳过
        取 C → C 不在任何 tail 中 ✅ → [D, B, C]
        merge(A, A)                ← C 已取出

Step 3: 取 A ✅ → [D, B, C, A]

最终 L[D] = [D, B, C, A, object]
print(D.mro())
# [<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]

d = D()
print(d.who_am_i())  # "B" — B 在 MRO 中排在 C 前面

面试重点:为什么 D 的 MRO 是 D→B→C→A 而不是 D→C→B→A?

因为 D 的定义是 class D(B, C) —— B 在前优先。C3 保证了这个"局部优先序"的同时还满足单调性。


🆕 九、菱形继承问题 (Diamond Problem)

当多重继承形成菱形结构时,如何确保祖先类只被初始化一次?

      A (有 __init__)
     / \
    B   C (都调用了 super().__init__())
     \ /
      D

传统语言的困境:C++ 中 A 会被初始化两次(B 初始化一次,C 初始化一次)。

Python 的解决方案协作式多重继承 (Cooperative MI) + super() 的 MRO 链式调用

class A:
    def __init__(self):
        print("  A.__init__")
        self.a_val = "from A"

class B(A):
    def __init__(self):
        print("  B.__init__ (before super)")
        super().__init__()  # 不硬编码 A,而是调用 MRO 中的下一个
        print("  B.__init__ (after super)")
        self.b_val = "from B"

class C(A):
    def __init__(self):
        print("  C.__init__ (before super)")
        super().__init__()
        print("  C.__init__ (after super)")
        self.c_val = "from C"

class D(B, C):
    def __init__(self):
        print("D.__init__ (before super)")
        super().__init__()  # 一行 super() 触发整条 MRO 链
        print("D.__init__ (after super)")

print("MRO:", [cls.__name__ for cls in D.mro()])
d = D()

# 输出:
# MRO: ['D', 'B', 'C', 'A', 'object']
# D.__init__ (before super)
#   B.__init__ (before super)
#   C.__init__ (before super)
#     A.__init__        ← A 只被初始化了一次!
#   C.__init__ (after super)
#   B.__init__ (after super)
# D.__init__ (after super)

关键结论

  • super() 不是"调用父类",而是调用 MRO 中的下一个类
  • 只要每个类的 __init__ 都使用 super().__init__(),就能保证每个类恰好初始化一次
  • 这就是为什么 Python 的 super() 推荐无参调用(Python 3+)

🆕 十、Mixin 模式 —— Python 多重继承的最佳实践

Mixin 是一种特殊的类,它:

  1. 不单独实例化(通常不定义 __init__
  2. 提供单一功能(单一职责)
  3. 通过组合混入到目标类(命名以 Mixin / able 结尾)
# ===== 定义 Mixin 类 =====

class JSONSerializableMixin:
    """提供 JSON 序列化能力"""

    def to_json(self) -> str:
        import json
        # 只序列化公有属性
        data = {k: v for k, v in self.__dict__.items() if not k.startswith('_')}
        return json.dumps(data, default=str)


class LoggingMixin:
    """提供方法调用日志能力"""

    def log_call(self, method_name: str, *args, **kwargs):
        print(f"[LOG] {self.__class__.__name__}.{method_name} called "
              f"with args={args}, kwargs={kwargs}")


class ValidationMixin:
    """提供数据校验能力"""

    def validate_required(self, *fields):
        for field in fields:
            if not getattr(self, field, None):
                raise ValueError(f"{field} is required")


# ===== 目标类通过多重继承组合 Mixin =====

class User(JSONSerializableMixin, LoggingMixin, ValidationMixin):
    def __init__(self, username, email):
        self.username = username
        self.email = email

    def save(self):
        self.log_call('save')
        self.validate_required('username', 'email')
        print(f"Saving user: {self.to_json()}")


u = User("alice", "alice@example.com")
u.save()
# [LOG] User.save called with args=(), kwargs={}
# Saving user: {"username": "alice", "email": "alice@example.com"}

Mixin 命名规范

命名风格含义示例
XxxMixin明确标识为 MixinLoggingMixin
Xxxable表示具备某种能力Serializable
以 _ 开头表示内部 Mixin_CacheMixin

Mixin 在 AI 框架中的应用

# PyTorch 风格的 Mixin 示例
class GradientCheckpointingMixin:
    """混合梯度 checkpoint 以节省显存"""

    def enable_gradient_checkpointing(self):
        if hasattr(self, 'forward'):
            original_forward = self.forward

            def checkpointed_forward(*args, **kwargs):
                from torch.utils.checkpoint import checkpoint
                return checkpoint(original_forward, *args, **kwargs,
                                  use_reentrant=False)

            self.forward = checkpointed_forward
            print(f"[{self.__class__.__name__}] Gradient checkpointing enabled")


class MyTransformer(GradientCheckpointingMixin, nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = nn.Linear(1024, 1024)

    def forward(self, x):
        return self.layer(x)


model = MyTransformer()
model.enable_gradient_checkpointing()  # 动态混入能力

🆕 十一、组合优于继承 (Composition over Inheritance)

原则:如果两个类之间的关系不是"is-a"(猫是动物),而是"has-a"(汽车有引擎),应该使用组合而非继承。

# ❌ 反例:滥用继承 —— 引擎并不是一种"汽车"
class Vehicle:
    def move(self): pass

class Engine(Vehicle):  # Engine is-a Vehicle? 语义错误!
    def start(self): pass

class Car(Engine):      # Car is-a Engine? 更加奇怪
    pass


# ✅ 正解:使用组合 —— 汽车"拥有"引擎
class Engine:
    def start(self):
        print("Engine started")

    def stop(self):
        print("Engine stopped")


class Wheel:
    def rotate(self):
        print("Wheel rotating")


class Car:
    """Car has-an Engine and has Wheels"""

    def __init__(self):
        self.engine = Engine()   # 组合
        self.wheels = [Wheel() for _ in range(4)]

    def start(self):
        self.engine.start()

    def drive(self):
        for wheel in self.wheels:
            wheel.rotate()


# ===== 决策指南 =====
#
# 问自己:X is a Y 吗?
#   ✓ 是 → 用继承  (Dog is an Animal)
#   ✗ 否 → 用组合  (Car has an Engine; Employee has a SalaryCalculator)

GoF 设计原则原文

“Favor ‘object composition’ over ‘class inheritance’. […] Inheritance breaks encapsulation (subclass depends on parent’s implementation details).”

继承破坏封装性的原因:子类依赖于父类的内部实现细节,父类的改动可能波及所有子类(脆弱基类问题)。


🆕 十二、__init_subclass__ —— 子类注册与约束钩子

Python 3.6+ 提供的类钩子,在子类被定义时自动调用(注意:不是在实例化时)。

class PluginBase:
    """插件基类:自动注册所有子类"""

    _registry = {}  # plugin_name -> subclass

    def __init_subclass__(cls, plugin_name=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if plugin_name is None:
            raise TypeError(
                f"Subclass {cls.__name__} must specify 'plugin_name' keyword argument"
            )
        cls._registry[plugin_name] = cls
        cls.plugin_name = plugin_name
        print(f"[Auto-register] Plugin '{plugin_name}' → {cls.__name__}")

    def run(self, data):
        raise NotImplementedError


# ===== 定义插件子类 =====
class TextProcessor(PluginBase, plugin_name="text"):
    def run(self, data):
        return data.lower()


class ImageProcessor(PluginBase, plugin_name="image"):
    def run(self, data):
        return f"[IMAGE processed: {len(data)} bytes]"


# ===== 使用注册表 =====
print("Available plugins:", list(PluginBase._registry.keys()))
# ['text', 'image']

processor = PluginBase._registry["text"]()
print(processor.run("HELLO WORLD"))  # "hello world"

实际应用场景

  • 插件系统:自动发现和注册处理器(如 FastAPI 的路由注册)
  • 契约强制:要求子类实现特定属性或方法
  • ORM 映射:SQLAlchemy 的 declarative_base 就是基于此机制

🆕 十三、super() 的本质深入

super() 到底返回什么?
class A:
    def method(self):
        print("A.method", self)

class B(A):
    def method(self):
        print("B.method (before super)", self)
        super().method()   # super() 返回的是什么?
        print("B.method (after super)", self)

b = B()
b.method()

super() 返回的是一个代理对象 (proxy),它:

  1. 记录了当前类的 MRO
  2. 记录了当前类在 MRO 中的位置
  3. 属性查找从 MRO 的下一个位置开始
# 验证 super() 的行为
print(super(B, b))  # <super: <class 'B'>, <B object>>
# 这个代理对象在查找 attribute 时,
# 会从 B 的 MRO 中 B 的下一个类开始找

为什么推荐无参 super()(Python 3+)?

  • Python 2 必须写 super(ClassName, self)
  • Python 3 的 super() 在类方法内部会自动推断当前类和 self
  • 无参写法在重构类名时不会出错

super() 的常见陷阱

# ⚠️ 陷阱:在类外部使用 super()
class Parent:
    def __init__(self):
        self.value = 10

class Child(Parent):
    def __init__(self):
        super().__init__()

# 在类外(非方法内)不能使用无参 super()
# super()  # RuntimeError: super(): no arguments

🆕 十四、AI/ML 框架实战:HuggingFace PreTrainedModel 继承体系

# 简化的 HuggingFace 模型继承链
from abc import abstractmethod
from typing import Optional, Dict, Any
import torch
import torch.nn as nn


# ===== 第一层:模型基类 =====
class PreTrainedModel(nn.Module):
    """HF 所有模型的根类 —— 展示大型项目中的继承设计"""

    config_class = None  # 类变量:子类必须覆写
    base_model_prefix = "model"

    def __init__(self, config):
        super().__init__()
        self.config = config

    @abstractmethod
    def forward(
        self,
        input_ids: Optional[torch.Tensor] = None,
        attention_mask: Optional[torch.Tensor] = None,
        **kwargs
    ) -> Dict[str, torch.Tensor]:
        """所有子类必须实现 forward"""
        ...

    def save_pretrained(self, output_dir):
        """通用保存逻辑 —— 所有子类共享,无需各自实现"""
        print(f"[Save] Saving {self.__class__.__name__} to {output_dir}")
        # 保存 config.json + model.safetensors
        self.config.save_pretrained(output_dir)
        torch.save(self.state_dict(), f"{output_dir}/model.pt")

    @classmethod
    def from_pretrained(cls, pretrained_path):
        """通用加载逻辑 —— 工厂方法模式"""
        print(f"[Load] Loading {cls.__name__} from {pretrained_path}")
        config = cls.config_class.from_pretrained(pretrained_path)
        model = cls(config)
        state_dict = torch.load(f"{pretrained_path}/model.pt")
        model.load_state_dict(state_dict)
        return model

    def gradient_checkpointing_enable(self):
        """可选能力:通过 mixin 或直接定义"""
        self.gradient_checkpointing = True


# ===== 第二层:具体模型 =====
class GPT2Model(PreTrainedModel):
    """GPT-2 模型 —— 继承通用能力,专注自身逻辑"""

    config_class = GPT2Config  # 绑定配置类

    def __init__(self, config: GPT2Config):
        super().__init__(config)
        # GPT-2 特有的层结构
        self.wte = nn.Embedding(config.vocab_size, config.hidden_size)
        self.wpe = nn.Embedding(config.max_position_embeddings, config.hidden_size)
        self.h = nn.ModuleList([
            GPT2Block(config) for _ in config.num_hidden_layers
        ])
        self.ln_f = nn.LayerNorm(config.hidden_size)

    def forward(self, input_ids, attention_mask=None, **kwargs):
        # GPT-2 特有的前向传播逻辑
        positions = torch.arange(input_ids.shape[-1])
        hidden_states = self.wte(input_ids) + self.wpe(positions)

        for block in self.h:
            hidden_states = block(hidden_states, attention_mask)

        hidden_states = self.ln_f(hidden_states)
        return {"last_hidden_state": hidden_states}


# ===== 第三层:带任务头的派生类 =====
class GPT2ForSequenceClassification(GPT2Model):
    """GPT-2 文本分类 —— 继承 GPT2Model,添加分类头"""

    def __init__(self, config):
        super().__init__(config)
        # 新增:分类头(组合)
        self.classifier = nn.Linear(config.hidden_size, config.num_labels)
        self.num_labels = config.num_labels

    def forward(self, input_ids, attention_mask=None, labels=None, **kwargs):
        # 复用父类的 transformer 编码
        outputs = super().forward(input_ids, attention_mask, **kwargs)
        logits = self.classifier(outputs["last_hidden_state"][:, -1, :])

        loss = None
        if labels is not None:
            loss_fn = nn.CrossEntropyLoss()
            loss = loss_fn(logits, labels)

        return {"loss": loss, "logits": logits}


# ===== 使用示例 =====
model = GPT2ForSequenceClassification.from_pretrained("./my-gpt2-classifier")
result = model(input_ids=torch.tensor([[101, 202, 303]]))
model.save_pretrained("./updated-model")  # 继承自 PreTrainedModel 的通用方法

这个继承体系的设计亮点

层次职责继承的价值
L1PreTrainedModel通用能力(save/load/checkpoint)子类免费获得
L2GPT2Model模型特有结构复用 L1,专注自身
L3GPT2ForSeqCls任务头复用 L1+L2,只加分类头

介绍一下Python中的多态(Polymorphism)思想

多态(Polymorphism) 是面向对象编程(OOP)的核心概念之一,指的是同一操作作用于不同对象时,能够产生不同的解释和行为。简单来说,多态允许我们在不考虑对象具体类型的情况下,对不同类型的对象执行相同的操作。在 Python 中,多态性通过动态类型和灵活的对象模型得以实现。

一、什么是多态?

1. 定义
  • 多态性(Polymorphism):源自希腊语,意为"多种形式"。在编程中,它指的是同一操作在不同对象上具有不同的行为
2. 多态的类型(🆕 补充完整分类)

多态在编程语言理论中有三种基本形式:

多态类型实现方式Python 支持情况
Ad-hoc 多态(特殊多态)方法重载、运算符重载运算符重载 ✅;方法重载用默认参数模拟 ✅
子类型多态(Subtype)继承 + 方法重写核心 ✅
参数多态(Parametric)泛型 / TypeVartyping.Generic ✅

此外还有两种重要的变体:

多态类型实现方式Python 支持情况
泛型函数多态singledispatch(按类型分派函数)functools.singledispatch ✅
结构化子类型Protocol(结构匹配而非继承)typing.Protocol ✅ (3.8+)

二、Python 中的多态实现

1. 动态类型和鸭子类型
  • 动态类型:Python 是动态类型语言,变量的类型在运行时确定。这使得多态性更自然。
  • 鸭子类型(Duck Typing):只要对象具有所需的方法或属性,就可以使用,无需关心对象的具体类型。

示例:

class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class Duck:
    def speak(self):
        return "Quack!"

def animal_speak(animal):
    return animal.speak()

animals = [Dog(), Cat(), Duck()]
for animal in animals:
    print(animal_speak(animal))

输出:

Woof!
Meow!
Quack!
  • 解释animal_speak 函数可以接受任何具有 speak 方法的对象,而不关心其具体类型。这就是鸭子类型的体现。
2. 继承和方法重写
  • 继承:子类继承父类的方法和属性。
  • 方法重写(Override):子类可以重写父类的方法,实现不同的行为。

示例:

class Animal:
    def speak(self):
        raise NotImplementedError("Subclasses must implement this method.")

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

def animal_speak(animal):
    return animal.speak()

animals = [Dog(), Cat()]
for animal in animals:
    print(animal_speak(animal))

输出:

Woof!
Meow!
  • 解释Animal 类定义了一个抽象方法 speak,子类 Dog 和 Cat 分别实现了自己的版本。animal_speak 函数调用时,根据传入对象的类型执行对应的方法。
3. 运算符重载
  • 运算符重载:在类中定义特殊方法,实现对内置运算符的重载。

示例:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # 重载加法运算符
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    # 重载字符串表示
    def __str__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(5, 7)
v3 = v1 + v2
print(v3)

输出:

Vector(7, 10)
  • 解释:通过定义 __add__ 方法,实现了 Vector 对象的加法运算。这是 Python 的另一种多态形式。

三、鸭子类型详解

1. 概念
  • 鸭子类型:如果一只鸟走起来像鸭子、游泳像鸭子、叫声像鸭子,那么这只鸟可以被称为鸭子。
  • 在 Python 中:只要对象具有所需的方法或属性,就可以将其视为某种类型。

示例:

class Bird:
    def fly(self):
        print("Bird is flying.")

class Airplane:
    def fly(self):
        print("Airplane is flying.")

class Fish:
    def swim(self):
        print("Fish is swimming.")

def lift_off(entity):
    entity.fly()

bird = Bird()
plane = Airplane()
fish = Fish()

lift_off(bird)   # 输出: Bird is flying.
lift_off(plane)  # 输出: Airplane is flying.
# lift_off(fish)  # AttributeError: 'Fish' object has no attribute 'fly'
  • 解释lift_off 函数可以接受任何具有 fly 方法的对象。Fish 对象由于没有 fly 方法,调用时会抛出 AttributeError

 2. 安全的鸭子类型:动态检查与容错

工程实践中,鸭子类型常配合动态检查使用:

def safe_fly(entity):
    """安全的鸭子类型调用:先检查再调用"""
    if hasattr(entity, 'fly'):
        entity.fly()
    elif hasattr(entity, 'swim'):
        print(f"{entity.__class__.__name__} can't fly, but can swim.")
    else:
        raise TypeError(f"{entity.__class__.__name__} is neither flyable nor swimmable")


# 或者用 getattr 提供默认值
def get_or_default(obj, attr, default=None):
    """获取属性,不存在时返回默认值而不是抛异常"""
    return getattr(obj, attr, default) or default


# EAFP 风格(Easier to Ask Forgiveness than Permission)
def eager_fly(entity):
    try:
        entity.fly()
    except AttributeError:
        print(f"{entity.__class__.__name__}: cannot fly")

三种鸭子类型安全策略对比

策略风格适用场景
hasattr() + ifLBYL (Look Before You Leap)有明确的 fallback 分支
getattr(obj, attr, default)LBYL需要提供默认值的场景
try/except AttributeErrorEAFP异常路径极少的情况(更快)

四、多态性的优点

1. 提高代码的灵活性
  • 可以编写与特定类型无关的代码,处理不同类型的对象。
2. 增强代码的可扩展性
  • 添加新类型的对象时,无需修改现有代码,只需确保新对象实现了所需的方法。
3. 代码重用
  • 通过多态,可以编写通用的函数或方法,避免重复代码。

五、抽象基类(Abstract Base Class)

  • 概念:抽象基类定义了接口规范,子类必须实现特定的方法。
  • 作用:确保子类实现必要的方法,提供一致的接口。

示例:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.1416 * self.radius ** 2

shapes = [Rectangle(3, 4), Circle(5)]
for shape in shapes:
    print(f"Area: {shape.area()}")

输出:

Area: 12
Area: 78.53999999999999
  • 解释Shape 是一个抽象基类,定义了 area 方法。Rectangle 和 Circle 实现了该方法。通过多态,可以统一处理不同形状的面积计算。

六、Python 中不支持方法重载

  • 说明:在 Python 中,方法重载(相同方法名,不同参数)并不被支持。后定义的方法会覆盖先前的方法。
  • 替代方案:使用默认参数或可变参数。

示例:

class MathOperations:
    def multiply(self, x, y, z=None):
        if z is not None:
            return x * y * z
        else:
            return x * y

math_ops = MathOperations()
print(math_ops.multiply(2, 3))       # 输出: 6
print(math_ops.multiply(2, 3, 4))    # 输出: 24
  • 解释:通过使用默认参数,实现类似方法重载的效果。

🆕 补充:typing.overload —— 静态类型层面的方法重载

虽然运行时不支持重载,但 Python 3.5+ 的 typing.overload 允许在静态类型检查时声明多个签名:

from typing import overload, Union

class Processor:
    @overload
    def process(self, data: str) -> str: ...
    @overload
    def process(self, data: list[str]) -> list[str]: ...
    @overload
    def process(self, data: None) -> None: ...

    def process(self, data):
        if isinstance(data, str):
            return data.upper()
        elif isinstance(data, list):
            return [item.upper() for item in data]
        elif data is None:
            return None
        raise TypeError("Unsupported type")

# mypy / pyright 能正确推断每种调用的返回类型
p = Processor()
result1: str = p.process("hello")        # 类型检查通过
result2: list[str] = p.process(["a"])    # 类型检查通过
result3: None = p.process(None)          # 类型检查通过

注意overload 只影响类型检查器的推断,运行时仍然只有一个 process 方法。它是给 IDE 和 mypy 用的"类型注解重载"。

七、方法解析顺序(MRO)在多态中的作用

  • MRO(Method Resolution Order):在多重继承中,Python 按照 MRO 决定调用哪个类的方法。
  • 多态与 MRO:当子类继承多个父类,且父类中有同名方法时,MRO 决定了方法的调用顺序。

示例:

class A:
    def do_something(self):
        print("Method from A")

class B:
    def do_something(self):
        print("Method from B")

class C(B, A):
    pass

c = C()
c.do_something()
print(C.mro())

输出:

Method from B
[<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
  • 解释C 继承了 B 和 A,由于 B 在前,调用同名方法时,B 的方法优先。

🆕 八、typing.Protocol —— 结构化子类型多态

Python 3.8+ 引入的 typing.Protocol 是对传统 ABC 多态的重要补充。它实现了结构化子类型 (Structural Subtyping):不需要显式继承,只要结构(方法/属性)匹配即可。

ABC vs Protocol 核心对比
# ===== 名义子类型 (Nominal) —— 传统 ABC =====
from abc import ABC, abstractmethod

class Flyer(ABC):
    @abstractmethod
    def fly(self) -> str: ...

class Bird(Flyer):       # 必须显式继承 Flyer
    def fly(self) -> str:
        return "Bird flies"

def let_it_fly(flyer: Flyer):  # 只接受 Flyer 及其子类
    return flyer.fly()

let_it_fly(Bird())       # ✅
let_it_fly(Airplane())   # ❌ Airplane 没有继承 Flyer


# ===== 结构化子类型 (Structural) —— Protocol =====
from typing import Protocol, runtime_checkable

@runtime_checkable
class Flyable(Protocol):  # Protocol 是接口约定,不是基类
    def fly(self) -> str: ...

class Bird:
    def fly(self) -> str:
        return "Bird flies"

class Airplane:          # 无需继承任何东西!
    def fly(self) -> str:
        return "Airplane flies"

def let_it_fly_cool(entity: Flyable):  # 接受任何有 fly() 方法的对象
    return entity.fly()

let_it_fly_cool(Bird())      # ✅ 结构匹配
let_it_fly_cool(Airplane())  # ✅ 结构匹配
完整对比表
维度ABC (名义子类型)Protocol (结构化子类型)
匹配依据继承关系(is-a)结构兼容(has-a-method)
是否需要继承✅ 必须❌ 不需要
运行时检查isinstance(x, AbcClass)需要 @runtime_checkable + isinstance
类型检查器原生支持mypy/pyright 支持
适用场景强契约、框架基类解耦、跨库接口、鸭子类型安全化
性能开销@runtime_checkable 有少量开销
Python 版本全部3.8+
Protocol 高级用法:复合协议
from typing import Protocol, TypeVar

T = TypeVar('T')

class Serializable(Protocol):
    def to_bytes(self) -> bytes: ...

class Hashable(Protocol):
    def hash(self) -> int: ...

# 复合协议:同时满足两个协议
class SerializableAndHashable(Serializable, Hashable, Protocol):
    pass

def cache_and_serialize(obj: SerializableAndHashable) -> bytes:
    """只接受同时可序列化和可哈希的对象"""
    key = obj.hash()
    print(f"Caching with key={key}")
    return obj.to_bytes()

AI 框架中的 Protocol 应用

# 定义模型接口协议 —— 不绑定任何基类
class HasForward(Protocol):
    config: object  # 需要有 config 属性

    def forward(self, x: torch.Tensor) -> torch.Tensor: ...

class HasLoss(Protocol):
    def compute_loss(self, pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor: ...

# 任何符合结构的模型都可以传入,无论来自哪个库
def train_step(model: HasForward & HasLoss, batch):
    pred = model.forward(batch['input'])
    loss = model.compute_loss(pred, batch['target'])
    loss.backward()
    return loss

🆕 九、functools.singledispatch —— 泛型函数多态

@singledispatch 实现了一种不同于继承的多态形式:根据第一个参数的运行时类型,将函数调用分派到不同的实现。

from functools import singledispatch

@singledispatch
def serialize(data):
    """默认实现:未知类型转字符串"""
    return str(data)


@serialize.register(int)
def _(data: int):
    return f"INT:{data}"


@serialize.register(float)
def _(data: float):
    return f"FLOAT:{data:.2f}"


@serialize.register(list)
def _(data: list):
    return f"LIST:[{','.join(serialize(x) for x in data)}]"


@serialize.register(dict)
def _(data: dict):
    pairs = [f"{k}={serialize(v)}" for k, v in data.items()]
    return f"DICT:{{{','.join(pairs)}}}"


# ===== 使用 =====
print(serialize(42))                  # INT:42
print(serialize(3.14))                # FLOAT:3.14
print(serialize([1, 2.5, "hello"]))   # LIST:[INT:1,FLOAT:2.5,hello]
print(serialize({"a": 1, "b": [2]}))  # DICT:{a=INT:1,b=LIST:[INT:2]}

与继承多态的关键区别

维度继承多态singledispatch 多态
分发依据对象自身的类型(self函数第一个参数的类型
扩展方式创建新子类@func.register(NewType)
耦合度对象需要知道自己的类对象完全不知道被分发
适用场景行为主要由对象自身决定同一数据有多种表示格式

AI/ML 中的典型应用

@singledispatch
def to_tensor(data):
    raise TypeError(f"Cannot convert {type(data).__name__} to tensor")

@to_tensor.register(torch.Tensor)
def _(data):
    return data  # 已经是 tensor

@to_tensor.register(np.ndarray)
def _(data):
    return torch.from_numpy(data)

@to_tensor.register(list)
def _(data):
    return torch.tensor(data)

@to_tensor.register(pd.DataFrame)
def _(data):
    return torch.tensor(data.values)

🆕 十、参数多态 (Parametric Polymorphism) —— TypeVar 与 Generic

参数多态是指函数或类在不指定具体类型的情况下工作,由调用者决定类型。

from typing import TypeVar, Generic, List

T = TypeVar('T')  # 类型变量


# ===== 泛型函数 =====
def first_item(items: List[T]) -> T:
    """适用于任何元素类型的列表"""
    return items[0]


int_list = [1, 2, 3]
str_list = ["a", "b", "c"]

a: int = first_item(int_list)    # 类型推断 T = int
b: str = first_item(str_list)    # 类型推断 T = str


# ===== 泛型类 =====
class Stack(Generic[T]):
    """类型安全的栈 —— 同一份代码适用于任何 T"""

    def __init__(self):
        self._items: List[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

    def peek(self) -> T:
        return self._items[-1]


int_stack = Stack[int]()
int_stack.push(1)
int_stack.push(2)
x: int = int_stack.pop()  # 类型检查器知道 pop() 返回 int

str_stack = Stack[str]()
str_stack.push("hello")
y: str = str_stack.pop()  # 类型检查器知道 pop() 返回 str

有界类型参数 (Bounded TypeVar)

from typing import TypeVar

class Shape:
    def area(self) -> float: ...

class Rectangle(Shape):
    def __init__(self, w, h): self.w, self.h = w, h
    def area(self) -> float: return self.w * self.h

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self) -> float: return 3.14 * self.r ** 2

S = TypeVar('S', bound=Shape)  # S 必须是 Shape 或其子类

def total_area(shapes: list[S]) -> float:
    """泛型函数 + 有界类型参数 = 类型安全的多态"""
    return sum(s.area() for s in shapes)

total_area([Rectangle(3, 4), Circle(5)])  # ✅ 返回 float
# total_area([1, 2, 3])                   # ❌ 类型错误:int 不满足 bound=Shape

🆕 十一、__call__ —— 可调用对象多态

Python 中一切皆对象,包括函数本身。__call__ 方法让任何对象都可以像函数一样被调用,这是极其强大的多态形式。

# ===== 不同形式的"可调用物" =====

def plain_function(x):
    return x * 2

class Multiplier:
    """可调用对象 —— 行为与函数一致"""

    def __init__(self, factor):
        self.factor = factor

    def __call__(self, x):
        return x * self.factor

class Exponential:
    """另一个可调用对象 —— 相同接口,不同行为"""

    def __call__(self, x):
        return x ** 2


# ===== 统一调用 =====
def apply_operation(value, operation):
    """operation 可以是函数、lambda、可调用对象 —— 多态"""
    return operation(value)


print(apply_operation(5, plain_function))   # 10  (函数)
print(apply_operation(5, Multiplier(3)))    # 15  (可调用对象)
print(apply_operation(5, Exponential()))    # 25  (可调用对象)
print(apply_operation(5, lambda x: x + 1))  # 6   (lambda)

AI/ML 框架中的 __call__ 多态

import torch
import torch.nn as nn

# PyTorch 中,模型、层、损失函数、激活函数全部是 callable
model = nn.Linear(10, 5)       # 层
activation = nn.ReLU()         # 激活函数
loss_fn = nn.MSELoss()         # 损失函数

x = torch.randn(4, 10)

# 统一的调用接口 —— 全部使用 __call__
x = model(x)          # 等价于 model.__call__(x)
x = activation(x)     # 等价于 activation.__call__(x)
# loss = loss_fn(pred, target)  # 等价于 loss_fn.__call__(pred, target)

# ===== 自定义 callable 模块 =====
class Preprocessor(nn.Module):
    """预处理 + 模型的组合 pipeline"""

    def __init__(self, vocab_size, embed_dim):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.dropout = nn.Dropout(0.1)

    def forward(self, input_ids):
        return self.dropout(self.embedding(input_ids))

    # 让它既可以用 forward() 也可以直接调用
    __call__ = forward


preprocessor = Preprocessor(vocab_size=30000, embed_dim=768)
tokens = torch.tensor([[1, 2, 3, 4]])
vectors = preprocessor(tokens)  # 通过 __call__ 调用

__call__ 多态的本质

  • 函数、bound method、lambda、functools.partial、__call__ 对象实现了统一的调用协议
  • callable(obj) 检查对象是否可调用
  • 这是 Python 中最高层次的多态之一

🆕 十二、协议多态 (Protocol-based Polymorphism) 概览

Python 中除了方法调用多态,还有多种隐式协议构成的多态:

# ===== 1. 迭代器协议 =====
class Counter:
    def __init__(self, limit):
        self.limit = limit
        self.count = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.count >= self.limit:
            raise StopIteration
        self.count += 1
        return self.count

# for 循环不关心对象类型,只要实现了 __iter__/__next__
for i in Counter(5):
    print(i, end=' ')  # 1 2 3 4 5


# ===== 2. 上下文管理器协议 =====
class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.time() - self.start
        print(f"\nElapsed: {elapsed:.3f}s")
        return False

# with 语句不关心对象类型,只要实现了 __enter__/__exit__
with Timer():
    sum(range(10_000_000))


# ===== 3. 容器协议 =====
class SparseVector:
    def __init__(self, data):
        self._data = {i: v for i, v in enumerate(data) if v != 0}

    def __len__(self):
        return max(self._data) + 1 if self._data else 0

    def __getitem__(self, index):
        return self._data.get(index, 0)

sv = SparseVector([0, 0, 5, 0, 8])
print(len(sv))      # 5  (__len__)
print(sv[2])        # 5   (__getitem__)
print(sv[4])        # 8
print(sv[0])        # 0   (默认值)

协议多态的意义:Python 的 for / with / [] / len() 等语法对所有实现了相应协议的对象都有效——这就是内置的多态基础设施。


🆕 十三、AI/ML 框架实战:多态在推理引擎中的应用

"""模拟一个多后端推理引擎 —— 展示多态在实际 AI 工程中的综合运用"""
from abc import ABC, abstractmethod
from typing import Protocol, Union
from functools import singledispatch
import numpy as np


# ===== 1. 抽象基类定义接口 =====
class InferenceBackend(ABC):
    """推理后端的抽象基类 —— 名义子类型多态"""

    @abstractmethod
    def load_model(self, model_path: str) -> None:
        ...

    @abstractmethod
    def infer(self, input_data: np.ndarray) -> np.ndarray:
        ...

    @abstractmethod
    def benchmark(self, iterations: int = 100) -> dict:
        ...


# ===== 2. 具体后端实现 =====
class ONNXBackend(InferenceBackend):
    """ONNX Runtime 后端"""

    def load_model(self, model_path: str):
        print(f"[ONNX] Loading model from {model_path}")
        self.model = f"onnx_model:{model_path}"

    def infer(self, input_data: np.ndarray):
        print(f"[ONNX] Running inference on shape {input_data.shape}")
        return input_data * 0.5 + 0.1  # 模拟推理

    def benchmark(self, iterations: int = 100) -> dict:
        return {"backend": "onnx", "avg_ms": 2.3, "iterations": iterations}


class TensorRTBackend(InferenceBackend):
    """TensorRT 后端"""

    def load_model(self, model_path: str):
        print(f"[TRT] Loading engine from {model_path}")
        self.engine = f"trt_engine:{model_path}"

    def infer(self, input_data: np.ndarray):
        print(f"[TRT] Running inference on shape {input_data.shape}")
        return input_data * 0.5 + 0.1

    def benchmark(self, iterations: int = 100) -> dict:
        return {"backend": "tensorrt", "avg_ms": 0.8, "iterations": iterations}


class VLLMBackend(InferenceBackend):
    """vLLM 后端 (LLM 推理)"""

    def load_model(self, model_path: str):
        print(f"[vLLM] Loading LLM from {model_path}")
        self.llm = f"vllm:{model_path}"

    def infer(self, input_data: np.ndarray):
        print(f"[vLLM] Generating tokens for batch {input_data.shape}")
        # 返回 token ids
        return np.random.randint(0, 32000, size=(input_data.shape[0], 20))

    def benchmark(self, iterations: int = 100) -> dict:
        return {"backend": "vllm", "avg_ms_p3": 45.2, "iterations": iterations}


# ===== 3. 推理引擎 —— 使用多态 =====
class InferenceEngine:
    """
    多态应用的综合展示:
    - 子类型多态:不同后端统一接口
    - Protocol 多态:输入数据可以是各种格式
    - singledispatch:不同输出格式的序列化
    """

    def __init__(self, backend: InferenceBackend):
        self.backend = backend

    def run(self, input_data: np.ndarray) -> np.ndarray:
        """统一入口 —— 不关心具体是哪种后端"""
        return self.backend.infer(input_data)

    def run_benchmark(self) -> dict:
        return self.backend.benchmark()

    def switch_backend(self, new_backend: InferenceBackend):
        """切换后端 —— 多态让切换零成本"""
        self.backend = new_backend


# ===== 4. singledispatch:输出格式多态 =====
@singledispatch
def format_result(result):
    return str(result)


@format_result.register(np.ndarray)
def _(result: np.ndarray):
    return f"ndarray(shape={result.shape}, dtype={result.dtype})"


@format_result.register(dict)
def _(result: dict):
    return " | ".join(f"{k}={v}" for k, v in result.items())


# ===== 5. 使用示例 =====
engine = InferenceEngine(ONNXBackend())
engine.backend.load_model("model.onnx")

data = np.random.randn(1, 3, 224, 224)
result = engine.run(data)
print(format_result(result))

print(format_result(engine.run_benchmark()))

# 切换到 TensorRT —— 接口不变,行为改变
engine.switch_backend(TensorRTBackend())
engine.backend.load_model("model.engine")
result = engine.run(data)
print(format_result(result))
print(format_result(engine.run_benchmark()))

这个案例综合运用了四种多态形式

多态形式在案例中的体现
子类型多态 (ABC)InferenceBackend + 三个具体后端
结构化子类型 (Protocol)输入数据可扩展为任何 HasArray 协议
泛型函数多态 (singledispatch)format_result 按类型选择序列化方式
协议多态InferenceEngine 对后端的无感知切换

第三部分:总结 —— 三节内容的关联与进阶路线

三者的关系图谱

┌─────────────────────────────────────────────────────┐
│                  面向对象编程 (OOP)                    │
│                                                       │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐         │
│  │  封装     │   │  继承     │   │  多态     │         │
│  │ Encaps.  │   │ Inherit. │   │ Poly.    │         │
│  ├──────────┤   ├──────────┤   ├──────────┤         │
│  │ public   │   │ 单继承    │   │ 子类型    │         │
│  │ private  │   │ 多重继承  │   │ 鸭子类型  │         │
│  │ protected│   │ 多层继承  │   │ 运算符重载│         │
│  │ property │   │ Mixin    │   │ Protocol │         │
│  │ 描述符   │   │ super()  │   │ singledisp│         │
│  │ __slots_│   │ __init_  │   │ 参数多态  │         │
│  │ 模块封装  │   │ subclass │   │ __call__ │         │
│  │ dataclass│   │ 组合优于  │   │ 协议多态  │         │
│  └────┬─────┘   └────┬─────┘   └────┬─────┘         │
│       │              │              │                │
│       └──────────────┼──────────────┘                │
│                      ▼                               │
│              ┌──────────────┐                        │
│              │  设计模式     │                        │
│              │ 工厂/策略/模板 │                        │
│              │ 观察者/命令   │                        │
│              └──────────────┘                        │
└─────────────────────────────────────────────────────┘

面试回答策略建议

问题回答要点对应本节内容
“什么是封装?”命名约定 + property + 描述符 + __slots__ + 模块级封装
“Python 的私有是真正的私有吗?”name mangling + “consenting adults” 哲学
“MRO 是什么?”C3 线性化 + 菱形继承 + super() 本质
“什么时候用 Mixin?”单一功能 + 不单独实例化 + 命名规范
“组合 vs 继承?”is-a 用继承,has-a 用组合 + 脆弱基类问题
“Python 有几种多态?”四种:子类型/鸭子/参数/泛型函数
“ABC vs Protocol?”名义 vs 结构 + 表格对比
“如何在 AI 项目中运用 OOP?”PyTorch/HF 继承体系 + 推理引擎多态

文档结束。本文档共包含:

  • 第17节:11 个子节(原文 5 个 + 🆕 6 个新增)
  • 第18节:14 个子节(原文 7 个 + 🆕 7 个新增)
  • 第19节:13 个子节(原文 7 个 + 🆕 6 个新增)
  • 新增 AI/ML 实战案例 4 个(PyTorch 封装、HF 继承体系、推理引擎多态、Mixin 梯度 checkpoint)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值