跳到主要内容
预计阅读 37 分钟

08|面向对象设计:用类与对象构建可扩展系统

场景引入:设备监控平台的架构挑战

你正在开发一个 IoT 设备监控平台。系统需要管理数百台不同类型的设备——温度传感器、摄像头、网关——每种设备有各自的属性(型号、位置、状态)和行为(上报数据、重启、升级固件)。如果继续用函数和字典来组织代码,数据结构与操作逻辑分散在各处,新增设备类型时需要到处修改,维护成本急剧上升。

**面向对象编程(OOP)**提供了更好的组织方式:将每种设备的属性和行为封装为一个”类”,通过继承来复用共性逻辑,通过多态让不同设备对同一指令做出各自的响应。


从过程式到面向对象

过程式写法的局限

sensor_a = {'device_id': 'S001', 'reading': 23.5}
sensor_b = {'device_id': 'S002', 'reading': 19.8}

def report_reading(device):
    print(f"设备 {device['device_id']}:读数 {device['reading']}")

report_reading(sensor_a)
report_reading(sensor_b)

数据(字典)和操作(函数)是分离的。随着功能增加,很难追踪哪些函数应该操作哪些数据。

面向对象的写法

class Sensor:
    def __init__(self, device_id, reading):
        self.device_id = device_id
        self.reading = reading

    def report(self):
        print(f'设备 {self.device_id}:读数 {self.reading}')

unit_a = Sensor('S001', 23.5)
unit_b = Sensor('S002', 19.8)

unit_a.report()    # 设备 S001:读数 23.5
unit_b.report()    # 设备 S002:读数 19.8

数据和操作被绑定在一起——每个传感器对象自身就知道如何上报数据。


类与对象的核心概念

  • 类(Class):定义一类事物共有的属性和行为的模板
  • 对象(Object / Instance):根据类创建的具体实体

可以类比为:类是”设备规格说明书”,对象是按照说明书制造出的一台具体设备。

定义类

class Gateway:
    """网关设备类"""

    def __init__(self, gw_id, location):
        """初始化方法:创建对象时自动调用"""
        self.gw_id = gw_id           # 实例属性
        self.location = location     # 实例属性
        self.connected = False

    def connect(self):
        """连接网关"""
        self.connected = True
        print(f'网关 {self.gw_id} 已连接({self.location})')

    def status(self):
        state = '在线' if self.connected else '离线'
        print(f'网关 {self.gw_id}{state}')

创建对象(实例化)

gw1 = Gateway('GW-101', '3号楼机房')
gw2 = Gateway('GW-102', '园区入口')

gw1.connect()     # 网关 GW-101 已连接(3号楼机房)
gw2.status()      # 网关 GW-102:离线

print(gw1.gw_id)       # GW-101
print(gw2.location)    # 园区入口

深入 __init__self

__init__ 方法

__init__ 是初始化方法(构造器),在对象创建时自动调用,用于设置对象的初始状态。

class Wallet:
    def __init__(self, owner_name, initial_balance=0):
        self.owner = owner_name
        self.balance = initial_balance
        self.history = []

    def deposit(self, amount):
        self.balance += amount
        self.history.append(f'+{amount}')
        print(f'充值成功,余额:{self.balance}')

    def pay(self, amount):
        if amount > self.balance:
            print('余额不足')
            return
        self.balance -= amount
        self.history.append(f'-{amount}')
        print(f'支付成功,余额:{self.balance}')

wallet = Wallet('运营部', 500)
wallet.deposit(200)     # 充值成功,余额:700
wallet.pay(150)         # 支付成功,余额:550

self 的本质

self 代表调用方法的那个对象本身。调用 gw1.connect() 时,Python 自动把 gw1 作为第一个参数传入——这就是 self

class Node:
    def __init__(self, label):
        self.label = label

    def identify(self):
        print(f'我是节点 {self.label}')

primary = Node('primary')
replica = Node('replica')

primary.identify()     # 我是节点 primary  -> self 是 primary
replica.identify()     # 我是节点 replica  -> self 是 replica

# 本质等价于:
Node.identify(primary)  # 我是节点 primary

self 不是关键字,只是约定命名。但请始终使用 self,这是 Python 社区的共识。


实例属性与类属性

实例属性:每个对象独有

class TaskRunner:
    def __init__(self, runner_id):
        self.runner_id = runner_id
        self.tasks = []          # 每个 runner 有独立的任务列表

r1 = TaskRunner('R1')
r2 = TaskRunner('R2')
r1.tasks.append('build')
print(r1.tasks)   # ['build']
print(r2.tasks)   # [](互不影响)

