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)]
这里的类就有存在的价值。balance 和 history 必须一起变——存款只改了其中一个就是 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”。
如果你说不出你的类排除了哪种非法状态,那你写的只是一个多绕了几步的命名空间。这不一定错,但它应该是一个决定,而不是条件反射。