Python技术迷

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

别说,你们那个项目里现在是不是也有个 utils.py,点开一看一千多行,谁改谁骂娘那种? 我前几天刚帮一个同事救火,线上一个小改动,结果翻半天才发现逻辑藏在一个叫 do_something_very_important 的工具函数里,神仙也看不懂干嘛的,跟我之前被 SpringBoot 默认配置坑了一样,问题本身不复杂,恶心的是排查的过程。

我那天一冲动,就把他那个 utils.py 给拆了,顺手用了三个设计模式,拆完的感觉就一个词:人话。跟你们唠唠是咋整的。


场景是这样的哈:一个老项目,有一堆“导出数据”的需求,什么导出 Excel、导出 CSV、导出 JSON,全塞在 utils.py 里:

# utils.py 里原来的鬼样子

defexport_excel(data, file_path):
# 各种格式化、各种 if
    ...

defexport_csv(data, file_path):
    ...

defexport_json(data, file_path):
    ...

defexport_data(format_type, data, file_path):
if format_type == "excel":
return export_excel(data, file_path)
elif format_type == "csv":
return export_csv(data, file_path)
elif format_type == "json":
return export_json(data, file_path)
else:
raise ValueError("不支持的格式")

你们看问题在哪儿? 一堆函数,谁都能调,哪里都能调,逻辑一改要全项目搜 "export_",改完还担心别的地方炸。典型“万能工具人”,干活多,背锅也多。

我先动的第一个刀,就是策略模式。


策略模式这个东西,别被书上那些画风吓到,其实就是:把“怎么导出”的那部分逻辑,拆成一个个可以随便换的对象,而不是一个大 if-else。那天我边改代码边骂街,大概是这样改的:

# export_strategies.py

from abc import ABC, abstractmethod

classExportStrategy(ABC):
    @abstractmethod
defexport(self, data, file_path: str) -> None:
        ...

classExcelExportStrategy(ExportStrategy):
defexport(self, data, file_path: str) -> None:
# 这里写真正的 Excel 导出逻辑
        print(f"[excel] 写入 {file_path}")

classCsvExportStrategy(ExportStrategy):
defexport(self, data, file_path: str) -> None:
        print(f"[csv] 写入 {file_path}")

classJsonExportStrategy(ExportStrategy):
defexport(self, data, file_path: str) -> None:
        print(f"[json] 写入 {file_path}")

然后原来那个 export_data 我直接换成一个小小的“策略选择器”,再也不写 if-elif-else 火车了:

# exporter.py

from export_strategies import (
    ExcelExportStrategy,
    CsvExportStrategy,
    JsonExportStrategy,
    ExportStrategy,
)

_STRATEGY_MAP: dict[str, ExportStrategy] = {
"excel": ExcelExportStrategy(),
"csv": CsvExportStrategy(),
"json": JsonExportStrategy(),
}

defexport_data(format_type: str, data, file_path: str) -> None:
try:
        strategy = _STRATEGY_MAP[format_type]
except KeyError:
raise ValueError(f"不支持的格式:{format_type}")
    strategy.export(data, file_path)

这样一整,几个好处你们自己感受一下:

  • 新增 XML 导出?加个 XmlExportStrategy,然后 _STRATEGY_MAP["xml"] = XmlExportStrategy() 完事;
  • 老项目里一堆散装 if-else 不见了,谁想搞导出,就老老实实走 export_data;
  • 单测的时候,我可以直接 new 一个 FakeExportStrategy,不用去真的写文件。

这就是第一个模式:策略模式,我一般就当“可插拔 if-else”用。


第二个我动手拆的是一坨“解析配置”的工具函数。原来 utils 里有这么个东西:

# utils.py 里的另一个大家伙

defparse_config(config: dict):
# 这个函数长得比 early access 的小说还长
    ...

任何地方想要从配置里算点东西,全调用它。问题是你根本不知道它内部到底依赖了啥,全局状态一大堆,改一次就通盘紧张。

我这次直接上工厂模式 + 小对象,也别搞教科书那种神乎其神的,简单粗暴一点:

# config_readers.py

classDbConfig:
def__init__(self, url: str, pool_size: int) -> None:
        self.url = url
        self.pool_size = pool_size

classCacheConfig:
def__init__(self, host: str, ttl: int) -> None:
        self.host = host
        self.ttl = ttl

classConfigFactory:
    @staticmethod
defcreate_db_config(raw: dict) -> DbConfig:
# 这里可以做校验、默认值之类
return DbConfig(
            url=raw["DB_URL"],
            pool_size=int(raw.get("DB_POOL_SIZE", 10)),
        )

    @staticmethod
