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 | 自动生成样板代码的数据类装饰器 |
| 组合 | 将对象作为属性嵌入另一个对象,替代过度继承 |
面向对象的三大支柱:
- 封装:数据与方法捆绑,隐藏内部实现
- 继承:基于已有类扩展新类,实现代码复用
- 多态:统一接口,差异化实现
补充:组合优于继承
继承虽然强大,但过度使用会导致类层次结构过于复杂。在很多场景中,组合(将一个对象作为另一个对象的属性)是更灵活的选择:
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