Blog

Python 中什么时候该用类

Python 里大多数类其实本该写成函数。判断标准不是“这是不是名词”,而是有没有一种非法状态需要类来阻止。

大多数 Python 代码库里都有本该写成函数的类。只有一个方法,没有值得保护的状态,存在的理由只是有人学过“面向对象就是写类”。

所以这个系列从“为什么别写类”讲起。

披着类外衣的函数

# a class with one method and no state is a function wearing a costume
class TaxCalculator:
    def __init__(self, rate):
        self.rate = rate
    def calculate(self, amount):
        return amount * self.rate

calc = TaxCalculator(0.2)
print(calc.calculate(100))

def calculate_tax(amount, rate=0.2):
    return amount * rate

print(calculate_tax(100))

输出:

20.0
20.0

结果一样。函数只有一行,不用先构造对象,测试时也不用实例化任何东西。这个类什么都没给你带来。

这是最常见的多余的类:保存一份配置,再用一个方法去用它。在 Python 里,这就是一个带默认参数的函数。

数据就只是数据

如果你只是把几个总是一起出现的值放在一起,用字典或 namedtuple 比用类表达得更清楚:

# a dict is enough when the data is just data
point = {'x': 3, 'y': 4}
print(point['x'], point['y'])

from collections import namedtuple
Point = namedtuple('Point', 'x y')
p = Point(3, 4)
print(p, p.x, p.x + p.y)

输出:

3 4
Point(x=3, y=4) 3 7

一行 namedtuple 就给了你属性访问、可读的 repr 和不可变性。本系列第 8 篇会把这四种写法放在一起认真比较。

真正管用的判断标准

不是“这是不是名词”。名词到处都是,大部分用字典就够了。

标准是:有没有一种非法状态,是这个类能阻止的?

# state that changes together is what a class is for
class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance
        self.history = []

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError(f"deposit must be positive, got {amount!r}")
        self.balance += amount
        self.history.append(('deposit', amount))

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError(f"balance is {self.balance}, cannot withdraw {amount}")
        self.balance -= amount
        self.history.append(('withdraw', amount))

acct = BankAccount(100)
acct.deposit(50)
acct.withdraw(30)
print(acct.balance, acct.history)

输出:

120 [('deposit', 50), ('withdraw', 30)]

这里的类就有存在的价值。balancehistory 必须一起变——存款只改了其中一个就是 bug,而从类外面根本写不出这种 bug。

看它怎么拒绝:

acct2 = BankAccount(10)
try:
    acct2.withdraw(999)
except ValueError as err:
    print(type(err).__name__ + ':', err)
print('balance untouched:', acct2.balance)

输出:

ValueError: balance is 10, cannot withdraw 999
balance untouched: 10

换成字典,这笔取款就会放过去,留下一个负数余额,历史记录里还什么都没有。

要点

  • 只有一个方法、没有状态,那就该写成函数。
  • 总是一起出现的值,用字典、namedtuple 或数据类(dataclass)。
  • 当两份以上的状态必须一起变,并且类能拒绝进入非法状态时,类才有存在的价值。
  • 标准不是“它是不是名词”,而是“它能不能防住 bug”。

如果你说不出你的类排除了哪种非法状态,那你写的只是一个多绕了几步的命名空间。这不一定错,但它应该是一个决定,而不是条件反射。

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。