从2018年正式用Python做项目到现在,算下来也有快八年时间了。这中间用过Flask写小工具,用Django搭过完整的业务后台,用Celery做过异步任务调度,也用Pandas和NumPy处理过数据分析的需求。不能说精通,但确实在这个语言上花了不少功夫。
写这篇文章,不是想讲什么高深的理论,就是想把这些年实际开发中遇到的坑、总结的经验整理一下。如果能帮到正在学Python或者刚入行的朋友,那就最好了。
关于GIL,别太焦虑也别无视
刚用Python做Web服务的时候,最怕别人问我:“Python有GIL,性能是不是不行?”
说实话,GIL确实是CPython的一个限制,但它并没有那么可怕。对于Web应用来说,大部分时间花在I/O等待上——数据库查询、外部API调用、文件读写,这些操作都会释放GIL,所以多线程在处理I/O密集型任务时并没有想象中那么不堪。
真正要注意的是CPU密集型场景。我们曾经有一个图像处理的模块,用Python做了大量的像素遍历和矩阵计算,跑起来确实很慢。后来换了两种方案——把计算部分用NumPy向量化实现,性能提升了十几倍;另一个更彻底的方案是用multiprocessing启动多个进程,每个进程处理一批数据,充分利用多核CPU。
所以我的感受是:先搞清楚你的场景是什么类型的任务,再决定要不要为GIL焦虑。大多数业务系统,GIL不是瓶颈;如果真的遇到了,总归有办法绕过去。
装饰器用得好,代码少一半
Python的装饰器是我用得最多的语言特性之一,也是让代码最优雅的工具。
以前写Flask接口的时候,每个视图函数都要写一堆重复的代码——参数校验、权限检查、日志记录、异常捕获。最开始是每个函数里手写,后来发现太冗余,就抽成了装饰器。
举个例子,我们当时有个需求:所有接口都要校验请求里的token,并且把解析出来的用户信息挂到上下文中。写了一个@login_required装饰器,往视图函数上一加,所有鉴权逻辑就统一了。代码清爽了很多,也减少了遗漏校验的风险。
还有计时装饰器,用来统计接口耗时;重试装饰器,用来处理外部API偶尔的超时;缓存装饰器,给某些计算密集的结果加一层本地缓存。
装饰器的核心价值在于分离关注点。业务逻辑就是业务逻辑,不要掺和鉴权、日志、性能统计这些横切关注点。当然也别滥用,三层以上的装饰器嵌套,可读性就会急剧下降。
生成器和迭代器带来的内存解放
有一回处理日志文件,单个文件有10个G,用readlines()一次性读进来,内存直接爆了。后来改成逐行读取:
python
with open('large.log', 'r') as f: for line in f: process(line)for line in f本质上就是在用迭代器,每次只读一行到内存,问题迎刃而解。
后来写数据处理管道的时候,我习惯用生成器函数把每个处理步骤串起来。比如读数据 -> 过滤 -> 转换 -> 聚合,每一步都是一个生成器,数据像流水一样流过整个管道。这样做的好处是内存占用恒定,而且每一步都可以独立测试和复用。
如果你在写一个需要处理大量数据的程序,优先考虑用生成器来惰性求值,而不是一次性加载全部数据。
上下文管理器的魔力
with open()大概是所有人最早接触的上下文管理器。但除了文件操作,它还能干很多事情。
我们项目里用Redis做缓存,每次操作完要释放连接。最开始总是在try...finally里写释放逻辑,代码很丑。后来自定义了一个上下文管理器:
python
@contextmanager def redis_client(): client = create_redis_client() try: yield client finally: client.close()
用起来就变成了:
python
with redis_client() as client: client.set('key', 'value')连接自动管理,再也不用担心忘记释放了。数据库事务也可以用同样的方式封装,with transaction()自动处理commit和rollback。
这个小工具让代码的健壮性和可读性都提升了不少。
异常处理不要沉默
新人常犯的一个错误是:
python
try: do_something() except Exception: pass
这种写法把异常悄无声息地吞掉了。一旦出问题,查都不知道从哪查起。
更好的做法是:
python
try: do_something() except SpecificError as e: logger.error(f"处理失败: {e}", exc_info=True) # 要么重新抛出,要么返回一个合理的默认值原则有三个:只捕获你预期会发生的异常类型;记录足够的上下文信息;不要让程序在未知状态下继续执行。
我们线上有一次严重的事故,就是一个空值的except Exception把关键的报错信息吞了,导致问题被掩盖了整整两天才发现。从那以后,Code Review对异常处理格外严格。
类型注解带来的变化
Python 3.5引入类型注解的时候,我一开始是抵触的——写Python不就图个灵活嘛,加类型注解不是自缚手脚?
后来团队扩大到十几个人,协作成本变高了,经常出现“这个函数到底要传什么类型、返回什么类型”的问题。文档写了也没人看,不如用类型注解把接口契约写在代码里。
再配合上Pydantic做数据校验,用Mypy做静态检查,很多低级错误在运行前就能发现。尤其是重构的时候,改了某个函数的签名,Mypy会把所有调用处标出来,省了很多排查时间。
类型注解不是让你写Java风格的Python,而是让代码更可读、更可靠。要不要用完全取决于项目规模和团队协作需求,小脚本没必要硬上。
用好标准库
Python的标准库真的很强大,很多时候不用急着上第三方包。
collections.defaultdict和Counter,让字典操作更简洁itertools里的各种迭代器工具,处理数据流非常高效functools.lru_cache,一句话给函数加上缓存dataclasses,省去写大量__init__的样板代码pathlib,路径操作比os.path优雅太多了
花了点时间把标准库过一遍,你会发现很多日常需求其实都有现成的解决方案,代码更稳定,也减少了依赖。
写测试的习惯
以前觉得写测试是浪费时间,直到有一次上线前改了一行代码,导致一个很边缘的逻辑出了bug,线上跑了三天才被发现,损失了不少数据。
后来强制要求每个新功能必须带测试,核心模块的覆盖率不低于80%。用pytest写起来其实很快,再加上pytest-cov看覆盖率,心里踏实很多。
CI/CD流水线里把测试和覆盖率检查加上,覆盖率不达标不允许合并。标准定下来之后,线上故障率确实肉眼可见地下降了。
最后想说的话
Python是一门很好上手的语言,但上手快和用好是两码事。这些年我越来越觉得,语言本身只占20%,剩下的80%是对业务的理解、对工程化的坚持、以及对代码可维护性的追求。
如果你刚入门,不用急着学各种花哨的框架,先把基础打牢——搞懂可变对象和不可变对象的区别、弄明白浅拷贝和深拷贝、搞清楚is和==的差异。这些细节看似琐碎,但往往是线上bug的根源。
写代码是为了解决问题,不是为了炫技。优雅、简洁、可读、可靠,这些才是一段好代码应该具备的品质。
共勉。