理解 Package (包)、Module (模块)

Package 与 Module

Package,中文的翻译叫 ,字如其意,就是 直接拿来用的一包东西
在 Python 中,正式的名称叫 Modules (模块),本质上,Module 就是一个 .py 文件,或者一个包含多个 .py 文件的文件夹。

比如有一个 hello.py 文件,源代码如下:

MY_VAR = 'wow'

def a_function():
    pass

那么,我们可以将它作为一个 Module 进行导入, 只要 import hello 就可以了。此时的 hello 其实就成了某种意义上的 空间 (Namespace),如下的调用都是可以的:

import hello
hello.MY_VAR
hello.a_function
from hello import MY_VAR
from hello import a_function

import 是引入一个 Module 的语法,除了上述的语法之外,还有如下语法:

from hello import * 
from hello import MY_VAR, a_function
import hello as he_or_other_name
from hello import MY_VAR as VAR, a_function

import * 表示能导入的全部导入,但是一般情况下并不推荐,比如这个例子中,* 代表 hello 这个 Namesapce 内的所有子元素,并且也会合并到当前 .py 文件所代表的 Namespace 了,可能会导致一些名称的混乱。
as 相当于一种 别名,有些时候某个 Module 的名称跟当下有冲突,或者我们自己有特殊改写的需求,要用 as 改变其原始的名称。

在《FirstWeb》中解释变量的时候,强调过一点,很多人会误认为函数 并不是 变量,狭隘地认为 变量 就是 a = 1 此类的赋值。
此处,我们继续扩展,一个 Module 被 import 后,在 Python 环境中,在当前的 Namespace 内,Module 也是一个变量。既然是一个变量,那么,它也要符合变量名称的要求,比如不能以数字、运算符号开头。
我们也已经清楚 Module 其实是一个 .py 文件或者一个文件夹,那么我们在 .py 文件命名的时候,应该尽可能避免类似 中文.py(包含了中文) 或者 hello world.py(有空格) 的文件名,不然,它们怎么被 import 会是一个问题。

如果让你设计 Module?

为了理解 Module 的自然逻辑,我们可以试试站在 Module 机制的设计者角度去探讨,就能避免一些不必要的误区。

单个 .py 文件作为 Module,倒是好理解的,可以直接将 .py 文件视为 Module。但如果是一个文件夹作为 Module,就不能这么简单理解了。

比如下面这个文件夹结构:

folder
    - name1.py
    - name2.py
    - sub_folder
        - sub_name1.py

如果遵循已有设计的,那么 from folder import * 是什么含义?是将 name1、name2 全部导入吗?那么跟它们处于同一级的文件夹 sub_folder 怎么办?难道也将它的全部子文件导入?那么它后面如果还有子文件夹的子文件夹呢?这不就存在一个明显的、潜在的性能问题?!
一个有趣的例子,就是鼠标,我们移动鼠标的时候,比如点击了一个按钮,潜意识中会认为自己在操作这个按钮,实际上是鼠标在操作。确切的说,是光标,它是鼠标在电子视觉中的映射,而鼠标的操作,也是人手的一种映射
这种映射关系,也是 Module 与一个文件夹之间的关系,看起来是同一个东西,但本质上不是同一个概念。Module 就是 Module,它有自己的特性。为了构建这个映射一个文件夹下必须有 __init__.py 这个文件,才能视为一个 Python 的 Module。
仍然以上面 folder 的示例,from folder import * 实际上是从 __init__.py 里进行 import,当然不会将所有的子文件都遍历 import 一次,而 __init__.py 也是一个普通的 .py 文件,你可以在里面增加一些变量,或者一些快捷性质的,比如:

FOLDER_VAR = 'I am __init__.py'
from name1 import NAME
from .sub_folder.sub_name1 import example_func
# 如此, from folder import * , 实际上可以获得 FOLDER_VAR、NAME、example_func

更具体的,请参考 LearnPython.app 内的代码演示。
本节的小标题是 《如果让你设计 Module?》,这种思考的方式,对于学习来说是很有帮助的,有些时候,即使站在这个角度,思考错了,也无关系。
回归本题,如果你来设计,你是会采用文件、文件夹的方式,还是创建出单独的一个格式来呢?如果使用文件夹对应 Module,你是会限制 Module 的特性以迎合文件夹,或者区分成文件夹文件两种不同类型的 Module 呢?
还是也会采用当下 Python 的方式呢?如果是的话,__init__.py 为什么是这个名字,难道叫 __module__.py 不应该更好吗?
这些问题,你终将会获得自己的答案。已经存在的设计,虽然有其历史性的原因,但不少事物,还是因为其逻辑非常的自然,才会不断地被留存下来。