类属性:所有对象共享

class TaskRunner:
    max_concurrent = 4       # 类属性:所有 runner 共享
    instance_count = 0       # 类属性:追踪创建数量

    def __init__(self, runner_id):
        self.runner_id = runner_id
        TaskRunner.instance_count += 1

r1 = TaskRunner('R1')
r2 = TaskRunner('R2')
r3 = TaskRunner('R3')

print(TaskRunner.instance_count)    # 3
print(r1.max_concurrent)           # 4
print(TaskRunner.max_concurrent)   # 4

如果用可变对象(列表)作为类属性,修改时需特别小心——所有实例共享同一个对象。

类方法与静态方法

class Converter:
    rate_usd_to_cny = 7.24

    @classmethod
    def usd_to_cny(cls, usd_amount):
        """类方法:第一个参数是类本身(cls)"""
        return usd_amount * cls.rate_usd_to_cny

    @staticmethod
    def celsius_to_fahrenheit(c):
        """静态方法:不需要 self 或 cls"""
        return c * 9 / 5 + 32

print(Converter.usd_to_cny(100))              # 724.0
print(Converter.celsius_to_fahrenheit(37))     # 98.6

继承与多态

继承:基于已有类扩展

# 父类
class Device:
    def __init__(self, device_id):
        self.device_id = device_id

    def ping(self):
        print(f'{self.device_id} 响应正常')

    def describe(self):
        print(f'设备 {self.device_id}')

# 子类——继承自 Device
class TemperatureSensor(Device):
    def __init__(self, device_id, precision):
        super().__init__(device_id)       # 调用父类构造器
        self.precision = precision        # 子类特有属性

    def ping(self):
        print(f'传感器 {self.device_id} 响应正常,精度 {self.precision}')

    def calibrate(self):
        print(f'{self.device_id} 校准完成')

class Camera(Device):
    def ping(self):
        print(f'摄像头 {self.device_id} 响应正常,画面清晰')

# 使用
sensor = TemperatureSensor('TS-001', '0.1C')
cam = Camera('CAM-003')

sensor.ping()         # 传感器 TS-001 响应正常,精度 0.1C
cam.ping()            # 摄像头 CAM-003 响应正常,画面清晰
sensor.describe()     # 设备 TS-001(继承自父类)
sensor.calibrate()    # TS-001 校准完成(子类特有方法)

多态:同一接口,不同行为

def health_check(devices):
    """对一批设备执行健康检查"""
    for dev in devices:
        dev.ping()     # 每种设备的 ping() 行为不同

fleet = [
    TemperatureSensor('TS-001', '0.1C'),
    Camera('CAM-003'),
    TemperatureSensor('TS-007', '0.5C'),
]
health_check(fleet)

health_check 不需要知道传入的具体设备类型——只要对象拥有 ping() 方法即可。这就是多态的威力:针对抽象接口编程,不要依赖具体实现细节

isinstance() 类型检查

sensor = TemperatureSensor('TS-001', '0.1C')

print(isinstance(sensor, TemperatureSensor))  # True
print(isinstance(sensor, Device))             # True(子类也是父类的实例)
print(isinstance(sensor, Camera))             # False

print(issubclass(TemperatureSensor, Device))  # True

封装与 @property

封装的核心思想:隐藏内部实现,只暴露必要的接口

直接暴露属性的风险

class Thermostat:
    def __init__(self, temp):
        self.temp = temp

t = Thermostat(22)
t.temp = 9999     # 无限制,可以设置任何值
t.temp = -500     # 不合理的值也能通过

@property:优雅的访问控制

class Thermostat:
    def __init__(self, temp):
        self._temp = temp    # 以 _ 开头表示内部使用

    @property
    def temp(self):
        """getter:读取温度"""
        return self._temp

    @temp.setter
    def temp(self, value):
        """setter:设置温度(带校验)"""
        if not isinstance(value, (int, float)):
            raise TypeError('温度必须是数字')
        if value < -40 or value > 80:
            raise ValueError('温度必须在 -40 到 80 之间')
        self._temp = value

    @property
    def status(self):
        """只读属性:根据温度返回状态"""
        if self._temp > 35:
            return 'HIGH'
        elif self._temp < 10:
            return 'LOW'
        return 'NORMAL'

t = Thermostat(22)
print(t.temp)        # 22
t.temp = 30          # 通过 setter 校验
print(t.status)      # NORMAL

