重识 Python

·

3 min read

我与 python 的渊源

记得当年 python 是自学的,至今还记得入门的那本书的名字叫做《a byte of python》

2014年入行时即写 python,后端 djang 应用,当时 python 版本还是2.7,现在都进化到 3.12了,变化不可谓不大。印象中最深刻的地方(有记忆)在于 python2.7 的 print 支持以下两种写法:

Python 2.7.18 (default, Apr 20 2020, 19:34:11)
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> print("hello")
hello
>>> print "hello"
hello

而在 python3.x 中,仅支持以下一种

Python 3.9.18 (main, Aug 24 2023, 18:16:58)
[Clang 15.0.0 (clang-1500.0.40.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> print("hello")
hello
>>> print "hello"
  File "<stdin>", line 1
    print "hello"
          ^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print("hello")?
>>>

后面的工作技术栈转向了 golang 和云原生相关,2019年因为工作的关系,接触过 appium,写过不到半年的 python,后面能用到 python 的地方只有处理一些字符串、表格等临时的活儿……

人生苦短,我选 python

最近公司人员变动有点大,组里的项目也不太行的样子,在此大背景下,我被派去支持隔壁 AI 项目组的一个 poc:

  1. 拿到了一个 java 写的服务 repo (项目开发人员即将于两天后离职)

  2. 一个阿里云服务器 ip、用户名、密码,用来部署 1 中的 java 服务

  3. 被拉进若干群

  4. TL给出的 10 分钟不到需求介绍

  5. 工作日每天下午 5 点半同步进度

  6. 有一位博士负责大模型调优

  7. deadline 为5月底

这就是全部,彼时,我手头还有其它项目支持的事情,说焦头烂额有点过,但也相差不多。虽然时不时还有其它人或事会打断我,但总归是有相对整块的时间来看遗留下来的 java 代码,梳理了一下这个 java 服务

  1. 提供了一个 post 接口,使用 SSE 模式, 用于客户方调用 ,接口有签名检验,但是代码中签名检验部分直接放过了……

  2. 调用了客户的一个 post 接口,接口同样要做签名

  3. 调用了百度千帆的 API 接口

功能还是比较简单的,困难的地方在签名规则的实现,没有详细的的文档参考;然后调用百度千帆的 API 接口这事吧,有点七弯八绕的:为啥不直接用 sdk? 问了下当时的开发,答曰:做这个项目的时候, 千帆的 java sdk 功能不全。通过翻阅官方文档发现:千帆的一众 sdk 中,对 python 的支持度是最好的。这也难怪,都要做 AI 了,没理由不用 python 啊,此时已经萌生了用 python 重写这个服务的想法。因为博士大模型调优的代码使用 python ,而我需要调用博士调优后的大模型的代码,这下没理由不用 python 了。在某天的工作进度同步会上,提出了这个设想,一帮指挥官直接说:可以啊,用啥不重要,能实现就行……

python 实现

python 版本选择

首先必须是3.x,3.12 都发布一段时间了,本着用新不用旧的原则,那就 3.12 吧(虽然我知道千帆的 python sdk 的版本是 3.9)。后面由于博士的代码是基于 python 3.9 开发的,我调用他的代码时出现了奇怪的包依赖错误,为了减少不必要的麻烦,老实切换到了 3.9, 然后就好了……

服务端框架选型

  • django: 以前写过,对于这个需求而言,django 显得略微有些臃肿

  • fastapi:在推上,fastapi 的出镜率很高,据说很强、很快

  • webpy、flask、bottle……听过但没咋写过的据说很轻巧的框架

如此随意,选了 fastapi,根据官方文档快速启动了,对 swagger 的支持很棒,这是我最喜欢的特性之一

依赖管理

  • requirements.txt: 当前我用的还是这个

  • pdm: 这个在我的信息流里面出现的频率也很高

pdm 很新很流行,但我在容器化部署时遇到一些奇奇怪怪的问题,彼时也没有耐心细细读文档,最后还是用了 requirements.txt 这种方式

静态检查

Ruff 的大名已经听说过很多次了

An extremely fast Python linter and code formatter, written in Rust.

主打一个快,用 Rust 编写,就是它了。

java -> python

这个时代有了各种免费的 AI 真好,将 java 函数扔给 AI 转成对应的 python 实现就好了,虽然代码有点丑陋,但是,能用。于是在 AI 的加持下,快速糊出了一个调用客户的接口及其签名实现,从这块开始的考虑很简单:

  • 因为是调用客户的接口,便于测试

  • 接口签名规则和算法是一致的,这个接口通了,则提供给客户的接口也就没问题了

写接口测试用例,使用 unittest 一顿操作猛如虎,写了三个 test case。

# test_push.py
import unittest
​
class TestPush(unittest.TestCase):
    def test_answer(self):
        ……
        self.assertEqual(push(input), 200)

执行

python3.9 -m unittest -v test_push

讲真,跑测试用例的感觉是真爽,虽然中间遇到一些问题,但都一一解决了。

于是找客户那边的对接开发索要了几个接口入参样例,先自测了签名验证的接口。不得不说,客户那边对接的开发同学蛮好的,给我提供了很多帮助,最终顺利完成了两个接口的本地自测。

类型恐惧

我是先用 python2.7,然后用 go,现在又写 python,我发现没有类型检查,内心还是有些不安的,比如以下代码

from pydantic import BaseModel
​
class ExampleModel(BaseModel):
    one: str
    two: int
​
if __name__ == "__main__":
    # two 字段的类型为 int
    em = ExampleModel(one='1', two='2')
    print(em.two)

竟然能正常输出:2

再看一个例子

from pydantic import BaseModel
​
class ExampleModel(BaseModel):
    one: str
    two: int
​
if __name__ == "__main__":
    # 我根本没有定义 three 字段
    em = ExampleModel(one='1', two='2', three=True)
    print(em.two)

还是能正常输出:2

毕竟,one: str 这些只是类型注解,不会在运行时执行数据验证或解析。记忆中我还是用过 python,然而,现在又感觉似乎完全不认识一样。如此情境,我只能增加测试用例,加强静态检查以及自测来补足隐藏的没有执行过的 python 代码。

fastapi 的优点

接口和入参定义真的做的蛮好

class RequestBody(BaseModel):
    app_id: str
    request_id: str

@app.post("/query")
async def example(
    body: RequestBody,
    timestamp: Union[str, None] = Header(alias='X-Signature-Timestamp', default=None),
):

header 和 body 的表达很简洁,当然,对swagger的支持也相当友好

fastapi 容器化

到这个环节,真切体会到了 go 的优势:编译后将可执行文件扔到镜像中即可(虽然在推上看过搞编译器的同学对 go 颇有微词)。来看看 fastapi 的 Dockerfile ,官网示例如下:


# 
FROM python:3.9

# 
WORKDIR /code

# 
COPY ./requirements.txt /code/requirements.txt

# 
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# 
COPY ./app /code/app

# 
CMD ["fastapi", "run", "app/main.py", "--port", "80"]

如果是本地打镜像,记得加上 .dockerignore 文件

**/__pycache__/
*.pyc
*.pyo
*.pyd

就笔者的需求而言,感观上容器化不如使用 python 虚拟环境,因为要把代码 py 文件加到镜像中让人挺难受。长期习惯了 go 应用轻巧的镜像, python 打出来的镜像动辄就 GB 级,属实有点难以接受了。

总结

python,作为我步入职场的初恋编程语言,由于自己长期的冷落,突然对其产生出一种陌生的感觉,总归还是动手少了。python 一直在持续迭代进步,而我已经固步自封了,有一种悲凉之感。不管怎么说,学习还是让人快乐的,要做到真正的终身学习,学以致用,并不简单。突然想起之前带我的一个 TL 对我说的一句话:要勇于把手弄脏,共勉。