Python 工匠:一个关于模块的小故事
前言

模块(Module)是我们用来组织 Python 代码的基本单位。很多功能强大的复杂站点,都由成百上千个独立模块共同组成。
虽然模块有着不可替代的用处,但它有时也会给我们带来麻烦。比如,当你接手一个新项目后,刚展开项目目录。第一眼就看到了攀枝错节、难以理解的模块结构,那你肯定会想:“这项目也太难搞了。”
在这篇文章里,我准备了一个和模块有关的小故事与你分享。
一个关于模块的小故事
小 R 是一个刚从学校毕业的计算机专业学生。半个月前,他面试进了一家互联网公司做 Python 开发,负责一个与用户活动积分有关的小项目。项目的主要功能是查询站点活跃用户,并为他们发送有关活动积分的通知:“亲爱的用户,您好,您当前的活动积分为 x”。
项目主要由 notify_users.py 脚本和 fancy_site 包组成,结构与各文件内容如下:

文件 notify_users.py:

文件 fancy_site/users.py:

文件:fancy_site/marketing.py:

只要在项目目录下执行 python notify_user.py,就能实现给所有活跃用户发送通知。
需求变更
但有一天,产品经理找过来说,光给用户发站内信通知还不够,容易被用户忽略。除了站内信以外,我们还需要同时给用户推送一条短信通知。
琢磨了五秒钟后,小 R 跟产品经理说:“这个需求可以做!”。毕竟给手机号发送短信的 send_sms() 函数早就已经有人写好了。他只要先给 add_notification 方法添加一个可选参数 enable_sms=False,当传值为 True 时调用 fancy_site.marketing 模块里的 send_sms 函数就行。
一切听上去根本没有什么难度可言,十分钟后,小 R 就把 user.py 改成了下面这样:

但是,当他修改完代码,再次执行 notify_users.py 脚本时,程序却报错了:

错误信息说,无法从 fancy_site.users 模块导入 User 对象。
解决环形依赖问题
小 R 仔细分析了一下错误,发现错误是因为 users 与 marketing 模块之间产生的环形依赖关系导致的。
当程序在 notify_users.py 文件导入 fancy_site.users 模块时, users 模块发现自己需要从 marketing 模块那里导入 send_sms 函数。而解释器在加载 marketing 模块的过程中,又反过来发现自己需要依赖 users 模块里面的 User 对象。
如此一来,整个模块依赖关系成为了环状,程序自然也就没法执行下去了。

不过,没有什么问题能够难倒一个可以正常访问 Google 的程序员。小 R 随便上网一搜,发现这样的问题很好解决。因为 Python 的 import 语句非常灵活,他只需要 把在 users 模块内导入 send_sms 函数的语句挪到 add_notification 方法内,延缓 import 语句的执行就行啦。

改动一行代码后,大功告成。小 R 简单测试后,发现一切正常,然后把代码推送了上去。不过小 R 还没来得及为自己点个赞,意料之外的事情发生了。
这段明明几乎完美的代码改动在 Code Review 的时候被审计人小 C 拒绝了。
小 C 的疑问
小 R 的同事小 C 是一名有着多年经验的 Python 程序员,他对小 R 说:“使用延迟 import,虽然可以马上解决包导入问题。但这个小问题背后隐藏了更多的信息。比如,你有没有想过 send_sms 函数,是不是已经不适合放在 marketing 模块里了?”
被小 C 这么一问,聪明的小 R 马上意识到了问题所在。要在 users 模块内发送短信,重点不在于用延迟导入解决环形依赖。而是要以此为契机,发现当前模块间依赖关系的不合理,拆分/合并模块,创建新的分层与抽象,最终消除环形依赖。
认识清楚问题后,他很快提交了新的代码修改。在新代码中,他创建了一个专门负责通知与消息类的工具模块 msg_utils,然后把 send_sms 函数挪到了里面。之后 users 模块内就可以毫无困难的从 msg_utils 模块中导入 send_sms 函数了

新的模块依赖关系如下图所示:

