这是 “Python 工匠”系列的第 9 篇文章。[查看系列全部文章]python
模块(Module)是咱们用来组织 Python 代码的基本单位。不少功能强大的复杂站点,都由成百上千个独立模块共同组成。git
虽然模块有着不可替代的用处,但它有时也会给咱们带来麻烦。好比,当你接手一个新项目后,刚展开项目目录。第一眼就看到了攀枝错节、难以理解的模块结构,那你确定会想: “这项目也太难搞了。” 😂程序员
在这篇文章里,我准备了一个和模块有关的小故事与你分享。github
小 R 是一个刚从学校毕业的计算机专业学生。半个月前,他面试进了一家互联网公司作 Python 开发,负责一个与用户活动积分有关的小项目。项目的主要功能是查询站点活跃用户,并为他们发送有关活动积分的通知: “亲爱的用户,您好,您当前的活动积分为 x”。面试
项目主要由 notify_users.py
脚本和 fancy_site
包组成,结构与各文件内容以下:函数
├── fancy_site
│ ├── __init__.py
│ ├── marketing.py # 与市场活动有关的内容
│ └── users.py # 与用户有关的内容
└── notify_users.py # 脚本:发送积分通知
复制代码
文件 notify_users.py
:工具
from fancy_site.users import list_active_users
from fancy_site.marketing import query_user_points
def main():
"""获取全部的活跃用户,将积分状况发送给他们"""
users = get_active_users()
points = list_user_points(users)
for user in users:
user.add_notification(... ...)
# <... 已省略 ...>
复制代码
文件 fancy_site/users.py
:oop
from typing import List
class User:
# <... 已省略 ...>
def add_notification(self, message: str):
"""为用户发送新通知"""
pass
def list_active_users() -> List[User]:
"""查询全部活跃用户"""
pass
复制代码
文件:fancy_site/marketing.py
:测试
from typing import List
from .users import User
def query_user_points(users: List[User]) -> List[int]:
"""批量查询用户活动积分"""
def send_sms(phone_number: str, message: str):
"""为某手机号发送短信"""
复制代码
只要在项目目录下执行 python notify_user.py
,就能实现给全部活跃用户发送通知。spa
但有一天,产品经理找过来讲,光给用户发站内信通知还不够,容易被用户忽略。除了站内信之外,咱们还须要同时给用户推送一条短信通知。
琢磨了五秒钟后,小 R 跟产品经理说:“这个需求能够作!”。毕竟给手机号发送短信的 send_sms()
函数早就已经有人写好了。他只要先给 add_notification
方法添加一个可选参数 enable_sms=False
,当传值为 True
时调用 fancy_site.marketing
模块里的 send_sms
函数就行。
一切听上去根本没有什么难度可言,十分钟后,小 R 就把 user.py
改为了下面这样:
# 导入 send_sms 模块的发送短信函数
from .marketing import send_sms
class User:
# <...> 相关初始化代码已省略
def add_notification(self, message: str, enable_sms=False):
"""为用户添加新通知"""
if enable_sms:
send_sms(user.mobile_number, ... ...)
复制代码
可是,当他修改完代码,再次执行 notify_users.py
脚本时,程序却报错了:
Traceback (most recent call last):
File "notify_users.py", line 2, in <module>
from fancy_site.users import list_active_users
File .../fancy_site/users.py", line 3, in <module>
from .marketing import send_sms
File ".../fancy_site/marketing.py", line 3, in <module>
from .users import User
ImportError: cannot import name 'User' from 'fancy_site.users' (.../fancy_site/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 语句的执行就行啦。
class User:
# <...> 相关初始化代码已省略
def add_notification(self, message: str, send_sms=False):
"""为用户添加新通知"""
# 延缓 import 语句执行
from .marketing import send_sms
复制代码
改动一行代码后,大功告成。小 R 简单测试后,发现一切正常,而后把代码推送了上去。不太小 R 还没来得及为本身点个赞,意料以外的事情发生了。
这段明明几乎完美的代码改动在 Code Review 的时候被审计人小 C 拒绝了。
小 R 的同事小 C 是一名有着多年经验的 Python 程序员,他对小 R 说:“使用延迟 import,虽然能够立刻解决包导入问题。但这个小问题背后隐藏了更多的信息。好比,你有没有想过 send_sms 函数,是否是已经不适合放在 marketing 模块里了?”
被小 C 这么一问,聪明的小 R 立刻意识到了问题所在。要在 users
模块内发送短信,重点不在于用延迟导入解决环形依赖。而是要以此为契机,发现当前模块间依赖关系的不合理,拆分/合并模块,建立新的分层与抽象,最终消除环形依赖。
认识清楚问题后,他很快提交了新的代码修改。在新代码中,他建立了一个专门负责通知与消息类的工具模块 msg_utils
,而后把 send_sms
函数挪到了里面。以后 users
模块内就能够毫无困难的从 msg_utils
模块中导入 send_sms
函数了。
from .msg_utils import send_sms
复制代码
新的模块依赖关系以下图所示:
在新的模块结构中,整个项目被整齐的分为三层,模块间的依赖关系也变得只有单向流动。以前在函数内部 import
的“延迟导入”技巧,天然也就没有用武之地了。
小 R 修改后的代码得到了你们的承认,很快就被合并到了主分支。故事暂告一段落,那么这个故事告诉了咱们什么道理呢?
模块间的循环依赖是一个在大型 Python 项目中很常见的问题,越复杂的项目越容易碰到这个问题。当咱们在参与这些项目时,若是对模块结构、分层、抽象缺乏应有的重视。那么项目很容易就会慢慢变得复杂无比、难以维护。
因此,合理的模块结构与分层很是重要。它能够大大下降开发人员的心智负担和项目维护成本。这也是我为何要和你分享这个简单故事的缘由。“在函数内延迟 import” 的作法固然没有错,但咱们更应该关注的是:整个项目内的模块依赖关系与分层是否合理。
最后,让咱们再尝试从 小 R 的故事里强行总结出几个道理吧:
看完文章的你,有没有什么想吐槽的?请留言或者在 项目 Github Issues 告诉我吧。
系列其余文章: