别院牧志知识库 别院牧志知识库
首页
  • 基础

    • 全栈之路
    • 😎Awesome资源
  • 进阶

    • Python 工匠系列
    • 高阶知识点
  • 指南教程

    • Socket 编程
    • 异步编程
    • PEP 系列
  • 面试

    • Python 面试题
    • 2022 面试记录
    • 2021 面试记录
    • 2020 面试记录
    • 2019 面试记录
    • 数据库索引原理
  • 基金

    • 基金知识
    • 基金经理
  • 细读经典

    • 德隆-三个知道
    • 孔曼子-摊大饼理论
    • 配置者说-躺赢之路
    • 资水-建立自己的投资体系
    • 反脆弱
  • Git 参考手册
  • 提问的智慧
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
首页
  • 基础

    • 全栈之路
    • 😎Awesome资源
  • 进阶

    • Python 工匠系列
    • 高阶知识点
  • 指南教程

    • Socket 编程
    • 异步编程
    • PEP 系列
  • 面试

    • Python 面试题
    • 2022 面试记录
    • 2021 面试记录
    • 2020 面试记录
    • 2019 面试记录
    • 数据库索引原理
  • 基金

    • 基金知识
    • 基金经理
  • 细读经典

    • 德隆-三个知道
    • 孔曼子-摊大饼理论
    • 配置者说-躺赢之路
    • 资水-建立自己的投资体系
    • 反脆弱
  • Git 参考手册
  • 提问的智慧
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 辨析

  • Sockets编程

  • Django

  • stackoverflow

  • Flask

  • 全栈之路

  • 面试

  • 代码片段

  • 异步编程

  • 😎Awesome资源

  • PEP

  • Python工匠系列

    • 『Python 工匠』是什么?
    • Python 工匠:善用变量来改善代码质量
    • Python 工匠:编写条件分支代码的技巧
    • Python 工匠:使用数字与字符串的技巧
    • Python 工匠:容器的门道
    • Python 工匠:让函数返回结果的技巧
    • Python 工匠: 异常处理的三个好习惯
    • Python 工匠:编写地道循环的两个建议
    • Python 工匠:使用装饰器的技巧
    • Python 工匠:一个关于模块的小故事
      • 前言
      • 一个关于模块的小故事
        • 需求变更
        • 解决环形依赖问题
        • 小 C 的疑问
      • 总结
      • 附录
    • Python 工匠:做一个精通规则的玩家
    • Python 工匠:高效操作文件的三个建议
    • Python 工匠:写好面向对象代码的原则(上)
    • Python 工匠:写好面向对象代码的原则(中)
    • Python 工匠:写好面向对象代码的原则(下)
    • Python 工匠:在边界处思考
  • 高阶知识点

  • Python 学习资源待整理
  • 设计模式

  • 好“艹蛋”的 Python 呀!
  • FIFO | 待学清单📝
  • pip 安装及使用
  • 数据分析

  • 源码阅读计划

  • OOP

  • 关于 python 中的 setup.py
  • 并行分布式框架 Celery
  • 七种武器,让你的代码提高可维护性
  • 使用 pdb 调试 Python 代码
  • 每周一个 Python 标准库
  • 🐍Python
  • Python工匠系列
佚名
2020-08-13
目录

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

# 前言

这是 “Python 工匠”系列的第 9 篇文章。[查看系列所有文章] (opens new window)

模块(Module)是我们用来组织 Python 代码的基本单位。很多功能强大的复杂站点,都由成百上千个独立模块共同组成。

虽然模块有着不可替代的用处,但它有时也会给我们带来麻烦。比如,当你接手一个新项目后,刚展开项目目录。第一眼就看到了攀枝错节、难以理解的模块结构,那你肯定会想: “这项目也太难搞了。” 😂

在这篇文章里,我准备了一个和模块有关的小故事与你分享。

# 一个关于模块的小故事

小 R 是一个刚从学校毕业的计算机专业学生。半个月前,他面试进了一家互联网公司做 Python 开发,负责一个与用户活动积分有关的小项目。项目的主要功能是查询站点活跃用户,并为他们发送有关活动积分的通知: “亲爱的用户,您好,您当前的活动积分为 x”。

项目主要由 notify_users.py 脚本和 fancy_site 包组成,结构与各文件内容如下:

