Python技术迷

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 = 35

paid_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 = a

a, 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;交换顺便带业务判断,就把旧值拿出来。代码不是越短越好,是别让人查问题的时候骂你。