Python项目如何合理组织规避import天坑
本文共3500余字,预计阅读时间12分钟,本文知乎连接: Python项目如何合理组织规避import天坑 ,本文同步发布于 silaoA的博客 和微信公众号平。
关注学习了解更多的Cygwin、Linux、Python技术。
目 录
- 0x00 module与package
- 代码复用性和分层思想
- module
- package
- 0x01 import
- 基本形式
- relative import和absolute import
- 0x02 项目组织建议
- 参考
- 更多阅读
Python程序代码中经常需要import语句,调用已有的函数、方法等,但在多目录、多文件的项目程序中,容易遇到各种import错误,本文将通过合理化组织规避import带来的错误。 本文描述针对Python3,Python2已过时并将进入无人维护状态,如是初学者建议直接选Python3。
0x00 module与package
代码复用性和分层思想
程序代码如果完成的功能比较简单,放在一个文件里比较正常。如果随着需求变多,要完成的功能越来越多样化,那么正常的做法都是按照功能模块做好设计,将程序代码分割成多个目录、文件组织,每个文件完成各自的那部分功能即可。这样做的好处,一方面是便于维护,一旦发现bug方便快速定位和调试;另一方面是可以提高代码复用性,比如最基础、最常用的那部分代码文件,还可以被其他项目所用,不必从零开始重写。
还没见过哪种编程语言不支持多文件程序设计(如果真有,那也该淘汰了),Python也不例外。
既然支持多文件组织程序了,那么
如何组织
就是个大学问。在
Cygwin系列(九):Cygwin学习路线
文中也提到了计算机系统粗略层次划分,在应用程序层面也可以照此继续细分更多层。
照这个思路,可以简单地把程序再分为
驱动
和
应用
层,
驱动
负责实现基本操作、屏蔽细节,也可以叫
底层
;
应用
层根据
驱动
提供的接口调用相关函数,实现更高级的功能,也可以叫
上层
。比如Python中
len
、
sum
等函数,我们不用关心它是怎么实现的,也不需要重写求长度、求和的函数,自己写的程序代码只要按照接口规定传入参数即可得到预期结果,从而实现更高级的功能需求,那么提供
len
、
sum
等函数的代码文件就称为
底层
,我们自己写的代码可称为
上层
。更进一步地,我们自己写的代码可以划分更多层次出来,不难理解,
底层
与
上层
是相对而言的。
把编程比作建大楼,层次可以划分为 水泥沙子制作、砖头制作、墙体制作、楼房制作 4层,楼房制作层直接取用墙体搭建房子,必要时可以取用砖头,墙体制作层直接取用砖头形成各式各样的墙体,砖头制作层直接取用水泥沙子形成不同规格的砖头,而水泥沙子一般是有做好的。并且到了某次需要建大桥的时候,水泥沙子制作、砖头制作这些功能可以再用,不必重新实现,减少了工作量。
module
在Python中,一个.py文件就是一个module,即“模块”, 模块的名称是文件名去掉末尾的'.py' 。一个模块(A)中的变量、函数、类等符号,被其他模块(B)import之后,可被其他模块(B)引用。我们写程序代码的时候,就可以把代码分门别类地放在不同的.py文件中,按照各自的层级位置,实现各自的功能。
比如将程序分成
brick.py
、
wall.py
、
house.py
。 -
brick.py
实现砖头制作函数,不同的参数指定砖头规格; -
wall.py
中
import brick
,调用函数得到不同规格砖头,组装形成墙体; -
house.py
是主程序,
import wall
后调用函数得到墙体,必要时也可以
import brick
制作特定的砖头,最后统一组装成房子。
Python是“电池内置”型(battery included)语言,初次安装Python解释器时会一并安装很多基础的模块和包,称为“标准库(standard library)”,在Python安装路径的
lib
子目录下可看到。此外,用户可以自行安装其他第三方库,默认会放在
lib/site-packages
子目录下。
有的模块出于运行效率考虑,是被编译进Python解释器的,并不以
.py
文件形式存在,比如常用的
builtins
、
sys
等。内置的
len()
函数、加减乘除运算等就在
builtins
模块中,使用时甚至不需要我们import,Python解释器在启动时便已import好了,而
sys
模块需要先import才能使用。
package
自己写的模块应该避免与Python内置模块重名,但不同人编写的模块名称相同怎么办?为解决名称冲突问题,Python引入按照目录组织模块的方法,创造了package(包)的概念。包是一个特殊的目录,其下
必须
含有名为
__init__.py
的文件,否则Python会将其当作普通目录而不是包,目录下
__init__.py
文件对应的模块名就是包(目录)名,
文件内容可以为空
。
有了包,只要顶层包名称与其他人不发生冲突,模块名称便不会冲突。如下目录组织,顶层包为
soud
目录,import成功后,相应模块名称为
sound.example
、
sound.formats.wavread
、
sound.effects.echo
、
sound.filters.vocoder
等,要引用模块中的变量、函数符号,按照
模块名.符号名
方式使用即可。
sound/ # 顶层包
__init__.py # 初始化sound包
formats/ # 子包formats
__init__.py
wavread.py
wavwrite.py
effects/ # 子包effects
__init__.py
echo.py
surround.py
reverse.py
filters/ # 子包filters
__init__.py
equalizer.py
vocoder.py
karaoke.py
example.py
0x01 import
基本形式
import语句支持两种形式:
- import xx
-
xx
是模块名(指含包路径的模块全名)的话,通过xx.符号
的形式访问函数、变量、类。 -
xx
仅是包名的话,只会import到包下子模块,并不会自动import子包,可以通过xx.mod1.符号
的形式访问函数、变量、类,通常会在包下的__init__.py
中写入import子包语句,这样便能通过xx.subpack1.mod2.符号
形式访问mod2中的函数、变量、类,还有更多技巧进一步简化访问子包下模块内的符号。 -
xx
不能定位到任何函数、变量、类名。 - from xx import yy
-
xx
是模块名(指含包路径的模块全名)的话,yy
就定位到具体的函数、变量、类。 -
xx
是包名的话,yy
可以定位到子包、模块或具体的函数、变量、类,比如from sound import __version__
,Python解释器首先检测__version__
是否为sound/__init__.py
中定义的符号,若不是,再检测是否存在sound.__version__
模块,如不存在便报错。如果yy
是*
,那么代表引入xx
模块中定义的所有符号;对于包,可以在包下__init__.py
文件定义__all__
变量规定允许导出的所有符号或模块,如无__all__
定义,则该语句只负责importxx
包,执行包下__init__.py
文件,当然文件内可以import子模块或子包。from xx import *
形式容易造成命名冲突,建议慎用!
以上两种形式,可以后续加
as
表达式,对import的对象取别名,如
import numpy as np
。
equalizer.py
、
vocoder.py
文件在同级目录下,
equalizer.py
中使用
import vocoder
或者
from vocoder import xx
,然后运行
python sound/filters/equalizer.py
是不会报
ImportError
错误的。那么,不在同级目录下该怎么处理呢?
relative import和absolute import
简单来说,相对导入(relative import)的语句通过使用
.
或
..
等相对路径的方式来定位要import的模块/包,基准路径是当前模块所在包,如在
sound/effects/reverse.py
中,基准路径是
sound.effects
包,使用下属语句都是合法的。
from . import echo # 对应模块 ./echo.py
from .surround import XXX # 对应模块 ./surround.py内的XXX符号
from .. import formats # 对应包 ../formats
from ..filters import equalizer # 对应模块 ..filters/equalizer.py
from ... import example # 对应模块 ../../example.py
但在执行
python sound/effects/reverse.py
时便会报
ModuleNotFoundError
错误,这是因为执行语句将
sound/effects/reverse.py
当做主模块(main module),主模块的名称在运行时会被Python强制命名为
__main__
,从而找不到
__main__.echo
模块,规避的正确方式是使用
-m
选项运行指定模块,即
python -m sound.effects.reverse
运行正常。相对导入方式可以解决导入当前目录、上级目录、上上级目录下模块的需求,但
使用相对导入的文件不能直接以脚本方式运行,只能以模块方式(-m选项)运行,并且当前路径要处于import语句定位的路径或更高层路径
。
绝对导入(absolute import)与上相反,不使用
.
或
..
定位要import的模块/包的路径,而使用完整路径定位模块/包,Python根据
sys.path
的值逐路径搜索要导入的模块/包,而对于内置的和安装第三方的模块/包,其路径都在
sys.path
值中。
绝对导入的方式无法导入上级目录的模块
,虽然可以通过在程序中改写
sys.path
追加特定路径的方式规避,但不推荐,会增加风险;
使用绝对导入的文档,应使用脚本方式直接运行
。
0x02 项目组织建议
为提高代码复用和有效模块化组织,建议使用主程序+包的方式组织项目,其中包内按分层思想放置若干模块,包内模块间使用相对导入(relative import),项目根目录下放置主程序文件。仍以开头建大楼为例,组织如下:
projectname/ # 项目目录
#__init__.py # 也可以变成包,供今后其他项目import
construct/ # 子包construct用于建造过程
__init__.py
brick.py # 制作砖头
wall.py # 制作墙体
misc.py # 其他不好归类的功能
decoration/ # 子包decoration用于装修装饰过程
__init__.py