Python 如何不使用中间变量,交换两个变量的值?
订单导入脚本里有两个字段写反了:paid_amount 和 refund_amount。 第一眼看代码,我不太信数据库,先看清洗脚本。 结果就卡在这一行:
row["paid_amount"] = row["refund_amount"]
row["refund_amount"] = row["paid_amount"]
这代码看着像在交换两个值,实际不是。
第二行拿到的 row["paid_amount"],已经是第一行改过后的值了。也就是说,两个字段最后会变成一样的。
这种 bug 很烦,不报错,数据还挺“规整”。等财务对账的时候才发现,退款金额和支付金额对不上,查起来比空指针恶心多了。
在 Python 里,交换两个变量,不需要自己搞一个中间变量。
直接写:
paid_amount = 128
refund_amount = 35paid_amount, refund_amount = refund_amount, paid_amount
print(paid_amount) # 35
print(refund_amount) # 128
就这一行。
很多人第一次看会觉得,这不也是先把左边赋值了吗?不会覆盖吗?
不会。
Python 会先把右边的表达式算完,再统一往左边塞。
也就是这句:
paid_amount, refund_amount = refund_amount, paid_amount
大概可以按这个顺序理解:
_right = (refund_amount, paid_amount)
paid_amount = _right[0]
refund_amount = _right[1]
当然,解释器不会真的让你写一个 _right 变量给你看,但执行顺序可以这么记。
所以前面那个清洗脚本,我一般会直接改成这样:
deffix_amount_fields(row: dict) -> dict:
paid = row.get("paid_amount")
refund = row.get("refund_amount")if paid isNoneor refund isNone:
row["check_msg"] = "amount_field_missing"
return row
row["paid_amount"], row["refund_amount"] = refund, paid
row["check_msg"] = "amount_field_swapped"
return row
bad_row = {
"order_no": "T202405190017",
"paid_amount": 128,
"refund_amount": 35,
}
print(fix_amount_fields(bad_row))
输出大概是:
{
'order_no': 'T202405190017',
'paid_amount': 35,
'refund_amount': 128,
'check_msg': 'amount_field_swapped'
}
这里我没有写成一大坨“通用工具函数”。字段清洗这种东西,越通用越容易藏坑。线上排数据时,我更愿意让代码一眼能看出来:哪两个字段被换了,换完之后有没有打标。
变量能这么换,列表里的元素也一样。
比如接口返回的商品列表,排序规则临时有个兜底逻辑,发现第一个不是主商品,就把主商品挪到前面:
goods = [
{"sku": "B02", "main": False},
{"sku": "A01", "main": True},
{"sku": "C09", "main": False},
]main_index = -1
for idx, item in enumerate(goods):
if item["main"]:
main_index = idx
break
if main_index > 0:
goods[0], goods[main_index] = goods[main_index], goods[0]
print(goods)
这种写法比先 tmp = goods[0] 再换三行要干净。
但也别把它神化。
我见过有人为了显得“懂 Python”,连这种代码都写:
a, b, c = c, a, b
不是不能写,是后面的人看着会停一下。
交换两个变量,很顺。三个变量轮转,已经开始有点别扭了。业务代码不是考试题,没必要让别人猜你脑子里的顺序。
还有一种写法,以前面试题里经常出现:
a = a + b
b = a - b
a = a - b
或者:
a = a ^ b
b = a ^ b
a = a ^ b
这种我一般直接划掉。
不是因为它一定错,而是它限制太多。
第一种要求对象能做加减。数字还行,字符串、列表、字典就不行。
第二种更窄,只适合整数这类能做异或的值。你把它写到业务代码里,后面字段类型一变,脚本当场炸。
比如:
left = "paid_amount"
right = "refund_amount"# 这就没法玩什么异或交换
left, right = right, left
print(left, right)
Python 的变量不是盒子里装值,更像是名字贴在对象上。
这点很关键。
看这个例子:
a = [1, 2]
b = [9, 8]a, b = b, a
a.append(7)
print(a) # [9, 8, 7]
print(b) # [1, 2]
交换的是两个名字绑定的对象,不是把列表内容逐个复制一份。
所以它很快,也很自然。
但如果两个变量本来就指向同一个对象,交换没有任何意义:
a = [1, 2]
b = aa, b = b, a
a.append(3)
print(a) # [1, 2, 3]
print(b) # [1, 2, 3]
这种地方我一般会多看一眼。
特别是处理缓存对象、默认配置、请求上下文的时候,别以为交换完就“隔离”了。你只是换了两个名字,底下还是同一个列表,该串还是串。
再贴一个更像现场的例子。
日志里有些历史数据,src_ip 和 dst_ip 被老脚本写反了。不是所有行都反,只能按端口规则判断。这个时候交换字段就很舒服:
defrepair_net_log(line: dict) -> dict:
src_port = int(line.get("src_port", 0))
dst_port = int(line.get("dst_port", 0))# 内网采集脚本的老问题:服务端口被写到了 src_port
if src_port in (80, 443, 8080) and dst_port > 10000:
line["src_ip"], line["dst_ip"] = line["dst_ip"], line["src_ip"]
line["src_port"], line["dst_port"] = line["dst_port"], line["src_port"]
line["repair_tag"] = "swap_client_server"
return line
raw = {
"src_ip": "10.8.3.21",
"src_port": "443",
"dst_ip": "172.16.9.44",
"dst_port": "51432",
}
print(repair_net_log(raw))
这里两组字段一起交换,比一行一行赋值安全。
尤其是这种相关字段,IP 和端口必须一起动。你分开写,中间再夹一个判断或者日志打印,后面改代码的人很容易把顺序改乱。
如果只是两个普通变量:
x, y = y, x
如果是列表两个位置:
items[i], items[j] = items[j], items[i]
如果是字典两个字段:
row["start_time"], row["end_time"] = row["end_time"], row["start_time"]
够用了。
别绕。
Python 已经把这个动作做得很直白了,再写临时变量不是错,但代码会显得像从 C 语言习惯里硬搬过来的。
当然,临时变量也不是永远不能用。
如果交换之前要打日志、做校验、保存旧值,那就老老实实写出来:
old_status = order["status"]
new_status = order["next_status"]if old_status == "PAID"and new_status == "CANCELLED":
order["status"], order["next_status"] = new_status, old_status
order["trace"] = f"status_swap:{old_status}->{new_status}"
这时候可读性比“少一行代码”重要。
我自己的习惯很简单:纯交换,用 a, b = b, a;交换顺便带业务判断,就把旧值拿出来。代码不是越短越好,是别让人查问题的时候骂你。