前言

模块(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 仔细分析了一下错误,发现错误是因为 usersmarketing 模块之间产生的环形依赖关系导致的。

当程序在 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 的故事里强行总结出几个道理吧:

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

附录

系列其他文章:

Python 工匠:让函数返回结果的技巧

Python 工匠:异常处理的三个好习惯

Python 工匠:编写地道循环的两个建议


蓝鲸智云

本文由腾讯蓝鲸智云编辑发布,腾讯蓝鲸智云(简称蓝鲸)软件体系是一套基于PaaS的技术解决方案,致力于打造行业领先的一站式自动化运维平台。目前已经推出社区版、企业版,欢迎体验。

Python 工匠:一个关于模块的小故事的更多相关文章

  1. 一听就懂:用Python做一个超简单的小游戏

    写它会用到 while 循环random 模块if 语句输入输出函数

  2. python 搭建一个http服务的小例子

    一.创建Server 1.Dos 命令 python -m BaseHTTPServer [port] 默认端口是8000, 2.Python 脚本启动 #coding:utf-8 ''' Creat ...

  3. [Python] Python工匠(Github)

    1.善用变量来改变代码质量 变量命名 变量要有描述性,不能太宽泛 BAD:day, host, cards, temp GOOD:day_of_week, hosts_to_reboot, expir ...

  4. python学习笔记_week5_模块

    模块 一.定义: 模块:用来从逻辑上组织python代码(变量,函数,类,逻辑:实现一个功能), 本质就是.py结尾的python文件(文件名:test.py,对应模块名:test) 包:用来从逻辑上 ...

  5. python函数和常用模块(三),Day5

    递归 反射 os模块 sys模块 hashlib加密模块 正则表达式 反射 python中的反射功能是由以下四个内置函数提供:hasattr.getattr.setattr.delattr,改四个函数 ...

  6. Python基础之--常用模块

    Python 模块 为了实现对程序特定功能的调用和存储,人们将代码封装起来,可以供其他程序调用,可以称之为模块. 如:os 是系统相关的模块:file是文件操作相关的模块:sys是访问python解释 ...

  7. Python自动化之常用模块

    1 time和datetime模块 #_*_coding:utf-8_*_ __author__ = 'Alex Li' import time # print(time.clock()) #返回处理 ...

  8. Python学习笔记-常用模块

    1.python模块 如果你退出 Python 解释器并重新进入,你做的任何定义(变量和方法)都会丢失.因此,如果你想要编写一些更大的程序,为准备解释器输入使用一个文本编辑器会更好,并以那个文件替代作 ...

  9. Day5 - Python基础5 常用模块学习

    Python 之路 Day5 - 常用模块学习   本节大纲: 模块介绍 time &datetime模块 random os sys shutil json & picle shel ...

随机推荐

  1. Redis集群搭建 三主三从 docker版 急速搭建

    最近学习了docker 发现使用docker搭建一个redis非常的简单接下来就是搭建步骤 1.首先清空一下容器  #清空所有容器docker rm -f $(docker ps -aq) 2.然后创 ...

  2. CentOS8设置国内镜像源(阿里云镜像)

    CentOS8设置国内镜像源(阿里云) 1.备份原有配置 [root@localhost ~]# mkdir /etc/yum.repos.d.bak [root@localhost ~]# mv / ...

  3. 12.web基础与HTTP协议

    web基础与HTTP协议 目录 web基础与HTTP协议 web基础 域名概述 HTML概述 HTML基本标签 HTML语法规则 HTML文件结构 头标签中常用标签 内容标签中常用标签 静态网页与动态 ...

  4. 一、shell编程与变量

    目录 命令是什么 命令是如何运行的: 基本语法 解释器 注释 如何执行 输入.输出流 重定向 管道符 | 变量 常见Shell变量的类型包括: 变量命名原则 单引号和双引号 反引号 变量作用范围 查看 ...

  5. 使用Node.js还可以发邮件

    前言 今天,我们给大家开发一个小效果.篇幅比较短,主要给大家展示效果.实战 首先我们初始化一个Node项目 npm init -y 创建一个app.js文件 'use strict'; const n ...

  6. Redis入门到精通01

    Redis入门到精通 目录 Redis入门到精通 一.Redis缓存框架基本介绍 1.1Redis的应用场景 二.Redis的安装方式 2.1Windows操作系统安装Redis 2.2Linux操作 ...

  7. UML图记忆技巧

    什么是UML类图 Class Diagram:用于表示类.接口.实例等之间相互的静态关系 虽然名字叫类图,但类图中并不只有类 记忆技巧 UML箭头方向: 从子类指向父类 我相信 很多同学都会有和我一样 ...

  8. Object类和toString方法 --和Object类的equals方法

    一,Object类概述:Object是类层次结构的根,每个类都可以将Object作为超类,所有类都直接或者间接的继承自该类构造方法:pulic Object()在面向对象中,子类要访问父类的无参构造方 ...

  9. 常用类-jdk8之前的日期和API

    一.System静态方法 点击查看代码 package com.Tang.StringDay01; import org.junit.Test; public class DateTimeTest { ...

  10. 5-4 Sentinel 限流_流控与降级

    Sentinel 介绍 什么是Sentinel Sentinel也是Spring Cloud Alibaba的组件 Sentinel英文翻译"哨兵\门卫" 随着微服务的流行,服务和 ...