Python技术迷

北上广深程序员,在北京年 20w,回郑州要降薪还不到15w。笑哭,回老家, 这么难嘛?

北上广深干程序员,年包二十来万,听着也不算啥财务自由,顶多就是房租一交、饭一吃、偶尔买点电子垃圾,余额开始装死。

结果一说回郑州,薪资直接往下砍,十五万都不一定稳。你以为回老家是生活降本,结果先给你来个收入腰斩预告,HR那边还挺淡定:我们这边行情就这样。

Image

最扎心的是,你还没来得及嫌钱少,人家岗位已经一堆人排着了。海归的、985的、大厂回流的、被裁后想稳一点的,全挤在一个池子里。

所以现在的问题不是“回老家能不能舒服点”,而是老家也不是以前那个老家了。大家都想跑回去,坑还是那么几个。

程序员这行,卷到最后,连退路都开始堵车了。

算法题:将函数绑定到上下文

calc_fee(100) 一调用就炸,报错也不绕弯子:

TypeError: calc_fee() missing 1 required positional argument: 'ctx'

这类题看着像语法题,其实考的是一件事:函数调用时,能不能提前把上下文塞进去。

在 Python 里,函数本身不认识什么“当前用户”“当前配置”“当前请求”。它只认参数。所谓“将函数绑定到上下文”,就是返回一个新函数,以后调用这个新函数时,不用每次手动传 ctx。

先看一个很土的业务函数:

classPriceContext:
def__init__(self, user_level, discount):
        self.user_level = user_level
        self.discount = discount


defcalc_fee(ctx, origin_price, count):
    fee = origin_price * count

if ctx.user_level == "vip":
        fee = fee * ctx.discount

return round(fee, 2)

正常调用是这样:

ctx = PriceContext("vip", 0.85)

print(calc_fee(ctx, 120, 2))

写一次还行。要是后面一堆地方都要算价格,每次都传 ctx,代码就开始难看了:

calc_fee(ctx, 120, 2)
calc_fee(ctx, 59, 5)
calc_fee(ctx, 199, 1)

我一般看到这种重复传上下文的代码,第一反应不是抽类,也不是上设计模式,先写个很小的绑定函数。

defbind_context(func, ctx):
defwrapped(*args, **kwargs):
return func(ctx, *args, **kwargs)

return wrapped

用起来是这样:

vip_calc_fee = bind_context(calc_fee, ctx)

print(vip_calc_fee(120, 2))
print(vip_calc_fee(59, 5))
print(vip_calc_fee(199, 1))

这里关键就一行:

return func(ctx, *args, **kwargs)

ctx 被提前固定住了,后面的 origin_price、count 照常从外面传进来。

这题真正容易写错的地方,是只处理普通参数,忘了关键字参数。比如有些函数长这样:

defbuild_log(ctx, action, *, trace_id=None):
    prefix = f"[{ctx.user_level}]"
if trace_id:
returnf"{prefix}{trace_id}{action}"
returnf"{prefix}{action}"

如果绑定函数里没有 **kwargs,这种调用就直接废了:

log = bind_context(build_log, ctx)

print(log("submit_order", trace_id="T20260520"))

所以完整一点的版本,我会这样写:

defbind_context(func, ctx, *fixed_args, **fixed_kwargs):
defwrapped(*args, **kwargs):
        merged_kwargs = {}
        merged_kwargs.update(fixed_kwargs)
        merged_kwargs.update(kwargs)

return func(ctx, *fixed_args, *args, **merged_kwargs)

return wrapped

这个版本多做了一件事:支持提前绑定一部分普通参数和关键字参数。

比如报表导出时,环境、租户、格式这些东西通常不会每次变:

classExportContext:
def__init__(self, tenant_id):
        self.tenant_id = tenant_id


defexport_rows(ctx, table_name, rows, *, file_type="csv"):
return {
"tenant": ctx.tenant_id,
"table": table_name,
"count": len(rows),
"file_type": file_type
    }


ctx = ExportContext("tenant_1001")

export_order = bind_context(
    export_rows,
    ctx,
"order_detail",
    file_type="xlsx"
)

result = export_order([
    {"id": 1, "amount": 98},
    {"id": 2, "amount": 199}
])

print(result)

输出大概是:

{
'tenant': 'tenant_1001',
'table': 'order_detail',
'count': 2,
'file_type': 'xlsx'
}

这就比较像真实代码了。调用方只关心 rows,不用知道租户从哪来、表名是什么、导出格式怎么定。

不过这里有个坑,我不太建议忽略。

如果 ctx 是可变对象,绑定进去的是对象引用,不是快照。

ctx.discount = 0.7

print(vip_calc_fee(120, 2))

这时候算出来会按新折扣走。这个行为有时候是你想要的,比如请求上下文里追加 trace;有时候就很危险,比如价格策略中途被改了。

如果想绑定那一刻就固定上下文,可以做一次拷贝:

import copy


defbind_context_snapshot(func, ctx):
    saved_ctx = copy.deepcopy(ctx)

defwrapped(*args, **kwargs):
return func(saved_ctx, *args, **kwargs)

return wrapped

算法题写到这里就够了。

它不是在考你会不会造一个复杂框架,而是考你能不能用闭包保存现场:外层函数接收 func 和 ctx,内层函数真正执行调用,并把上下文补到参数最前面。

这个东西在 Python 里很常见。装饰器、偏函数、回调兜底,底下都有这股味道。写的时候别把它想玄了,盯住调用那一刻参数怎么排,就不容易错。