018、Python面向对象编程(OOP)初探:类与对象
018、Python面向对象编程(OOP)初探:类与对象
上周帮同事调试一段数据处理代码,问题出得挺典型。他写了几十个相似函数来处理不同传感器数据,每个函数里都重复写着校验范围、转换单位的逻辑。改一个校验规则,得把所有函数全翻一遍。我指着屏幕说:“这活儿该交给类和对象来干。”他愣了一下:“我函数用得好好的,为啥要学这个?”
其实很多从脚本入门的开发者都有这个阶段——面向过程写惯了,觉得OOP(面向对象编程)是“高级玩意儿”用不上。直到某天发现代码改不动了,加个小功能像在豆腐渣工程上盖楼。
一、从现实问题到代码抽象
先看个真实场景。假设我们在开发嵌入式设备日志系统,需要记录两种日志:事件日志(带时间戳和事件类型)和错误日志(额外带错误码和严重等级)。
不用OOP的写法可能是这样:
def create_event_log(content, event_type):
import time
log = {
'timestamp': time.time(),
'content': content,
'type': event_type
}
return log
def create_error_log(content, error_code, severity):
import time
log = {
'timestamp': time.time(),
'content': content,
'error_code': error_code,
'severity': severity
}
return log
# 使用时得记住每个函数返回的字典结构
event = create_event_log("系统启动", "system")
error = create_error_log("内存不足", 0x12, "high")
# 打印日志得写两套逻辑
if 'error_code' in event:
print(f"[错误] {event['content']}")
else:
print(f"[事件] {event['content']}")
问题很明显:数据结构松散,判断逻辑重复,加个新日志类型又得写新函数。更麻烦的是,如果哪天要统一给所有日志加个“设备ID”字段,你得改所有创建函数。
二、类:把数据和操作打包
类(Class)就是个自定义的数据类型模板。它把数据(属性)和操作这些数据的函数(方法)打包在一起。
class BaseLog:
# __init__ 是初始化方法,相当于构造函数
# self 必须写,它代表实例对象本身
def __init__(self, content):
import time
self.timestamp = time.time() # 属性:时间戳
self.content = content # 属性:日志内容
def display(self):
# 方法:显示日志
print(f"[{self.timestamp}] {self.content}")
# 创建实例(对象)
log1 = BaseLog("系统启动")
log1.display() # 输出:[1730000000.123] 系统启动
这里有个新手常踩的坑:忘记写self。类里的每个方法第一个参数必须是self,Python自动把对象实例传给它。你调用log1.display()时,Python实际执行的是BaseLog.display(log1)。
三、继承:避免重复造轮子
现在实现事件日志和错误日志的差异。用继承(Inheritance)可以复用基类代码:
class EventLog(BaseLog):
def __init__(self, content, event_type):
# 先调用父类的初始化
super().__init__(content)
self.event_type = event_type # 新增属性
def display(self):
# 重写(Override)父类方法
print(f"[事件/{self.event_type}] {self.content}")
class ErrorLog(BaseLog):
def __init__(self, content, error_code, severity):
super().__init__(content)
self.error_code = error_code
self.severity = severity
def display(self):
# 十六进制显示错误码
print(f"[错误/0x{self.error_code:02X}] {self.content} (等级:{self.severity})")
# 可以添加子类特有方法
def need_alert(self):
return self.severity in ["high", "critical"]
关键点:
super().__init__(content)调用父类初始化,这句经常被新手漏掉- 子类可以添加新属性(
event_type)、重写方法(display)、添加新方法(need_alert) - 重写时完全覆盖父类实现,如果想扩展而非替换,可以在子类方法里也调用
super().xxx()
四、多态:统一接口,不同实现
OOP最实用的特性之一。看这段代码:
logs = [
EventLog("用户登录", "auth"),
ErrorLog("内存泄漏", 0x12, "medium"),
EventLog("数据备份完成", "system")
]
for log in logs:
log.display() # 自动调用各自类的display方法
输出:
[事件/auth] 用户登录
[错误/0x12] 内存泄漏 (等级:medium)
[事件/system] 数据备份完成
这就是多态(Polymorphism):log.display()这行代码,在循环里对不同对象执行不同操作。我们不需要写if isinstance(log, EventLog): ...这样的判断语句。
想象一下,如果以后加个WarningLog类,只要它也有display()方法,上面的循环代码一行都不用改。这种可扩展性在大型项目中能救命。
五、封装:隐藏内部细节
好的类设计应该“高内聚、低耦合”。封装(Encapsulation)就是把内部细节藏起来,只暴露必要的接口。
class SensorData:
def __init__(self, raw_value):
self._raw = raw_value # 下划线开头表示“内部属性”
self._calibrate()
def _calibrate(self):
# 下划线开头表示“内部方法”
# 校准算法是内部实现细节,外部不需要知道
self._calibrated = self._raw * 0.95 + 2.0
def get_value(self, unit='celsius'):
# 对外提供统一接口
if unit == 'celsius':
return self._calibrated
elif unit == 'fahrenheit':
return self._calibrated * 1.8 + 32
else:
raise ValueError("不支持的单位")
# 使用
sensor = SensorData(25.0)
print(sensor.get_value()) # 输出校准后的摄氏温度
print(sensor.get_value('fahrenheit')) # 转华氏温度
# print(sensor._raw) # 别这样写!虽然Python不禁止,但破坏了封装约定
Python没有真正的私有属性(不像Java的private),靠单下划线_和双下划线__的命名约定。双下划线会让Python做名称改写(name mangling),用在需要避免子类属性冲突的场景,但别滥用。
六、类变量与实例变量
这个坑我见过不少人掉进去:
class Device:
firmware_version = "V1.0" # 类变量,所有实例共享
def __init__(self, device_id):
self.device_id = device_id # 实例变量,每个实例独立
# 测试
d1 = Device("SN001")
d2 = Device("SN002")
print(d1.firmware_version) # V1.0
print(d2.firmware_version) # V1.0
# 修改类变量
Device.firmware_version = "V1.1"
print(d1.firmware_version) # V1.1(跟着变了!)
print(d2.firmware_version) # V1.1
# 危险操作:通过实例修改类变量
d1.firmware_version = "V1.2" # 这其实创建了实例变量!
print(d1.firmware_version) # V1.2
print(d2.firmware_version) # V1.1(没变)
print(Device.firmware_version) # V1.1(类变量也没变)
记住规则:
- 类变量用于所有实例共享的数据(如配置、常量)
- 实例变量用于每个对象特有的数据
- 通过实例修改类变量会创建同名实例变量,通常这是bug不是特性
七、特殊方法:让类更“Pythonic”
Python有很多双下划线开头结尾的特殊方法(魔术方法),用它们可以让你的类用起来像内置类型一样自然。
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
# 定义print()时的输出
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
# 支持 + 运算符
return Vector(self.x + other.x, self.y + other.y)
def __len__(self):
# 定义len()行为,这里返回维度
return 2
def __getitem__(self, index):
# 支持下标访问 v[0], v[1]
if index == 0:
return self.x
elif index == 1:
return self.y
else:
raise IndexError("Vector只有两个分量")
# 使用
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1) # Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(len(v1)) # 2
print(v1[0]) # 1
这些特殊方法让代码更直观。在嵌入式开发中,我常用__repr__生成调试信息,用__eq__比较两个配置对象是否相同。
八、静态方法与类方法
这两个装饰器容易混淆:
class DataParser:
# 类变量
default_encoding = "utf-8"
def __init__(self, data):
self.data = data
@staticmethod
def is_valid_hex_string(s):
# 静态方法:和类相关但不需要访问实例或类变量
# 可以看作放在类里的普通函数
import re
return bool(re.match(r'^[0-9A-Fa-f]+$', s))
@classmethod
def from_file(cls, filename):
# 类方法:第一个参数是类本身(cls)
# 常用作替代构造函数
with open(filename, 'r', encoding=cls.default_encoding) as f:
data = f.read()
return cls(data) # 创建新实例
@classmethod
def set_default_encoding(cls, encoding):
# 修改类变量
cls.default_encoding = encoding
# 使用
print(DataParser.is_valid_hex_string("FF00")) # True,不用创建实例
parser1 = DataParser("some data")
parser2 = DataParser.from_file("log.txt") # 工厂方法创建实例
DataParser.set_default_encoding("gbk") # 影响所有实例
简单区分:
- 静态方法:工具函数,放类里是为了代码组织
- 类方法:需要访问或修改类状态,或作为替代构造函数
个人经验建议
-
不要为了OOP而OOP。写个简单脚本处理一次性数据,用函数就行。但当代码超过300行,或需要维护超过一个月时,就该考虑OOP了。
-
继承别超过三层。我见过Java风格的深度继承树,改底层类得测试所有子类。多用组合(has-a),少用继承(is-a)。比如日志系统里,让
ErrorLog包含一个Severity对象,而不是继承它。 -
属性访问要谨慎。Python的
@property装饰器很好用,但别把所有属性都写成property。只有需要在获取/设置时执行额外逻辑(如校验、计算)的才用。 -
嵌入式场景的特殊考虑。在资源受限的设备上,每个对象都有内存开销。如果创建大量微小对象(比如每秒生成1000个日志对象),考虑对象池或结构体数组。
-
调试时多看
__dict__。遇到奇怪的对象状态问题,print(obj.__dict__)能看到所有实例变量,比猜强。 -
命名约定要遵守。单下划线
_开头的属性表示“内部使用”,双下划线__开头的会触发名称改写。这不是强制规定,但团队协作时这些约定能减少很多误会。
面向对象不是银弹,但确实是管理复杂代码的利器。刚开始写类可能觉得繁琐,等你在凌晨三点调试代码,发现只需要改一个类就能修复十个相似bug时,你会感谢当初花时间学了OOP。好的类设计像乐高积木,坏的设计像一团乱麻——区别往往就在最初的那点规划。
更多推荐


所有评论(0)