拆分变量
拆分变量(Split Variable)把一个先后承担了几种用途的变量拆成几个变量,每个只承担一种用途。 它解决的痛是:同一个名字在函数的前半段是一个意思,后半段是另一个意思,读的人要追踪每一次赋值才知道它此刻装着什么。
一个算矩形尺寸的函数,图省事用一个 temp 先存周长、再存面积。另一个算折扣的函数,直接修改了传进来的参数。
"""矩形与折扣:拆分前。"""
def describe(width: float, height: float) -> str: temp = 2 * (width + height) perimeter_text = f"周长 {temp}" temp = width * height return f"{perimeter_text},面积 {temp}"
def discounted(price: float, quantity: int) -> float: if price > 50: price = price - 2 if quantity > 100: price = price - 1 return price
if __name__ == "__main__": print(describe(3, 4)) print(discounted(60, 120), discounted(30, 10))
# 输出:# 周长 14,面积 12# 57 30// 矩形与折扣:拆分前。function describe(width: number, height: number): string { let temp = 2 * (width + height); const perimeterText = `周长 ${temp}`; temp = width * height; return `${perimeterText},面积 ${temp}`;}
function discounted(price: number, quantity: number): number { if (price > 50) price = price - 2; if (quantity > 100) price = price - 1; return price;}
console.log(describe(3, 4));console.log(discounted(60, 120), discounted(30, 10));
// 输出:// 周长 14,面积 12// 57 30如果现在要在面积之后再输出周长和面积之比,写的人看到 temp 会犹豫:它现在是周长还是面积?得往上数赋值。更危险的是有人在第二次赋值之后又用 temp 当周长,代码照常运行,结果却错了。
discounted 里的 price 也一样:函数开头它是原价,中间某一行之后它变成了折后价。想在函数末尾打印“原价多少、优惠了多少”,原价已经被覆盖掉了。这是可变数据在一个函数之内的样子。
"""矩形与折扣:拆分后。"""
def describe(width: float, height: float) -> str: perimeter = 2 * (width + height) area = width * height return f"周长 {perimeter},面积 {area}"
def discounted(original_price: float, quantity: int) -> float: result = original_price if original_price > 50: result = result - 2 if quantity > 100: result = result - 1 return result
if __name__ == "__main__": print(describe(3, 4)) print(discounted(60, 120), discounted(30, 10))
# 输出:# 周长 14,面积 12# 57 30// 矩形与折扣:拆分后。function describe(width: number, height: number): string { const perimeter = 2 * (width + height); const area = width * height; return `周长 ${perimeter},面积 ${area}`;}
function discounted(originalPrice: number, quantity: number): number { let result = originalPrice; if (originalPrice > 50) result = result - 2; if (quantity > 100) result = result - 1; return result;}
console.log(describe(3, 4));console.log(discounted(60, 120), discounted(30, 10));
// 输出:// 周长 14,面积 12// 57 30perimeter 和 area 各自只被赋值一次,TS 里可以用 const 声明,编译器保证它们不会被再次赋值。discounted 的原价保存在 original_price 里不再改动,折后价在 result 里累积,两个值同时都在。
怎么一步步做
Section titled “怎么一步步做”- 在变量第一次被赋值的地方,给它改一个说明这一次用途的名字。TS 里如果可以就声明成
const。 - 把这一次赋值到下一次赋值之间的所有引用改成新名字。
- 在第二次赋值的地方声明一个新变量,同样起一个说明用途的名字。
- 跑测试。对每一次赋值重复。
像 total += x 这种在循环里累加的变量不是“多种用途”,它从头到尾都是“累计值”,不要拆。
定义与适用场景
Section titled “定义与适用场景”拆分变量:如果一个变量被赋值不止一次,并且每次赋值代表不同的东西,就为每一种用途单独声明一个变量。它最常见于两处:名叫 temp、result、value 这种泛泛的变量被反复复用;函数参数在函数体里被重新赋值。后者在 Python 和 JS 里都是合法的,但它让参数在函数中途改变了含义,Fowler 建议把参数当作只读的。
拆分之后还经常能顺手做下一步。perimeter 和 area 各自只依赖宽和高,可以直接提炼成两个函数(第 4 章),或者在类里变成两个属性(第 16 章以查询取代临时变量)。而一个被复用的 temp 是没法提炼的,因为它在不同的行代表不同的东西。拆分变量常常是这些手法的第一步:先让每个名字只代表一件事,后面的重构才有抓手。
discounted 里的 result 是另一种常见的写法:一个专门用来累积返回值的变量,名字就叫 result,从头到尾只表示“将要返回的值”。它被赋值了好几次,但每次都是同一个含义,所以不需要再拆。
真实系统里的例子
Section titled “真实系统里的例子”请求处理里被复用的变量
Section titled “请求处理里被复用的变量”接口处理函数里常见一个 data 变量:先是原始请求体,再是校验后的字典,最后是要返回的响应。拆开之后,每个阶段的数据都有自己的名字,出错时打印也知道打的是哪一个。
"""接口处理:一个 data 变量拆成三个。"""
import json
def handle(raw_body: str) -> str: payload = json.loads(raw_body) validated = {"name": payload["name"].strip(), "qty": int(payload["qty"])} response = {"ok": True, "item": validated["name"], "total": validated["qty"] * 25} return json.dumps(response, ensure_ascii=False)
if __name__ == "__main__": print(handle('{"name": " 咖啡豆 ", "qty": "3"}'))
# 输出:# {"ok": true, "item": "咖啡豆", "total": 75}原来三行都写成 data = ...,第二行出了 KeyError 时,日志里打印的 data 已经分不清是哪个阶段的了。
Python 没有 const,但可以用 typing.Final 标注一个变量不应再被赋值,mypy 会在有人重新赋值时报错。这在模块级常量上很常用;在函数内部的局部变量上用得少一些,因为拆分之后每个变量只被赋值一次,名字本身已经足够清楚。真正要防的是第二次赋值悄悄改变了含义,拆分变量从结构上消除了这种可能。
什么时候别用
Section titled “什么时候别用”循环里的累加器、拼接字符串的缓冲区,从头到尾是同一个意思,不拆。只在两三行里存在的临时变量,用途一目了然,也不必拆。
判断是否该拆,可以问:能不能给这个变量起一个从头到尾都准确的名字?起不出来,就是该拆了。
- 一个变量被赋值两次、两次含义不同,就拆成两个变量,各起一个准确的名字。
- 能用
const就用const,让编译器保证不会被再次赋值。 - 不要给函数参数重新赋值,把要变化的值放进新的局部变量。
- 累加器从头到尾是同一个意思,不需要拆。