前言
"该用类还是用函数"是写 Python 时绕不开的选择。一句话概括:函数封装的是 "行为",类封装的是"状态 + 一组操作状态的行为"。 如果一件事只是一次输入到 一次输出的变换,函数就够了;如果一件事要在多次调用之间记住某些东西、 并且有一组相关的操作要围绕这份状态展开,类通常更合适。
常见的误解有两个。一个是"面向对象更高级,所以什么都该写成类",于是出现了 一堆只有一个 @staticmethod 的类,纯粹把函数包了一层。另一个是"类一定比函数 重",于是该维护状态的地方硬用全局变量,结果到处都能改、难以测试。两种都源于 没分清"要不要状态"这个关键点。
本文按"本质区别 → 各自适用的场景 → 闭包作为轻量替代 → 数据类简化封装"来讲, 最后给出一个对照表。示例基于 Python 3,dataclass 需要 3.7 及以上。
一、本质区别:行为 vs 状态加行为
函数接收参数、返回结果,通常不保留调用之间的状态:
python
# 适用于 Python 3.8+
def area(width, height):
return width * height
print(area(3, 4)) # 12
类则定义一个模板,用它创建的实例各自带一份状态(属性),并拥有一组可以 操作这份状态的方法:
python
# 适用于 Python 3.8+
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
r = Rectangle(3, 4)
print(r.area()) # 12
print(r.width) # 3 状态被记住
关键差别:Rectangle 的实例记得 自己的 width 和 height,而 area 函数每次都要重新把两个参数传进去。
二、什么时候用函数
- 逻辑是无状态的:相同输入必得相同输出。
- 是一次输入到一次输出的变换:格式化字符串、计算、解析。
- 不需要在调用之间记住任何东西。
- 你想让代码容易被单测:纯函数最好测,给输入、验输出即可。
python
# 适用于 Python 3.8+
def slugify(title):
"""把标题变成 URL 友好的短横线形式。"""
return title.strip().lower().replace(" ", "-")
print(slugify("Hello World")) # hello-world
这里没有任何需要"记住"的东西,函数就是最自然的表达。
三、什么时候用类
- 有状态需要在多次调用之间保持:连接、余额、计数器、配置。
- 有一组相关操作围绕同一份状态:存款 / 取款 / 查余额。
- 需要多态:多个类实现同一套接口,运行时替换。
- 需要身份:两个实例即使数据相同,也代表不同对象。
python
# 适用于 Python 3.8+
class Account:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("余额不足")
self.balance -= amount
return self.balance
acc = Account(100)
print(acc.deposit(50)) # 150
print(acc.withdraw(30)) # 120
余额这份状态被实例记住,两个操作共享它------这正是类擅长的场景。
四、闭包:不用类也能封装状态
如果只需要"一份状态 + 一两个操作",**闭包(closure)**是轻量替代,不必动用 类:
python
# 适用于 Python 3.8+
def make_counter():
count = 0
def step():
nonlocal count
count += 1
return count
return step
c = make_counter()
print(c(), c(), c()) # 1 2 3
count 被闭包"关"在函数里,外部访问不到,效果类似私有属性。什么时候用闭包、 什么时候用类?经验是:
- 只有一两个操作、不需要继承、不需要多态:闭包更短。
- 操作多、要组合、要继承、要能被
isinstance判断:用类。
五、数据类:只想装数据时的简化封装
如果类的职责主要是"装一组字段",可以用 @dataclass(需要 Python 3.7 及 以上 )自动生成 __init__、__repr__、__eq__,省掉一堆样板代码。
python
# 适用于 Python 3.7+
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p) # Point(x=1.0, y=2.0)
print(p == Point(1.0, 2.0)) # True
注意 dataclass 只是替你写样板,它不会让对象变成不可变 ------除非加上 frozen=True。
六、对照表
| 维度 | 函数 | 类 |
|---|
|-------|----|---------|
| 封装的东西 | 行为 | 状态 + 行为 |
|--------|-------|--------|
| 是否保留状态 | 通常不保留 | 实例各自保留 |
|------|-----------|------------|
| 调用方式 | f(a, b) | obj.m(a) |
|------|---|-------------|
| 能否多态 | 否 | 可以(继承 / 协议) |
|------|--------|-------|
| 测试难度 | 纯函数最易测 | 需构造实例 |
|------|----------|--------------|
| 典型场景 | 变换、计算、解析 | 连接、余额、配置、状态机 |
|------|--------|--------------------------|
| 轻量替代 | ------ | 闭包 / functools.partial |
七、怎么选:一条决策线
按下面顺序问自己:
- 这份数据需要在多次调用之间保留吗?不需要 → 用函数。
- 只有一两个操作、不需要继承吗?是 → 闭包也可以。
- 主要是装字段 吗?是 → 用
@dataclass。 - 有多个相关操作 / 需要多态 / 需要身份吗?是 → 用类。
常见坑点
- 类属性当实例状态用,导致所有实例共享
```python # ❌ items 是类属性,所有实例共用同一个列表 class Basket: items = \[\] def add(self, x): self.items.append(x)
b1 = Basket(); b1.add(1) b2 = Basket(); b2.add(2) print(b1.items) # 1, 2,被 b2 也改了 # ✅ 在 init 里给每个实例建自己的列表 class Basket: def init(self): self.items = \[\] ```
- 只有一个 staticmethod 的类
``python # ❌ 包了一层,没有任何状态,还不如直接写函数 class MathUtil: @staticmethod def square(x): return x * x # ✅ 模块级函数更简单 def square(x): return x * x ``
- 用类当"命名空间"
``python # ❌ 类里全是常量,实例化毫无意义 class Config: HOST = "localhost" PORT = 8080 # ✅ 简单场景用模块级常量或 dataclass HOST = "localhost" PORT = 8080 ``
- 通过实例"原地修改"类属性,连带改了所有实例
``python class Basket: items = [] # 类属性,全类共享 b1 = Basket() # ❌ 以为改的是自己那份,其实动的是那个共享列表 b1.items.append("apple") # ✅ 要每个实例独立,必须在 __init__ 里建 self.items class Basket: def __init__(self): self.items = [] ``
- init 少了 self
``python # ❌ 忘记 self,实例状态根本保存不下来 class A: def __init__(balance): balance = 0 # ✅ 第一个参数是 self,用 self. 保存 class A: def __init__(self): self.balance = 0 ``
- 在 init 里用可变默认参数
``python # ❌ 默认列表在所有实例间共享 class Basket: def __init__(self, items=[]): self.items = items # ✅ 用 None 哨兵 class Basket: def __init__(self, items=None): self.items = [] if items is None else items ``
- 想链式调用却忘了 return self
``python # ❌ 返回 None,链式调用会崩 class Query: def where(self, cond): self.conds.append(cond) # ✅ 返回 self class Query: def where(self, cond): self.conds.append(cond) return self ``
- 把无状态逻辑硬塞进类,导致难以测试
``python # ❌ 需要先构造实例、设置一堆属性才能测一个纯计算 # ✅ 纯计算抽成模块级函数,类只负责持有状态 ``
总结
| 需求 | 推荐 |
|---|
|------------|----|
| 无状态的输入输出变换 | 函数 |
|--------------|----|
| 一份状态 + 一两个操作 | 闭包 |
|---------------|---|
| 一份状态 + 一组相关操作 | 类 |
|---------|--------------------|
| 主要用来装字段 | @dataclass(3.7+) |
|-----------|---|
| 需要多态 / 身份 | 类 |
选择的关键就一个问题:这件事需要在调用之间"记住"东西吗? 不需要就用 函数,需要就用类(或轻量的闭包)。别为了"面向对象"把纯函数包进只有一个 静态方法的类里,也别为了省事把本该封装的状态丢给全局变量------前者是过度设计, 后者是埋雷。