# t.temp = 9999      # ValueError
# t.status = 'HIGH'  # AttributeError(只读)

使用 @property 后,外部代码像访问普通属性一样操作,但内部自动进行校验。


魔法方法:让自定义类融入 Python 生态

Python 中以双下划线开头和结尾的方法称为魔法方法(也叫 dunder 方法),它们让自定义类支持内置运算和函数。

__str____repr__

class Coordinate:
    def __init__(self, lat, lng):
        self.lat = lat
        self.lng = lng

    def __str__(self):
        """print() 调用,面向终端用户"""
        return f'({self.lat}, {self.lng})'

    def __repr__(self):
        """交互模式显示,面向开发者"""
        return f'Coordinate(lat={self.lat}, lng={self.lng})'

pt = Coordinate(31.23, 121.47)
print(pt)     # (31.23, 121.47)

__len__

class Playlist:
    def __init__(self, title):
        self.title = title
        self.tracks = []

    def add_track(self, track):
        self.tracks.append(track)

    def __len__(self):
        return len(self.tracks)

pl = Playlist('工作专注')
pl.add_track('ambient_01')
pl.add_track('ambient_02')
pl.add_track('ambient_03')

print(len(pl))   # 3

__eq__

class Version:
    def __init__(self, major, minor, patch):
        self.major = major
        self.minor = minor
        self.patch = patch

    def __eq__(self, other):
        if not isinstance(other, Version):
            return False
        return (self.major == other.major and
                self.minor == other.minor and
                self.patch == other.patch)

    def __str__(self):
        return f'{self.major}.{self.minor}.{self.patch}'

v1 = Version(2, 1, 0)
v2 = Version(2, 1, 0)
v3 = Version(3, 0, 0)

print(v1 == v2)   # True
print(v1 == v3)   # False

运算符重载

class Vec2D:
    def __init__(self, dx, dy):
        self.dx = dx
        self.dy = dy

    def __add__(self, other):
        return Vec2D(self.dx + other.dx, self.dy + other.dy)

    def __mul__(self, factor):
        return Vec2D(self.dx * factor, self.dy * factor)

    def __abs__(self):
        return (self.dx ** 2 + self.dy ** 2) ** 0.5

    def __bool__(self):
        return self.dx != 0 or self.dy != 0

    def __repr__(self):
        return f'Vec2D({self.dx}, {self.dy})'

a = Vec2D(3, 4)
b = Vec2D(1, 2)

print(a + b)           # Vec2D(4, 6)
print(a * 3)           # Vec2D(9, 12)
print(abs(a))          # 5.0
print(bool(Vec2D(0, 0)))  # False

dataclass:简化数据类定义

Python 3.7 引入的 @dataclass 装饰器能自动生成 __init____repr____eq__ 等样板代码。

传统写法 vs dataclass

# 传统写法
class EndpointOld:
    def __init__(self, host, port, protocol):
        self.host = host
        self.port = port
        self.protocol = protocol

    def __repr__(self):
        return f'Endpoint({self.host}:{self.port}, {self.protocol})'

    def __eq__(self, other):
        return (self.host == other.host and
                self.port == other.port and
                self.protocol == other.protocol)
# dataclass 写法
from dataclasses import dataclass

@dataclass
class Endpoint:
    host: str
    port: int
    protocol: str = 'https'    # 默认值

ep1 = Endpoint('10.0.1.5', 8080)
ep2 = Endpoint('10.0.1.5', 8080)

print(ep1)            # Endpoint(host='10.0.1.5', port=8080, protocol='https')
print(ep1 == ep2)     # True

进阶用法

from dataclasses import dataclass, field

@dataclass
class ServiceConfig:
    name: str = 'default'
    port: int = 8000
    debug: bool = False
    allowed_origins: list = field(default_factory=list)

cfg = ServiceConfig()
print(cfg)

prod_cfg = ServiceConfig(name='production', port=443)
print(prod_cfg)
# 不可变 dataclass
@dataclass(frozen=True)
class Coordinate:
    lat: float
    lng: float

pt = Coordinate(31.23, 121.47)
# pt.lat = 0   # FrozenInstanceError

# frozen=True 的 dataclass 可以作为字典键和集合元素
locations = {Coordinate(0, 0), Coordinate(31.23, 121.47), Coordinate(0, 0)}
print(len(locations))   # 2(自动去重)
# 添加自定义方法
@dataclass
class Circle:
    radius: float

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

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

ring = Circle(10)
print(f'面积:{ring.area:.2f}')           # 面积:314.16
print(f'周长:{ring.circumference:.2f}')  # 周长:62.83

