倘若你曾使用过, 那么你必定使用过, 借助关键字加载过形形的模块。然而, 你对其中模块与包的概念可熟悉呢? 又或者, 对于以下几个问题, 你可有确切的答案?
鲁迅先生讲, 所说的「于无声处听惊雷」的意思是, 平淡的时候会有那种令人惊奇、意外的事情发生, 相关各种模块、包的概念也是这样的情况, 要是你对上面提及的几个问题存在疑惑, 那么这一篇作品说要撰的目的就是为了你呀。
模块为什么要有模块
尽人皆知, 存在着一个具备交互式特性的解释器, 于该解释器之内, 你能够施展其所有的功能, 然则, 该解释器属于一次性的那种, 具体意思就是, 要是你将解释器关闭掉, 那么先前已经定义以及运行的所有事物, 都会消失得无影无踪, 从另一方面来讲, 于解释器里输入代码是一件相当麻烦的事儿, 之所以如此, 是由于在解释器里复用代码是颇为困难的。
在这种情况下, 人们会将具备相对稳定特征且篇幅较长的代码, 存放在一个纯文本文件当中。通常而言, 我们把像这样扩展名为.py的文件称作脚本。为了能够提升代码复用率, 我们能够把一组存在关联的定义、声明, 保存于同一个.py文件之中。在这个时候, 这个脚本便是一个模块。我们能够在解释器里面, 或者是在其他脚本里, 借助载入定义好的模块。
模块的识别
与中的别的对象相同, 也针对模块设定了一些类似的变量。就模块而言, 其最为关键的便是自身的名字了。每当执行脚本时, 它会给该脚本赋予一个名称。对于「主程序」来讲, 此脚本的被规定为"";对于被引入主程序的模块而言, 此脚本的被定义成脚本的文件名(base)。所以, 我们能够运用 if =="": 在模块代码里定义一些测试代码。
.py
def fib_yield(n):
a, b = 0, 1
while b < n:
yield b
a, b = b, a+b
def fib(n):
for num in fib_yield(n):
print(num)
if __name__ == "__main__":
fib(10)
我们将其保存为 .py,而后在 解释器中 它。
In [1]: import fibonacci
In [2]: dir(fibonacci)
Out[2]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'fib',
'fib_yield']
In [3]: print(fibonacci.__name__)
fibonacci
In [4]: fibonacci.fib(5)
In [5]: for num in fibonacci.fib_yield(5):
…: print(num)
…:
能够观察到, 当.py以模块引入的情形时, .被设定为文件名""。然而要是在命令行直接去执行.py, 那么if语句块就会被执行, 在这个时候, 是""。
模块的内部变量和初始化
为各个模块分别维护了独自的符号表, 所以能够达成类似于C++里名字空间()的功能。模块当中的函数, 能够运用模块的内部变量, 去达成相关的初始化操作;与此同时, 应用模块的时候, 也无需担忧这些模块内部变量与用户自行定义的变量出现同名冲突的情况。
.py
foo = 0
def show():
print(foo)
if __name__ == "__main__":
show()
在这里, 我们于模块内部, 定义了一个被称作 foo 的内部变量, 而且, 在名为 show 的函数当中, 还引用了这个变量。
In [7]: import module_var
…:
…: foo = 3
…:
…: print(foo)
…: print(module_var.foo)
…:
…: module_var.show()
…:
需要特别指出的是, 模块的初始化行为, 这里所讲的是 foo = 0 这个语句, 仅仅是在解释器头一回处置该模块之际才会运行。换句话讲, 要是同一模块被多次使用, 它仅仅会执行一回初始化。
from … …
模块给出了如同名字空间那样的限制, 只是呢, 也能够准许从模块里把指定的符号(变量、函数、类等等)导入到当前的模块之中。导入过后, 这些符号就能够直接去使用, 而用不着加上前缀模块名。
In [8]: from fibonacci import fib_yield, fib
In [9]: fib(10)

