在做 Python开发 的上位机开发或工具类项目时,很多人一开始就被“对象、属性、方法”绕晕:属性到底放哪?方法该不该是静态的?为什么一个类写着写着就难以维护?本文聚焦“Python 面向对象—属性与方法”的核心实践,用通俗语言和可复制的代码示例,带你搭建既清晰又好扩展的类设计。你将学会:如何区分类属性/实例属性、实例方法/类方法/静态方法、何时使用属性描述符与@property,以及在 Windows 下做设备管理、配置管理等上位机开发的落地写法。
Pythonclass SerialDevice:
port = "COM1" # 错误:应为每个设备实例独有
baudrate = 115200 # 错误:不同设备可能不同
connected = False # 错误:状态不应共享
def __init__(self):
pass
def connect(self):
# 假装连接
self.connected = True
def read_value(self):
# 读取数据(伪代码)
return 42
问题:connected 是类属性,多个实例互相影响;port、baudrate 也不应共享。这样的“共享状态”在上位机开发中非常危险。
下面给出一个可直接用于实际项目的设备通信类雏形,演示属性与方法的正确组织方式。为方便演示,串口通信用伪代码代替,你可以在 Windows 环境用 pyserial 替换标注位置。
Pythonfrom __future__ import annotations
from dataclasses import dataclass
from typing import Optional, ClassVar, Iterable
import serial
from serial.tools import list_ports
class PortNotFoundError(Exception):
pass
@dataclass(frozen=True)
class DeviceInfo:
"""不可变的数据类,描述设备识别信息(只读配置)。"""
vendor_id: int
product_id: int
model: str
class SerialDevice:
"""上位机设备抽象:清晰区分类属性、实例属性、方法类型。"""
# 类属性(默认配置、常量)
SUPPORTED_BAUDRATES: ClassVar[set[int]] = {9600, 19200, 38400, 57600, 115200}
DEFAULT_BAUDRATE: ClassVar[int] = 115200
READ_TIMEOUT_S: ClassVar[float] = 1.0
def __init__(self, port: str, baudrate: Optional[int] = None, info: Optional[DeviceInfo] = None):
# 实例属性(对象状态和参数)
self._port = port
self._baudrate = baudrate or self.DEFAULT_BAUDRATE
self._connected = False
self._info = info or DeviceInfo(vendor_id=0xFFFF, product_id=0x0001, model="Generic")
# 延迟初始化资源(如串口对象)
self._sp = None # type: ignore # 串口句柄占位
# 校验:尽早失败,保证有效状态
if self._baudrate not in self.SUPPORTED_BAUDRATES:
raise ValueError(f"Unsupported baudrate: {self._baudrate}")
# -------------------------
# 属性与校验:只读/可写视图
# -------------------------
@property
def port(self) -> str:
"""所连接的串口名,初始化后一般不变。"""
return self._port
@property
def baudrate(self) -> int:
return self._baudrate
@baudrate.setter
def baudrate(self, value: int) -> None:
if self._connected:
raise RuntimeError("Cannot change baudrate while connected")
if value not in self.SUPPORTED_BAUDRATES:
raise ValueError(f"Unsupported baudrate: {value}")
self._baudrate = value
@property
def info(self) -> DeviceInfo:
"""设备只读信息对象。"""
return self._info
@property
def connected(self) -> bool:
"""只读状态,由 connect()/disconnect() 管理。"""
return self._connected
# -------------------------
# 实例方法:管理生命周期与 I/O
# -------------------------
def connect(self) -> None:
if self._connected:
return
self._sp = serial.Serial(self._port, self._baudrate, timeout=self.READ_TIMEOUT_S)
self._connected = self._sp.is_open
def disconnect(self) -> None:
if not self._connected:
return
# if self._sp and self._sp.is_open:
# self._sp.close()
self._sp = None
self._connected = False
def send_frame(self, payload: bytes) -> None:
"""发送一帧数据,自动封包(示例使用简化协议)。"""
self._ensure_connected()
frame = self._build_frame(payload)
self._sp.write(frame)
def read_frame(self) -> Optional[bytes]:
"""读取一帧数据,含校验和验证(伪实现)。"""
self._ensure_connected()
# raw = self._sp.read_until(expected=b'\n', size=64)
# 演示用假数据:
raw = self._build_frame(b"OK")
# 解析与校验
payload = self._parse_frame(raw)
return payload
# -------------------------
# 静态/类方法:工具与工厂
# -------------------------
@staticmethod
def checksum(data: bytes) -> int:
"""静态工具方法:与实例无关,方便单测与复用。"""
return sum(data) & 0xFF
@classmethod
def from_port(cls, port: str, baudrate: Optional[int] = None) -> "SerialDevice":
"""工厂方法:提供统一构造入口,便于替换子类。"""
return cls(port=port, baudrate=baudrate)
@classmethod
def auto_detect(cls, candidates: Optional[Iterable[str]] = None) -> "SerialDevice":
"""基于枚举端口的简化自动识别流程。"""
ports = list_ports.comports()
if not ports:
raise PortNotFoundError("No serial ports found")
# 简化:默认第一个端口
port_name = ports[0].device
return cls.from_port(port=port_name)
# -------------------------
# 内部实现(下划线方法)
# -------------------------
def _ensure_connected(self) -> None:
if not self._connected:
raise RuntimeError("Device not connected. Call connect() first.")
def _build_frame(self, payload: bytes) -> bytes:
"""简化协议:0xAA | len | payload | csum | 0x0A"""
length = len(payload)
header = bytes([0xAA, length])
csum = self.checksum(payload)
tail = bytes([csum, 0x0A])
return header + payload + tail
def _parse_frame(self, frame: bytes) -> bytes:
if len(frame) < 5 or frame[0] != 0xAA or frame[-1] != 0x0A:
raise ValueError("Bad frame")
length = frame[1]
payload = frame[2:2 + length]
csum = frame[-2]
if self.checksum(payload) != csum:
raise ValueError("Checksum mismatch")
return payload
if __name__ == "__main__":
# 典型使用流程(可直接运行验证逻辑)
dev = SerialDevice.auto_detect()
dev.connect()
dev.send_frame(b"HELLO")
resp = dev.read_frame()
print("RX payload:", resp)
dev.disconnect()