在新的模块结构中,整个项目被整齐的分为三层,模块间的依赖关系也变得只有单向流动。之前在函数内部 import 的“延迟导入”技巧,自然也就没有用武之地了。
小 R 修改后的代码获得了大家的认可,很快就被合并到了主分支。故事暂告一段落,那么这个故事告诉了我们什么道理呢?
总结
模块间的循环依赖是一个在大型 Python 项目中很常见的问题,越复杂的项目越容易碰到这个问题。当我们在参与这些项目时,如果对模块结构、分层、抽象缺少应有的重视。那么项目很容易就会慢慢变得复杂无比、难以维护。
所以,合理的模块结构与分层非常重要。它可以大大降低开发人员的心智负担和项目维护成本。这也是我为什么要和你分享这个简单故事的原因。“在函数内延迟 import” 的做法当然没有错,但我们更应该关注的是:整个项目内的模块依赖关系与分层是否合理。
最后,让我们再尝试从 小 R 的故事里强行总结出几个道理吧:
- 合理的模块结构与分层可以降低项目的开发维护成本
- 合理的模块结构不是一成不变的,应该随着项目发展调整
- 遇到问题时,不要选“简单但有缺陷”的那个方案,要选“麻烦但正确”的那个
- 整个项目内的模块间依赖关系流向,应该是单向的,不能有环形依赖存在
附录
- 题图来源: Photo by Ricardo Gomez Angel on Unsplash
- 更多系列文章地址:https://github.com/piglei/one-python-craftsman
系列其他文章:
Python 工匠:让函数返回结果的技巧
Python 工匠:异常处理的三个好习惯
Python 工匠:编写地道循环的两个建议
蓝鲸智云
本文由腾讯蓝鲸智云编辑发布,腾讯蓝鲸智云(简称蓝鲸)软件体系是一套基于PaaS的技术解决方案,致力于打造行业领先的一站式自动化运维平台。目前已经推出社区版、企业版,欢迎体验。
- 官网:https://bk.tencent.com/
- 下载链接:https://bk.tencent.com/download/
- 社区:https://bk.tencent.com/s-mart/community/question
Python 工匠:一个关于模块的小故事的更多相关文章
- 一听就懂:用Python做一个超简单的小游戏
写它会用到 while 循环random 模块if 语句输入输出函数
- python 搭建一个http服务的小例子
一.创建Server 1.Dos 命令 python -m BaseHTTPServer [port] 默认端口是8000, 2.Python 脚本启动 #coding:utf-8 ''' Creat ...
- [Python] Python工匠(Github)
1.善用变量来改变代码质量 变量命名 变量要有描述性,不能太宽泛 BAD:day, host, cards, temp GOOD:day_of_week, hosts_to_reboot, expir ...
- python学习笔记_week5_模块
模块 一.定义: 模块:用来从逻辑上组织python代码(变量,函数,类,逻辑:实现一个功能), 本质就是.py结尾的python文件(文件名:test.py,对应模块名:test) 包:用来从逻辑上 ...
- python函数和常用模块(三),Day5
递归 反射 os模块 sys模块 hashlib加密模块 正则表达式 反射 python中的反射功能是由以下四个内置函数提供:hasattr.getattr.setattr.delattr,改四个函数 ...
- Python基础之--常用模块
Python 模块 为了实现对程序特定功能的调用和存储,人们将代码封装起来,可以供其他程序调用,可以称之为模块. 如:os 是系统相关的模块:file是文件操作相关的模块:sys是访问python解释 ...
- Python自动化之常用模块
1 time和datetime模块 #_*_coding:utf-8_*_ __author__ = 'Alex Li' import time # print(time.clock()) #返回处理 ...
- Python学习笔记-常用模块
1.python模块 如果你退出 Python 解释器并重新进入,你做的任何定义(变量和方法)都会丢失.因此,如果你想要编写一些更大的程序,为准备解释器输入使用一个文本编辑器会更好,并以那个文件替代作 ...
- Day5 - Python基础5 常用模块学习
Python 之路 Day5 - 常用模块学习 本节大纲: 模块介绍 time &datetime模块 random os sys shutil json & picle shel ...
随机推荐
- 用C语言实现井字棋(人人/AI人机)--完结版
目录 用C语言实现井字棋(人人/AI人机)--完结版 BUG与优化3: 1. 修改了step的计算方法,每个玩家玩完就加一次step 2. 改变了电脑下棋的逻辑,每个玩家玩完之后都跳过这次循环 源码: ...
- Kafka 的稳定性
一.事务 1. 事务简介 1.1 事务场景 producer发的多条消息组成⼀个事务这些消息需要对consumer同时可⻅或者同时不可⻅ producer可能会给多个topic,多个partition ...
- Java基础-并发篇
3.1. JAVA 并发知识库 3.2. JAVA 线程实现/创建方式 3.2.1. 继承 Thread 类 Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例. ...
- 在.NET 6.0上使用Kestrel配置和自定义HTTPS
大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进. 本章是<定制ASP NET 6.0框架系列文章>的第四篇.在本章,我们 ...
- 使用Kind快速构建k8s
什么是 KindKind(Kubernetes in Docker) 是一个 Kubernetes 孵化项目,Kind 是一套开箱即用的 Kubernetes 环境搭建方案.顾名思义,就是将 Kube ...
- 内存分析器 (MAT)
内存分析器 (MAT) 1. 内存分析器 (MAT) 1.1 MAT介绍 MAT是Memory Analyzer tool的缩写.指分析工具. 1.2 MAT作用 Eclipse Memory ...
- 爬虫(9) - Scrapy框架(1) | Scrapy 异步网络爬虫框架
什么是Scrapy 基于Twisted的异步处理框架 纯python实现的爬虫框架 基本结构:5+2框架,5个组件,2个中间件 5个组件: Scrapy Engine:引擎,负责其他部件通信 进行信号 ...
- Mybatis SqlNode源码解析
1.ForEachSqlNode mybatis的foreach标签可以将列表.数组中的元素拼接起来,中间可以指定分隔符separator <select id="getByUserI ...
- 各种Git Bash乱码解决
乱码情景一: 当使用git log 出现乱码时,修改 %GIT_HOME%\etc\gitconfig 文件,加入如下内容: [gui] encoding = utf-8[i18n] commiten ...
- APISpace 周公解梦API接口 免费好用
<周公解梦>,是根据人的梦来卜吉凶的一本解梦书籍,它对人的七类梦境进行解述. 周公解梦API,周公解梦大全,周公解梦查询,免费周公解梦. APISpace 有很多免费通用的API接 ...