Python技术迷

别再用 utils.py 了!这 3 个设计模式让你的 Python 代码更清晰

utils.py 一开始都挺顺手。过两个月再看,里面塞了 40 多个函数:format_time()、send_email()、build_headers()、load_yaml()、retry_request()、mask_phone()……谁都能往里丢,最后谁也不敢动。

这玩意最烦的,不是丑,是它会把边界抹平。你明明是在写订单同步,结果一堆核心逻辑都藏在 utils.xxx() 里,调用点看着干净,排障时全是坑。尤其 Python 项目,没点约束,utils.py 很容易养成“公共垃圾桶”。这种味道,我一般看到第二眼就不太信了。

真想把代码收拾清楚,不是把 utils.py 改个名字就行,得把“变化”拎出来。下面这 3 个设计模式,够用,而且比继续堆工具函数靠谱得多。

1)策略模式:别让 if/elif 越长越像事故现场

很多人写导出、计费、风控规则,第一版都这样:

defcalc_fee(channel: str, amount: int) -> int:
if channel == "wechat":
return int(amount * 0.006)
elif channel == "alipay":
return int(amount * 0.0055)
elif channel == "bank":
return2
raise ValueError(f"unknown channel: {channel}")

刚开始 3 个分支还能忍。等你加上企业渠道、活动期费率、海外卡通道,这段代码就会开始发黏。再过一阵,测试说“只有 bank 的老商户走旧逻辑”,你就知道报应来了。

这种地方别再往 utils.py 塞函数,直接上策略模式:

from abc import ABC, abstractmethod

classFeeStrategy(ABC):
    @abstractmethod
defcalc(self, amount: int) -> int:
pass

classWechatFee(FeeStrategy):
defcalc(self, amount: int) -> int:
return int(amount * 0.006)

classAlipayFee(FeeStrategy):
defcalc(self, amount: int) -> int:
return int(amount * 0.0055)

classBankFee(FeeStrategy):
defcalc(self, amount: int) -> int:
return2

STRATEGIES = {
"wechat": WechatFee(),
"alipay": AlipayFee(),
"bank": BankFee(),
}

defcalc_fee(channel: str, amount: int) -> int:
try:
return STRATEGIES[channel].calc(amount)
except KeyError:
raise ValueError(f"unknown channel: {channel}")

好处不是“优雅”,是改动范围变小。以后某个渠道要改,你只动一个类,不会顺手把别的分支搞坏。线上这类逻辑,我宁愿多建几个文件,也不想继续养一个万能 utils.py。

2)工厂模式:对象创建别到处散着写

另一个典型坏味道,是各种客户端初始化散落一地:

defsend_message(channel: str, to: str, content: str):
if channel == "sms":
        client = SmsClient(api_key="xxx", timeout=3)
elif channel == "email":
        client = EmailClient(host="smtp.xxx.com", port=465)
else:
raise ValueError("unsupported channel")

    client.send(to, content)

看着没啥,实际很容易失控。配置混在业务里,构造细节到处复制,后面如果要加重试、埋点、熔断,改起来一片。

工厂模式就是专门收这种烂摊子的:

classNotifyFactory:
    @staticmethod
defcreate(channel: str):
if channel == "sms":
return SmsClient(api_key="xxx", timeout=3)
if channel == "email":
return EmailClient(host="smtp.xxx.com", port=465)
raise ValueError(f"unsupported channel: {channel}")

defsend_message(channel: str, to: str, content: str):
    client = NotifyFactory.create(channel)
    client.send(to, content)

再往前走一步,工厂里还可以顺手兜配置校验、实例复用、降级逻辑。你会发现,真正复杂的从来不是 send() 那一行,而是“这个对象到底该怎么造”。既然创建本身会变,那就别散着写。

3)装饰器模式:日志、重试、鉴权,不要污染业务函数

最容易被塞进 utils.py 的,还有一类“横着切”的逻辑。比如重试、耗时统计、异常告警。很多人会这么写:

deffetch_order(order_id: str):
    start = time.time()
try:
for _ in range(3):
try:
return request_order(order_id)
except TimeoutError:
continue
raise TimeoutError("retry failed")
finally:
        cost = int((time.time() - start) * 1000)
        print(f"fetch_order cost={cost}ms")

业务还没展开,脏东西先糊一层。函数越写越重,后面看日志都费劲。

这种场景装饰器就很顺手:

import time
from functools import wraps

defretry(times: int = 3):
defouter(func):
        @wraps(func)
definner(*args, **kwargs):
            last_exc = None
for _ in range(times):
try:
return func(*args, **kwargs)
except TimeoutError as e:
                    last_exc = e
raise last_exc
return inner
return outer

defrecord_cost(func):
    @wraps(func)
definner(*args, **kwargs):
        start = time.time()
try:
return func(*args, **kwargs)
finally:
            cost = int((time.time() - start) * 1000)
            print(f"{func.__name__} cost={cost}ms")
return inner

@record_cost
@retry(times=3)
deffetch_order(order_id: str):
return request_order(order_id)

这样业务函数就只剩业务。以后你想把 print 换成结构化日志,或者把重试异常上报 Prometheus,都不用去翻每个调用点。

说到底,utils.py 最大的问题不是文件名土,而是它默认你“不想建边界”。可项目只要稍微活久一点,边界早晚都得补,晚补一般更疼。

策略模式收变化分支,工厂模式收创建逻辑,装饰器模式收横切能力。这 3 个东西不是什么高深设计,都是写着写着被 bug 教出来的。utils.py 当然不是绝对不能有,但它最好只放那些真正稳定、无状态、不会越长越歪的纯函数。再多,就该拆了。