要值得说一下的是, 存在一种情况, 如果被导入进来的符号在引用的时候用的是模块里面的变量, 那么即便已经导入了, 之后要是用到这个变量, 也依然会使用模块之中的那个变量, 然而并非当前所处环境的那个有着相同名字的变量, 是另有深意的不一样的变量呢。
In [11]: from module_var import show
In [12]: foo = 3
In [13]: show()
0
还有更为粗暴的方式, 那就是导入模块内部的所有公开符号, 也就是那些没有前缀下划线的符号。然而, 总体来讲, 除了进行相关实验以及排查方面的情况, 并不主张采用这样的做法。这是由于通常情况下, 你并非知晓模块里面都定义了何种符号。也不知道和当前所处环境是否存在重名的符号。一旦出现重名的状况, 那么如此粗暴地导入模块内的所有符号, 将会把当前环境的版本给覆盖掉。进而酿就难以进行排查的错误。
模块搜索路径
在先前的时候, 我们一直都在针对模块的利处加以讨论, 然而却遗漏了一个疑问: 该怎样得知从什么地方去寻找到模块文件呢?
要是你对命令行有所熟悉, 那这个问题于你而言就并非难以明白。于命令行里执行的任何一项命令, 事实上背后都对应着一个能够执行的文件。命令行解释器, 像 cmd 、bash 这样的, 会从出自一个全局的环境变量 PATH 那里读取一张有序的列表。这张列表涵盖了一系列的路径, 可是命令行解释器, 会依照顺序在这些路径之中, 去搜寻所需的可执行文件。
对模块文件展开搜寻, 同样依照了相近的思路, 举例来说, 要是用户于其中试着进行导入时, 那么。
In [14]: import foobar
—————————————————————————
ImportError Traceback (most recent call last)
input-14-909badd622c0> in ()
—-> 1 import foobar
ImportError: No module named foobar
pyc 文件
与 LaTeX 里碰到的状况相同, 即: 加载诸多文本文件是迟缓的, 所以, 也运用了类 LaTeX 的解决办法, 也就是: 把模块编译成易于加载的文件, 随后保存起来(等同于 LaTeX 里的 dump 格式文件 .fmt), 这些经过编译且保存好的文件, 带有后缀名 .pyc。
当模块被编译好之后, 在下次进行载入这个动作的时候, 就会去读取与之对应的那个.pyc文件, 而并非是去读取.py文件。并且, 装载.pyc文件这件事情会比装载.py文件这件事情要更快一些。
有一点值得说一下, 对于那所谓的.pyc, 好多人一直以来都存在着错误的理解。实际上, 从程序运行这个方面来看, 去装载.pyc文件, 其速度并不比去装载.py文件要来得更快。这里所讲的加速情况, 仅仅只是在进行模块装载的这个过程当中才会发挥作用。所以, .pyc里面的C, 其实更多地能够被理解成是cache。
将目录当做模块看待的包(), 是中对模块更高一级的抽象 , 目录视作模块看法下的包()简单来说做之这样 , 目录中的各个之不同相关模块 , 就变成了「包」以内的子模块 , 此外 , 包之中目录里头还能够可以有子目录 , 这些之那些都可谓能够是包。这种具有分层性质的情况 , 对模块识别以及管理 , 都是十分非常有好处的 , 特别之处在于 , 对于一些大型的工具包 , 内里也许大概可能有成百上千个不同功能的模块 , 要是逐个模块进行发布 , 那简直就会犹如成为一场灾难。
对于科学计算领域而言, SciPy, 还有NumPy等第三方工具, 它们均是以包的形式来进行发布的。
目录结构
于每一个谓之「包」的目录之内, 则必然得有一个名为以 .py的文件。就这文件的名字而言呀, 首先呢它有着 __ 于此前此后做缀饰, 如此我们就能了解到, 这个文件必定就是在内部专门用以进行某种辨识用途的;其次呢它具备字符init, 靠此我们晓得它肯定是和初始化存在关联的;最后呀它还有着 .py当作后缀名, 所以呢它同样是一个模块, 能够达成一些特定的工作事项。
此刻假定你打算炮制一个用于处置图片的工具包, 它兴许是由多个模块组合而成的。因而你会思量将其打造成一个包, 其内部依据功能划分成若干的子包, 接着再进一步细分成各异的模块去予以实现的。犹如会存有这样的目录架构的。
picture/ Top-level package
__init__.py Initialize the picture package
formats/ Subpackage for file format conversions
__init__.py
jpgread.py
jpgwrite.py
pngread.py
pngwrite.py
bmpread.py
bmpwrite.py
…
filters/ Subpackage for filters
__init__.py
boxblur.py
gaussblur.py
sharpen.py
…
下面这里, 存在着一个目录, 在此目录之下, 有着名为.py的文件, 故此, 会把它当作是一个包来对待;与之相类似的别样情况是, 有一些子目录, 它们分别是和, 如此一来, 就变成了处在之下的子包。在这里面, 针对于子包的划分操作, 是以功能作为标准来进行的。处于之下的那些模块, 这类模块被设计出来的目的, 是专门用来处理不同格式的图片文件的读取以及写入这些操作的;然而, 处于之下的另外一些模块, 反倒这个样子被设计, 是用于去实现各种各样的滤镜达成的效果的。
使用 包
拥有包的运用与模块的运用相类似, 属于颇为自然的样式。针对我们所具有的包而言, 借若你期望运用其中特定的模块, 能够像下面这样来操作 , 是可以如此去做的。
import picutre.filters.gaussblur

