前言

Python 里"一切皆对象",但很多同学写脚本一直用不上自定义类。这篇把 OOP 核心概念讲到位,同时告诉你 Pythonic 的取舍——不为了面向对象而面向对象。

一、类与实例

class Server:
    """表示一台服务器的类"""                # docstring

    def __init__(self, host, port=80):    # 构造方法
        self.host = host                   # 实例属性
        self.port = port
        self._status = "unknown"           # _前缀 = 约定私有
        self.__secret = "x"                # __双下划线 = 名称改写(name mangling)

    def ping(self):                        # 实例方法
        return f"ping {self.host}:{self.port} -> {self._status}"

    def set_status(self, status):          # 实例方法: 修改状态
        self._status = status


web = Server("10.0.0.1", 8080)             # 实例化(不需要 new)
web.set_status("up")
print(web.ping())                          # ping 10.0.0.1:8080 -> up

db = Server("10.0.0.2", 5432)              # 每个实例状态独立
print(web.port, db.port)                   # 8080 5432

要点:

  • self 是实例自身的引用,实例方法第一个参数永远是它(调用时不用传)
  • Python 没有 true private,_x / __x 只是约定和名称改写

类属性 vs 实例属性:

class Server:
    kind = "linux"            # 类属性, 所有实例共享

    def __init__(self, host):
        self.host = host      # 实例属性, 每个实例独立

a, b = Server("s1"), Server("s2")
print(a.kind)                 # linux(实例找不到就找类)
Server.kind = "bsd"           # 改类属性, 全体生效
a.kind = "windows"            # 给 a 创建了实例属性遮蔽类属性
print(b.kind)                 # bsd —— b 不受影响

二、继承与 super()

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

    def send(self, msg):
        raise NotImplementedError           # 接口约定: 子类必须实现

    def info(self):
        return f"notifier: {self.name}"


class DingtalkNotifier(BaseNotifier):
    def __init__(self, name, webhook):
        super().__init__(name)              # 复用父类初始化
        self.webhook = webhook

    def send(self, msg):                    # 方法重写(override)
        print(f"[{self.name}] POST {self.webhook} body={msg}")


class EmailNotifier(BaseNotifier):
    def __init__(self, name, smtp):
        super().__init__(name)
        self.smtp = smtp

    def send(self, msg):
        print(f"[{self.name}] SMTP {self.smtp} body={msg}")


# 多态: 同一接口, 不同行为
notifiers = [
    DingtalkNotifier("ding", "https://oapi.dingtalk.com/robot/send?token=x"),
    EmailNotifier("mail", "smtp.corp.local"),
]
for n in notifiers:
    n.send("CPU usage high!")       # 各自的 send 被调用

isinstance / issubclass / 鸭子类型:

isinstance(n, BaseNotifier)      # True
issubclass(DingtalkNotifier, BaseNotifier)   # True

# 但 Python 更推崇鸭子类型: "会 send 就行, 管你是谁生的"
class Webhook:                    # 没继承任何类
    def send(self, msg): ...

for n in [DingtalkNotifier(...), Webhook()]:
    n.send("hi")                  # ✅ 都能跑

三、魔术方法:让对象更 Pythonic

class Money:
    def __init__(self, yuan):
        self.yuan = yuan

    def __repr__(self):                      # 调试/打印显示
        return f"Money({self.yuan}¥)"

    def __eq__(self, other):                 # ==
        return self.yuan == other.yuan

    def __lt__(self, other):                 # <  (配合 sorted)
        return self.yuan < other.yuan

    def __add__(self, other):                # +
        return Money(self.yuan + other.yuan)

    def __len__(self):                       # len()
        return self.yuan


a, b = Money(100), Money(50)
print(a)                # Money(100¥)
print(a + b)            # Money(150¥)
print(a > b)            # True
sorted([b, a])          # [Money(50¥), Money(100¥)]

常用魔术方法速查:

方法 触发场景
__init__ 实例化
__repr__ / __str__ 打印、调试
__eq__ __lt__ 比较、排序
__len__ __getitem__ len()、下标访问
__enter__ __exit__ with 上下文管理
__iter__ __next__ for 迭代

四、property:优雅的读写控制

class AppConfig:
    def __init__(self):
        self._port = 80

    @property
    def port(self):                     # 读 port 时触发
        return self._port

    @port.setter
    def port(self, value):              # 写 port 时触发校验
        if not (1 <= value <= 65535):
            raise ValueError(f"非法端口: {value}")
        self._port = value


conf = AppConfig()
conf.port = 8080                        # 走 setter 校验
print(conf.port)                        # 走 getter
# conf.port = 99999                     # ValueError

好处:调用方代码不变(conf.port = x),校验逻辑却收进类里。

五、dataclass:消灭样板代码

Python 3.7+ 的 @dataclass 让"数据类"一行搞定:

from dataclasses import dataclass, field

@dataclass
class Host:
    name: str
    ip: str
    port: int = 22
    tags: list[str] = field(default_factory=list)   # 可变默认值的正确姿势

# 自动获得: __init__ / __repr__ / __eq__
h1 = Host("web01", "10.0.1.11", tags=["web"])
h2 = Host("web01", "10.0.1.11", tags=["web"])
print(h1)          # Host(name='web01', ip='10.0.1.11', port=22, tags=['web'])
print(h1 == h2)    # True

再加一个方法就是完整业务对象:

@dataclass
class Host:
    ...
    @property
    def addr(self) -> str:
        return f"{self.ip}:{self.port}"

六、什么时候不需要类

# ❌ 过度设计: 一个脚本也要 AbstractFactorySingleton
# ✅ 这些场景用函数/字典就够了:

# 场景1: 只是存数据 -> dict / dataclass
server = {"host": "a", "port": 80}

# 场景2: 一组相关函数 -> 模块本身就够
# deploy.py: install(), start(), stop()

# 场景3: 简单分发 -> 字典比类层次好
handlers = {"start": do_start, "stop": do_stop}
handlers[action]()

判断标准:有状态要封装、有多态要抽象、样板代码太多——满足其一才上类。

小结

概念 关键点
__init__ + self 实例状态初始化
继承 + super() 复用父类;Python 更爱组合与鸭子类型
魔术方法 让自定义对象融入语言原生操作
property 无感校验读写
dataclass 数据类首选,一行顶十行
克制 脚本级代码用函数 + dict 就够

本文是「Python」系列第 4 篇。