├── fancy_site
│   ├── __init__.py
│   ├── marketing.py        # 与市场活动有关的内容
│   └── users.py            # 与用户有关的内容
└── notify_users.py     # 脚本:发送积分通知
1
2
3
4
5

文件 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(... ...)
        #  <... 已省略 ...>
1
2
3
4
5
6
7
8
9
10
11

文件 fancy_site/users.py:

from typing import List


class User:
    # <... 已省略 ...>

    def add_notification(self, message: str):
        """为用户发送新通知"""
        pass


def list_active_users() -> List[User]:
    """查询所有活跃用户"""
    pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14

文件: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):
    """为某手机号发送短信"""
1
2
3
4
5
6
7
8
9
10

只要在项目目录下执行 python notify_user.py,就能实现给所有活跃用户发送通知。

# 需求变更

但有一天,产品经理找过来说,光给用户发站内信通知还不够,容易被用户忽略。除了站内信以外,我们还需要同时给用户推送一条短信通知。

琢磨了五秒钟后,小 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, ... ...)
1
2
3
4
5
6
7
8
9
10
11

但是,当他修改完代码,再次执行 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)
1
2
3
4
5
6
7
8

错误信息说,无法从 fancy_site.users 模块导入 User 对象。

# 解决环形依赖问题

小 R 仔细分析了一下错误,发现错误是因为 users 与 marketing 模块之间产生的环形依赖关系导致的。

当程序在 notify_users.py 文件导入 fancy_site.users 模块时,users 模块发现自己需要从 marketing 模块那里导入 send_sms 函数。而解释器在加载 marketing 模块的过程中,又反过来发现自己需要依赖 users 模块里面的 User 对象。

如此一来,整个模块依赖关系成为了环状,程序自然也就没法执行下去了。

modules_before

不过,没有什么问题能够难倒一个可以正常访问 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
1
2
3
4
5
6
7

改动一行代码后,大功告成。小 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 函数了。

from .msg_utils import send_sms
1

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

modules_afte

在新的模块结构中,整个项目被整齐的分为三层,模块间的依赖关系也变得只有单向流动。之前在函数内部 import 的“延迟导入”技巧,自然也就没有用武之地了。

小 R 修改后的代码获得了大家的认可,很快就被合并到了主分支。故事暂告一段落,那么这个故事告诉了我们什么道理呢?

# 总结

模块间的循环依赖是一个在大型 Python 项目中很常见的问题,越复杂的项目越容易碰到这个问题。当我们在参与这些项目时,如果对模块结构、分层、抽象缺少应有的重视。那么项目很容易就会慢慢变得复杂无比、难以维护。

所以,合理的模块结构与分层非常重要。它可以大大降低开发人员的心智负担和项目维护成本。这也是我为什么要和你分享这个简单故事的原因。“在函数内延迟 import” 的做法当然没有错,但我们更应该关注的是:整个项目内的模块依赖关系与分层是否合理。

最后,让我们再尝试从 小 R 的故事里强行总结出几个道理吧:

  • 合理的模块结构与分层可以降低项目的开发维护成本
  • 合理的模块结构不是一成不变的,应该随着项目发展调整
  • 遇到问题时,不要选**“简单但有缺陷”的那个方案,要选“麻烦但正确”**的那个
  • 整个项目内的模块间依赖关系流向,应该是单向的,不能有环形依赖存在

看完文章的你,有没有什么想吐槽的?请留言或者在 项目 Github Issues (opens new window) 告诉我吧。

# 附录

  • 题图来源: Photo by Ricardo Gomez Angel on Unsplash
  • 更多系列文章地址:https://github.com/piglei/one-python-craftsman

系列其他文章:

  • 所有文章索引 [Github] (opens new window)
  • Python 工匠:编写条件分支代码的技巧 (opens new window)
  • Python 工匠:异常处理的三个好习惯 (opens new window)
  • Python 工匠:编写地道循环的两个建议 (opens new window)
编辑 (opens new window)
#编程#Python#模块#import
上次更新: 2024-07-15, 08:03:22
Python 工匠:使用装饰器的技巧
Python 工匠:做一个精通规则的玩家

← Python 工匠:使用装饰器的技巧 Python 工匠:做一个精通规则的玩家→

最近更新
01
提升沟通亲和力的实用策略
03-26
02
工作
07-15
03
如何选房子
06-25
更多文章>
Theme by Vdoing | Copyright © 2019-2025 IMOYAO | 别院牧志
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式