如下这般, 你便导入了, 存在于包之中的, 那个作为子包的里面所含有的模块了, 如此一来, 你便能够去运用高斯模糊模块所提供出来的功能了, 其具体的使用方式呢, 和使用模块也是保持着一致的状态的。
picture.filters.gaussblur.gaussblur_filter(input, output)
from picture.filters import gaussblur
这样一来,你就可以直接按如下方式使用高斯模糊这一滤镜了。
gaussblur.gaussblur_filter(input, output)
.py
之前曾进行过 .py 这个特别文件的简要讲解, 只是未曾深入展开, 在此处我们要对其展开详细阐述。
第一个问题是, 为何要设计.py, 而不是自动把任意一个目录都视作包呢? 这主要是为防止重名引发的问题。比如说, 极有可能用户在目录下创建了一个子目录, 其名为;但有内建的同名模块。要是毫无限制地将子目录当作包, 那么, 就会引入这个包。而这般行为, 或许并非用户所期望的。从这个层面讲, 设计.py是一种保护举措。
接下来的问题是,.py 具体还有什么用?
最开始来讲, .py 在相应包被引入时会率先被引入, 作为模块文件, 它能够执行某些初始化的操作。这就意味着, .py 当中是可以留存一些初始化代码的, 像是引入依赖的其他模块这样的代码。因此 确切来说, 相当于是。这就是说, 相当于是 .。
其次, 或许细心的你已然发觉,在上小小的一个小节当中, 我们并未将对包的from *的用法予以介绍。此番的缘故在于, 从某个包里头导入全部内容, 这样的一项行为不是明晰的;那必然要是包的作者去做指定才行。我们能够于.py里界定名为列表的东西。如此这般之后, 便能够运用from *了。
具体来说,我们可以在 /.py 中做如下定义。
.py
import collections # import the built-in package
__all__ = ["formats", "filters"]
彼时, 要是于用户模块之中采用 from * 这种方式, 那么最先导入在的是内建的模块, 接续导入的又会是 . 以及 . 这俩子包。
在包内使用相对层级引用其他模块
你若细心, 想必已然发觉, 于引入包之中的模块之际, 咱们用书句号.去替代斜线(抑或是反斜线)进行路径层级之标记(实则为包与模块的层级)。于包的内部, 咱们同样能够运用类乎相对路径的方式, 借助相对层级去简化包内模块间的相互引用。
比如说, 于处于.py的环境里, 你能够借助下面这四种路径去引入.py, 并且它们所带来的成效均是相同的。
import boxblur
from . import boxblur
from ..filters import boxblur
from .. import filters.boxblur as boxblur