defcreate_cache_config(raw: dict) -> CacheConfig:
return CacheConfig(
            host=raw["CACHE_HOST"],
            ttl=int(raw.get("CACHE_TTL", 60)),
        )

业务代码就很干净了,不再去碰什么 utils.parse_config 那个黑盒子:

# somewhere in service

from config_readers import ConfigFactory

definit_app(settings: dict):
    db_config = ConfigFactory.create_db_config(settings)
    cache_config = ConfigFactory.create_cache_config(settings)

    print("db:", db_config.url, db_config.pool_size)
    print("cache:", cache_config.host, cache_config.ttl)

为啥这里我要用点“工厂”的意思?就一个感觉:你让别人 new 对象的时候,别让他想太多。

以前大家是这样的:

db_url = settings["DB_URL"]
pool = int(settings.get("DB_POOL_SIZE", 10))
# 复制粘贴出十份

现在只要记住一句:“要 DB 配置?找工厂要。” 到这一步,其实 utils.py 里那些“跟配置相关”的东西,已经被迁走一半了。


第三个,我是真受不了他们项目的“接口调用工具”,你们肯定有类似的:

# utils.py

defhttp_get(url, headers=None, timeout=3):
    ...

defhttp_post(url, data=None, headers=None, timeout=3):
    ...

defcall_xxx_system(payload):
# 这里面各种拼 URL,各种加签名,各种埋点
    ...

这个一看就注定变垃圾场:各种系统都往里加“call_某某_system”,最后变成“跨团队公共坟场”。

我这次用了一个稍微高级点的:外观模式 + 适配器那味儿。但你别管名词,思路就一句话:给每个外部系统一个专门的客户端类,再搞一个统一入口。

比如我给“支付系统”单独弄了个 client:

# clients/payment_client.py

import requests

classPaymentClient:
def__init__(self, base_url: str, token: str) -> None:
        self._base_url = base_url.rstrip("/")
        self._token = token

def_headers(self) -> dict:
return {
"Authorization": f"Bearer {self._token}",
        }

defcreate_order(self, amount: int, user_id: str) -> dict:
        resp = requests.post(
f"{self._base_url}/orders",
            json={"amount": amount, "user_id": user_id},
            headers=self._headers(),
            timeout=3,
        )
        resp.raise_for_status()
return resp.json()

defquery_order(self, order_id: str) -> dict:
        resp = requests.get(
f"{self._base_url}/orders/{order_id}",
            headers=self._headers(),
            timeout=3,
        )
        resp.raise_for_status()
return resp.json()

再做一个“门面”把所有外部系统的 client 聚在一起:

# gateway.py

from clients.payment_client import PaymentClient
# from clients.sms_client import SmsClient  # 假装还有一堆

classExternalGateway:
def__init__(self, settings: dict) -> None:
        self.payment = PaymentClient(
            base_url=settings["PAYMENT_URL"],
            token=settings["PAYMENT_TOKEN"],
        )
# self.sms = SmsClient(...)

# 用的时候

gateway = ExternalGateway(settings)

defpay(user_id: str, amount: int):
    order = gateway.payment.create_order(amount=amount, user_id=user_id)
return order["id"]

你看现在那个 utils.py 里原来那堆 http_get/http_post/call_* 啥的,就只剩下一些真正通用的小工具可以留着,比如签名算法、重试装饰器之类的。其他跟具体“业务系统”绑定的东西,都该滚去自己的 client 文件里。

外观模式的感觉就是:我给你一扇门,门后面多乱你别管,你只认这把门就行。


整个拆完,我做了个统计,原来 utils.py 有 1800 多行,改完不到 200 行,还都是那种“小而纯”的函数,比如:

defretry(times=3, exceptions=(Exception,)):
defdecorator(fn):
defwrapper(*args, **kwargs):
            last = None
for _ in range(times):
try:
return fn(*args, **kwargs)
except exceptions as e:
                    last = e
raise last
return wrapper
return decorator

这种我觉得留在 utils 里就挺正常的:跟业务毫无关系,放哪儿都差不多,那就集中在一个“工具箱”里。 真正要干活的东西——导出策略、配置工厂、外部系统客户端——都已经回归“各自的模块”。

所以你要真问一句:我到现在为止还会不会写 utils.py?会的,但是会特别克制:

  • 跟业务名词沾边的,坚决不放 utils;
  • 要扩展的逻辑,优先想“能不能变成一个策略或者工厂”;
  • 外部依赖多、又容易变的,干脆包成一个客户端,再用个网关把它藏起来。

反正那天改完我跟同事说: 你以后要是再往 utils 里塞东西,我就把你电脑 utils.py 给你 git rm 掉,真说的。

算了不说了,我去喝口水,你们先看看自己项目里那个 utils 吧…