前言

最近加入了一家初创公司,负责技术方面的工作。在设计 Python 工程架构时,用到了 uv 的 workspace 能力,采用 monorepo 做了各模块的划分,并使用依赖注入来管理资源和依赖。过程中,感觉好像把 Python 写得“复杂了”。这让我想起之前在盛大工作时,有算法同事说过:“为什么 Python 代码写得这么像 Java?我讨厌 Java 的这种写法”。于是就有了这篇文章,想聊聊一些感受。 针对这位同事的问题,我的回答是:像,也不像。而且并非我们刻意让项目复杂,而是一个成熟的产品本身要求项目必须具备一定的复杂度。

为什么是 Python

Python 在诞生之初就是一门脚本语言,并非为大型工程而设计。因此,它在大型项目上的可维护性确实不如一些静态类型语言,无论是其动态性、类型检查的缺失,还是工程管理工具的匮乏,都是由 Python 的初始定位所决定的。

然而,Python 在数据分析与科学计算领域一直备受欢迎。在 AI 时代的推动下,Python 更是热度激增,无论是训练、推理还是应用层,各种框架层出不穷。Python 语法简洁,上手容易,功能开发效率极高(这里指的是开发效率,而非运行时效率)。加之近年来 Rust 生态与 Python 愈发紧密,诸如 uvpyo3 等高效工具也广受好评,为 Python 引入了更多优秀的工具链。

由于团队规模较小,我们不希望技术栈扩散得太厉害。因此,无论是传统后端工程还是算法工程,我们都统一使用 Python,以避免引入过多语言、框架和工具所带来的额外心智负担。

在未来的一段时间内,我们都会以 Python 为基础进行开发与迭代。如果遇到性能瓶颈,我们会尝试使用 Rust 或 Cython 等高性能语言来编写扩展(由于我们的工程代码都要求类型注释,因此也可以直接通过 mypyc 编译来获得额外的性能提升)。

当然,还有另外一个重要原因:我个人对 Python 比较熟悉。当由我负责整体的技术基础时,我自然会倾向于选择自己更有把握的语言。我始终认为这一点非常关键,甚至比语言本身的成熟度更为重要。语言本身并无绝对壁垒,底层原理相通,我们可以轻松学习多种语言、编写各种程序。但对于注重生产和团队协作的商业项目而言,考量点就不仅仅是语言本身,还包括对语言生态的了解程度、对工具链的熟悉程度等等。无论是人类开发还是AI辅助编程,一个深谙此道的“教练”都至关重要。

为什么项目复杂

项目的复杂度是必然结果,无论从产品需求还是团队协作的角度来看都是如此。要么是业务场景变复杂了,要么是参与开发的人员变多了。但如果没有明确的规范,项目在变得复杂的同时也会走向失控。我们还是希望“代码债”来得慢一些,因此在项目中引入了一些规范与设计。这使得项目“看上去”复杂了,但我们的初衷是,当业务逻辑日益复杂时,这套架构能承载代码层面的更多变化,最终保证项目在整体结构上的稳定。本质上,这是通过增强协作范式来减少协作过程中可能引入的混乱(无论来自人类还是AI)。

同时,我们也绝不希望项目从第一天起就在部署和运维层面过于复杂。我们不会一开始就引入微服务、Service Mesh、自动扩缩容等。虽然我们从 Day 0 就采用了 K8S(实际上是 k3s,云原生的优势在于其方案本身是可扩展的),但这并不意味着我们从一开始就要跨语言或采用微服务(目的是降低运维成本,同时为未来预留扩展空间)。这篇博客说得很有道理:You Want Modules, Not Microservices 。因此,我们首先将整个项目划分为多个模块,并按照业务领域进行了简单的边界划定。这既是为了厘清代码结构,也是为未来的微服务化和可扩展性打下基础。在引入任何一个微服务之前,我们都会谨慎权衡其利弊,以评估其必要性。

此外,我也不喜欢将 serverservice 写在一起(目前很多 Python 项目习惯将二者置于同一个 app 下)。毕竟,server 只是 service 的一种承载形式。如有必要,我们可以轻松地将 service 独立出来,通过不同的方式承载(如 Serverless、K8S Job、CLI、MCP 等)。

所以,复杂仅是表象。在项目层面,我们通过模块拆分和代码边界划分来降低协作成本;在系统层面,我们理性运用云原生技术,旨在降低运维成本并保留扩展性,而非盲目追求微服务。

为什么像 Java

我在盛大工作时,曾有算法同事问我:“为什么 Python 代码这么像 Java?我非常讨厌 Java 的这种写法”。

我当时不知如何作答,但我内心比较同意他的后半句话 ☕️。

现在想来,如果有人说 Python 代码像 Java,可能是因为:

  1. 使用 class 而非 module 级别的 function 来承载业务逻辑。
  2. 使用依赖注入来管理资源与依赖。

只要具备这两个特征,Python 代码就很容易让人联想到 Java,或者 Spring (Boot)。

但这终究只是“形似”。我们使用 Class 的初衷与 Java 不同。在 Java 中,Class 是一个强制的、基础的概念;而在 Python 中,我们使用 Class 是出于以下设计考量:

  1. 利用传统 Class 特性:我们希望通过类的继承与重写机制来增强代码的灵活性。但我们通常不会定义接口再提供实现(这在 Java 中很常见),这也是 Python 鸭子类型的体现。
  2. 使用 MetaClass/SubclassHook 进行增强:这种方式比 Monkey Patch 更直观。相比之下,如果只使用 module 级别的函数,要实现类似的功能往往只能依赖 Monkey Patch,这种方式不够直观。在 Java 中,类似能力通常由 cglib 或 ByteBuddy 这类字节码操作库提供(例如用于构建可观测性平面、事务管理等)。
  3. 资源复用与延迟初始化:例如,一个计算 Pipeline 可能需要额外资源(如 Dask / 多进程),类就成了承载这些资源的理想载体。同时,它也避免了在 import 时即初始化资源,从而占用程序启动时间。

而依赖注入(DI)本质上就是为了管理这些资源和实例。如果手动管理这些依赖,会非常繁琐,资源也会重复创建。幸运的是,Python 中实现 DI 的方式更加轻量。不过,由于仍然需要编写相关代码,会给人一种“这难道不是 Java?”的错觉。

总而言之,在 Python 中采用 Class 和 DI 并非跟风,而是基于实际需求进行设计权衡的结果。至于它是否“像 Java”,就见仁见智了。