Python中使用封装类还是函数以及它们的区别

前言

"该用类还是用函数"是写 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 |

七、怎么选:一条决策线

按下面顺序问自己:

  1. 这份数据需要在多次调用之间保留吗?不需要 → 用函数。
  2. 只有一两个操作、不需要继承吗?是 → 闭包也可以。
  3. 主要是装字段 吗?是 → 用 @dataclass。
  4. 有多个相关操作 / 需要多态 / 需要身份吗?是 → 用类。

常见坑点

  1. 类属性当实例状态用,导致所有实例共享

```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 = \[\] ```

  1. 只有一个 staticmethod 的类

``python # ❌ 包了一层,没有任何状态,还不如直接写函数 class MathUtil: @staticmethod def square(x): return x * x # ✅ 模块级函数更简单 def square(x): return x * x ``

  1. 用类当"命名空间"

``python # ❌ 类里全是常量,实例化毫无意义 class Config: HOST = "localhost" PORT = 8080 # ✅ 简单场景用模块级常量或 dataclass HOST = "localhost" PORT = 8080 ``

  1. 通过实例"原地修改"类属性,连带改了所有实例

``python class Basket: items = [] # 类属性,全类共享 b1 = Basket() # ❌ 以为改的是自己那份,其实动的是那个共享列表 b1.items.append("apple") # ✅ 要每个实例独立,必须在 __init__ 里建 self.items class Basket: def __init__(self): self.items = [] ``

  1. init 少了 self

``python # ❌ 忘记 self,实例状态根本保存不下来 class A: def __init__(balance): balance = 0 # ✅ 第一个参数是 self,用 self. 保存 class A: def __init__(self): self.balance = 0 ``

  1. 在 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 ``

  1. 想链式调用却忘了 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 ``

  1. 把无状态逻辑硬塞进类,导致难以测试

``python # ❌ 需要先构造实例、设置一堆属性才能测一个纯计算 # ✅ 纯计算抽成模块级函数,类只负责持有状态 ``

总结

需求 推荐

|------------|----|
| 无状态的输入输出变换 | 函数 |

|--------------|----|
| 一份状态 + 一两个操作 | 闭包 |

|---------------|---|
| 一份状态 + 一组相关操作 | 类 |

|---------|--------------------|
| 主要用来装字段 | @dataclass(3.7+) |

|-----------|---|
| 需要多态 / 身份 | 类 |

选择的关键就一个问题:这件事需要在调用之间"记住"东西吗? 不需要就用 函数,需要就用类(或轻量的闭包)。别为了"面向对象"把纯函数包进只有一个 静态方法的类里,也别为了省事把本该封装的状态丢给全局变量------前者是过度设计, 后者是埋雷。

相关推荐
代码什么用1 小时前
Java Web 21-22
java·开发语言·web
青柠之夏cc1 小时前
Python Java PHP底层对比:内存并发性能全解析
开发语言·python·线程
TomEval1 小时前
【测AI】第10篇:AI 模型功能测试实战 —— 测试用例怎么设计?
人工智能·python·功能测试·测试用例
半亩码田1 小时前
C#转Python第5.1篇:异步编程:asyncio vs async/await,语法相似但机制不同
开发语言·python·c#
yi0111 小时前
DAY27: LeetCode 110. 平衡二叉树 | 在递归中判断是否平衡
人工智能·笔记·python·算法·leetcode·二叉树·递归
LMT_Lanmantech1 小时前
MATLAB合规管理:如何避免侵权赔偿和审计风险
开发语言·matlab·信息可视化
言乐62 小时前
PythonLOLM自动生成复盘报告模型
开发语言·前端·数据库·人工智能·python
霸道流氓气质2 小时前
HashiCorp Vault 与云 KMS 入门实战:动态密钥、SecretLeaseContainer 与 Java 集成完整指南
java·开发语言
水水不水啊2 小时前
告别切换N个窗口调试:我用 Python + WebView 做了款现代化串口助手(含 TCP/UDP、波形、协议解析)
python·tcp/ip·udp