交叉引用

交叉引用 导致的错误,你是必然会遇到的。特别是一个文件夹作为 Module 来使用时,并且将 __init__.py 当做一个快捷变量书写的地方,多方应用过程中,很容易出现这个问题。另一方面,当然也可以理解为代码的规划不是太好,但事无绝对,完美的代码结构,在现实生活中,是罕见的,也不用过于追求。

代码是逐行、从上往下运行的,Python 也因此存在 交叉引用 的问题,特别是在 __init__.py 中做了一些快捷方式的 import,更容易产生交叉引用的问题。
不能因为 Python 存在这个问题,就将这个问题的存在合理化;这个问题不是无法解决的,但也不是需要解决的。不过,这个现象产生的原因,却是非常自然的。

比如下面是 a.py:

from b import v2
v1 = 123

下面是 b.py:

from a import v1
v2 = 456

a.py 运行,或者被 import (本质上也是运行一次)的时候,会尝试去 import b,而 b.pyimport a 了。严格来说,并不是两者不能互相 import,而是此时 a没有完成 import,b 又去 import a,就会触发一次 a.py 的运行 (相当于在 a.py 之内死循环一般地调用 a.py 本身了)。
换个角度思考,难道 a 已经完成 import 的时候,就能交叉 import 了? 是的!一个 Module 被 import 完成后,就会被缓存起来,相当于它只会被运行一次,除非你删除了对应的缓存。更具体的,请参考 LearnPython.app 内的演示。

从哪里载入 Package

我们在命令行窗口调用一个命令的时候,并不需要完整的输入这个命令(文件)的路径,因为 $PATH 的存在,是操作系统的 环境 在起作用。同样的,在 Python 中,import 一个 Module,并不需要指定其具体的路径,也是因为 Python 的环境 在起作用。
一般情况下,我们通过 pip 安装第三方的 package,再 import,倒不用去管 Python 的环境 是如何引导一个 import 找到对应 package 路径的。
但作为 Python 的运行基础,我们应该掌握这个基础知识,这非常重要!

我们先尝试 import re (正则表达式的 package),然后直接看到被载入的 re,其原始文件的路径所在。

注: 当我们直接在 Python 的交互环境中输入一个变量,会显示一些信息,但不要认为此时变量性质是字符串,它可能是你尚未接触的某种类型,比如截图中的 re,它是 module 类型,既不是整数,也不是字符串。

Python 的 Module 会从 sys.path 中查询并载入 (内置的几个 module 除外),sys.path 是一个列表性质的值,而且查询对应 package 时,是 sys.path 依序查询,如果存在多个重名的 package,sys.path 中排在前面,会先被找到。import 一次之后,module 就会被缓存到 sys.modules 中。
sys.pathsys.moduels 都是 list 类型,换句话说,你可以在程序运行的过程中改变它,比如 sys.path 中添加 (append) 一个 (文件夹的) path,那么有些 package 并不需要特别安装,也能直接 import 了。
我们且看一下,默认情况下 sys.path 是哪些:

默认情况下,.py 文件所在的目录,会自动被添加到 sys.path,因为这个目录也是当前 .py 文件的运行目录。如果在 PyCharm 中运行,PyCharm 的默认设置中,还会将 Project 的根目录添加到 sys.path 中。
当前目录如果以相对目录的形式表现出来,就是空字符,我们可以用 os.path.abspath 查看到其绝对位置的路径:

>>> import os
>>> os.path.abspath('')
'/Users/hepochen'
>>> 

优先级与冲突

如果两个以上的同名 package 存在,那么 import 某个 package 的时候,就会产生优先级的冲突。其优先级一般是如下逻辑:

  1. sys.modules 的缓存优先;
  2. 然后按照 sys.path 中路径的次序,逐个查询,排越前面优先级越高。

就如 《FirstWeb》中说的,处理冲突的最好办法,就是不要产生冲突
我们也要注意,从创建一个 .py 文件开始,一些系统保留的变量、内置的 package、常用的 package,就要尽可能避免与这些名称产生冲突。