Python技术迷

某大厂P9,工作10年,年薪300万+,带上百人团队。他说:“今年业务砍了三轮,团队从200人砍到80人。下一个砍的,可能就是他

45岁,阿里干了10年,年薪300万+,手底下管过上百号人,放外面看,这履历够硬吧?很多人可能觉得,这种人怎么会慌,资源、人脉、钱都不缺。

结果人家一开口就是:我还能撑多久?

Image

这话听着挺扎心的。不是他能力突然不行了,也不是他不努力了,是业务说砍就砍,团队从200人砍到80人,砍到最后,刀未必只往下面落。

以前大家羡慕大厂高P,觉得那是终点。现在看,可能只是另一种高压锅,薪水高是真的,半夜睡不着也是真的。辉煌的时候一群人围着你,风向一变,连自己都得先问一句:下一个是不是我。

算法题:深度合并两个对象

线上配置被覆盖,最烦的不是报错,是它不报错。

base_config 里明明有数据库超时、重试次数、日志级别,环境配置一合并,某个嵌套字段直接没了。你盯着 JSON 看半天,最后发现用的是普通 update()。

base = {
"db": {
"host": "127.0.0.1",
"pool": {
"min": 2,
"max": 10
        }
    },
"log": {
"level": "INFO"
    }
}

prod = {
"db": {
"pool": {
"max": 30
        }
    }
}

base.update(prod)
print(base)

这个输出我一般第一眼就不太信。

因为 update() 只看第一层。它看到 db 这个 key 已经存在,就直接把整个 db 替换掉。至于里面的 host、pool.min,它不管。

最后结果会变成:

{
"db": {
"pool": {
"max": 30
        }
    },
"log": {
"level": "INFO"
    }
}

db.host 没了。

这就不是“合并”,这是覆盖。

真正要写的是深度合并:两边都是字典,就继续往下合;只要有一边不是字典,就按右边覆盖左边。

我平时会这么写:

from copy import deepcopy
from collections.abc import Mapping


defmerge_config(base, patch):
    result = deepcopy(base)

for key, patch_value in patch.items():
        base_value = result.get(key)

if isinstance(base_value, Mapping) and isinstance(patch_value, Mapping):
            result[key] = merge_config(base_value, patch_value)
continue

        result[key] = deepcopy(patch_value)

return result

拿刚才的数据跑一下:

merged = merge_config(base, prod)

print(merged)

输出:

{
"db": {
"host": "127.0.0.1",
"pool": {
"min": 2,
"max": 30
        }
    },
"log": {
"level": "INFO"
    }
}

这才对。

这里有两个地方我会特别留意。

第一个是 deepcopy。不要直接在原对象上改,尤其是配置、请求参数、默认模板这种东西。你这次改了,下一次调用可能就带着脏数据继续跑,排查起来很恶心。

第二个是列表不要乱合并。

比如:

base = {"allow_ips": ["10.0.0.1", "10.0.0.2"]}
patch = {"allow_ips": ["10.0.0.3"]}

到底是追加,还是覆盖?

这不是算法能拍脑袋决定的,这是业务规则。权限列表可能要覆盖,告警接收人可能要追加,插件顺序甚至还要去重保序。

所以我默认只对字典做深度合并,其他类型直接以后面的为准。规则清楚,问题少。

再补一个现场常用的校验:

defpick(data, path):
    cur = data
for name in path.split("."):
ifnot isinstance(cur, Mapping) or name notin cur:
returnNone
        cur = cur[name]
return cur


merged = merge_config(base, prod)

assert pick(merged, "db.host") == "127.0.0.1"
assert pick(merged, "db.pool.max") == 30
assert pick(merged, "db.pool.min") == 2

这种断言别嫌土。配置合并这种代码,平时看着简单,真出问题一般都是线上某个字段“悄悄没了”。

递归这块不复杂,复杂的是边界别装聪明。

两边都是对象,继续合。

不是对象,右边覆盖。

列表、字符串、数字、布尔值,别擅自发挥。

这样写,代码短,行为也稳。