Pythonclass Heavy:
def __init__(self):
self._cache = None
@property
def signature(self) -> str:
if self._cache is None:
# 模拟昂贵计算
self._cache = "calc-result"
return self._cache
h = Heavy()
print(h.signature) # calc-result
Pythonclass OnceAttr:
def __init__(self):
self._token = None
@property
def token(self) -> str:
if self._token is None:
raise AttributeError("token not set")
return self._token
@token.setter
def token(self, value: str) -> None:
if self._token is not None:
raise RuntimeError("token is write-once")
self._token = value
o=OnceAttr()
o.token="hello"
print(o.token)
o.token="world"

Pythonfrom pathlib import Path
import json
class Config:
def __init__(self, data: dict):
self._data = data
@classmethod
def from_json_file(cls, path: str) -> "Config":
text = Path(path).read_text(encoding="utf-8")
return cls(json.loads(text))
@property
def db_url(self) -> str:
return self._data.get("db_url", "sqlite:///app.db")
c=Config({"db_url":"sqlite:///mydb.db"})
print(c.db_url)

本文从“属性与方法”的边界入手,解决了上位机开发中最容易踩的坑。请牢记三点:
职责清晰:用类属性承载默认值与常量,用实例属性承载状态;用实例方法处理业务逻辑,用类方法做工厂与配置入口,用静态方法承载与状态无关的工具函数。
可控性强:通过 @property 公开只读或惰性计算,setter 做强校验,避免对象进入非法状态;必要时使用不可变数据类确保配置稳定。
易扩展与测试:将协议细节封装在内部方法,工具逻辑做静态方法,工厂方法便于替换子类与模拟;这样既利于单元测试,也让 Python开发 的 上位机开发 代码更可维护。
如果你正在做串口设备或仪器通信类项目,不妨把上面的类直接替换为真实串口实现,并逐步引入抽象基类与策略模式。这样做能让你的 编程技巧 从“能跑”跃升为“稳健可扩展”。
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!