前言该用类还是用函数是写 Python 时绕不开的选择。一句话概括函数封装的是 行为类封装的是状态 一组操作状态的行为。如果一件事只是一次输入到 一次输出的变换函数就够了如果一件事要在多次调用之间记住某些东西、 并且有一组相关的操作要围绕这份状态展开类通常更合适。常见的误解有两个。一个是面向对象更高级所以什么都该写成类于是出现了 一堆只有一个staticmethod的类纯粹把函数包了一层。另一个是类一定比函数 重于是该维护状态的地方硬用全局变量结果到处都能改、难以测试。两种都源于 没分清要不要状态这个关键点。本文按本质区别 → 各自适用的场景 → 闭包作为轻量替代 → 数据类简化封装来讲 最后给出一个对照表。示例基于 Python 3dataclass需要 3.7 及以上。一、本质区别行为 vs 状态加行为函数接收参数、返回结果通常不保留调用之间的状态# 适用于 Python 3.8def area(width, height):return width * heightprint(area(3, 4)) # 12类则定义一个模板用它创建的实例各自带一份状态属性并拥有一组可以 操作这份状态的方法# 适用于 Python 3.8class Rectangle:def __init__(self, width, height):self.width widthself.height heightdef area(self):return self.width * self.heightr Rectangle(3, 4)print(r.area()) # 12print(r.width) # 3 状态被记住关键差别Rectangle的实例记得自己的width和height而area函数每次都要重新把两个参数传进去。二、什么时候用函数逻辑是无状态的相同输入必得相同输出。是一次输入到一次输出的变换格式化字符串、计算、解析。不需要在调用之间记住任何东西。你想让代码容易被单测纯函数最好测给输入、验输出即可。# 适用于 Python 3.8def slugify(title):把标题变成 URL 友好的短横线形式。return title.strip().lower().replace( , -)print(slugify(Hello World)) # hello-world这里没有任何需要记住的东西函数就是最自然的表达。三、什么时候用类有状态需要在多次调用之间保持连接、余额、计数器、配置。有一组相关操作围绕同一份状态存款 / 取款 / 查余额。需要多态多个类实现同一套接口运行时替换。需要身份两个实例即使数据相同也代表不同对象。# 适用于 Python 3.8class Account:def __init__(self, balance0):self.balance balancedef deposit(self, amount):self.balance amountreturn self.balancedef withdraw(self, amount):if amount self.balance:raise ValueError(余额不足)self.balance - amountreturn self.balanceacc Account(100)print(acc.deposit(50)) # 150print(acc.withdraw(30)) # 120余额这份状态被实例记住两个操作共享它——这正是类擅长的场景。四、闭包不用类也能封装状态如果只需要一份状态 一两个操作闭包closure是轻量替代不必动用 类# 适用于 Python 3.8def make_counter():count 0def step():nonlocal countcount 1return countreturn stepc make_counter()print(c(), c(), c()) # 1 2 3count被闭包关在函数里外部访问不到效果类似私有属性。什么时候用闭包、 什么时候用类经验是只有一两个操作、不需要继承、不需要多态闭包更短。操作多、要组合、要继承、要能被isinstance判断用类。五、数据类只想装数据时的简化封装如果类的职责主要是装一组字段可以用dataclass需要Python 3.7 及 以上自动生成__init__、__repr__、__eq__省掉一堆样板代码。# 适用于 Python 3.7from dataclasses import dataclassdataclassclass Point:x: floaty: floatp Point(1.0, 2.0)print(p) # Point(x1.0, y2.0)print(p Point(1.0, 2.0)) # True注意dataclass只是替你写样板它不会让对象变成不可变——除非加上frozenTrue。六、对照表维度函数类封装的东西行为状态 行为是否保留状态通常不保留实例各自保留调用方式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__ 少了 selfpython # ❌ 忘记 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, itemsNone): self.items [] if items is None else items想链式调用却忘了 return selfpython # ❌ 返回 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 # ❌ 需要先构造实例、设置一堆属性才能测一个纯计算 # ✅ 纯计算抽成模块级函数类只负责持有状态总结需求推荐无状态的输入输出变换函数一份状态 一两个操作闭包一份状态 一组相关操作类主要用来装字段dataclass3.7需要多态 / 身份类选择的关键就一个问题这件事需要在调用之间记住东西吗不需要就用 函数需要就用类或轻量的闭包。别为了面向对象把纯函数包进只有一个 静态方法的类里也别为了省事把本该封装的状态丢给全局变量——前者是过度设计 后者是埋雷。