综合实战:设备资产管理系统

from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class Asset:
    asset_id: str
    asset_type: str
    location: str
    is_available: bool = True

    def __str__(self):
        status = '可用' if self.is_available else '维护中'
        return f'[{self.asset_id}] {self.asset_type} @ {self.location} ({status})'

class AssetRegistry:
    def __init__(self, registry_name):
        self.registry_name = registry_name
        self._assets = {}

    def register(self, asset):
        self._assets[asset.asset_id] = asset
        print(f'已注册:{asset}')

    def checkout(self, asset_id):
        asset = self._assets.get(asset_id)
        if asset is None:
            raise ValueError(f'资产不存在:{asset_id}')
        if not asset.is_available:
            raise ValueError(f'{asset.asset_type} 当前不可用')
        asset.is_available = False
        print(f'已借出:{asset.asset_type}')

    def checkin(self, asset_id):
        asset = self._assets.get(asset_id)
        if asset is None:
            raise ValueError(f'资产不存在:{asset_id}')
        asset.is_available = True
        print(f'已归还:{asset.asset_type}')

    def find(self, keyword):
        matches = [a for a in self._assets.values()
                   if keyword in a.asset_type or keyword in a.location]
        return matches

    def __len__(self):
        return len(self._assets)

    def __str__(self):
        return f'{self.registry_name}(共 {len(self)} 件资产)'

# 使用
registry = AssetRegistry('研发部资产库')
registry.register(Asset('A001', '笔记本电脑', '3F-工位区'))
registry.register(Asset('A002', '投影仪', '会议室B'))
registry.register(Asset('A003', '示波器', '实验室'))

print(registry)

registry.checkout('A001')

results = registry.find('实验室')
for item in results:
    print(item)

本章回顾

概念说明
类 (class)对象的模板,定义属性和行为
对象 (object)类的具体实例
__init__构造器,创建对象时自动调用
self指向当前对象的引用
实例属性self.xxx,每个对象独有
类属性直接定义在类中,所有对象共享
继承子类自动继承父类定义的全部属性与行为
多态不同对象对同一方法有不同行为
@property将方法包装为属性,实现访问控制
魔法方法__str__ __len__ __eq__
@dataclass自动生成样板代码的数据类装饰器
组合将对象作为属性嵌入另一个对象,替代过度继承

面向对象的三大支柱:

  1. 封装:数据与方法捆绑,隐藏内部实现
  2. 继承:基于已有类扩展新类,实现代码复用
  3. 多态:统一接口,差异化实现

补充:组合优于继承

继承虽然强大,但过度使用会导致类层次结构过于复杂。在很多场景中,组合(将一个对象作为另一个对象的属性)是更灵活的选择:

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        print(f'{self.horsepower} 马力引擎启动')

class GPS:
    def __init__(self, model):
        self.model = model

    def locate(self):
        print(f'{self.model} 定位中...')

class Vehicle:
    """通过组合而非继承来构建功能"""
    def __init__(self, name, engine, gps=None):
        self.name = name
        self.engine = engine     # 组合
        self.gps = gps           # 组合

    def launch(self):
        self.engine.start()
        if self.gps:
            self.gps.locate()
        print(f'{self.name} 出发')

truck = Vehicle('运输车', Engine(300), GPS('北斗'))
truck.launch()
# 300 马力引擎启动
# 北斗 定位中...
# 运输车 出发

组合让你可以自由地拼装功能模块,而不受继承链的约束。

补充:何时使用面向对象

面向对象并非万能方案。以下是一些决策参考:

适合使用类的场景:

  • 需要管理一组相关的状态(属性)和行为(方法)
  • 存在明显的”是一种”(is-a)关系,适合用继承建模
  • 需要创建同类对象的多个实例
  • 代码需要长期维护和扩展

可能不需要类的场景:

  • 只需要几个纯函数,没有共享状态
  • 一次性脚本或简短的数据处理流水线
  • 过度封装反而增加了复杂性

Python 的一大优势在于它同时支持面向过程和面向对象两种范式,你可以根据具体场景选择最合适的方式,甚至在同一个项目中混合使用两种风格。

面向对象不仅是一种编程技术,更是一种组织思维。如果某些数据与操作这些数据的函数经常一起使用,说明是时候用类来组织它们了。随着项目规模增长,你会越来越深刻地体会到这种组织方式带来的可维护性和可扩展性。

购买课程解锁全部内容

零基础到独立开发:Python 自动化与 Web 实战

¥29.90