深入理解python的导入问题——包,模块(请勿参考,未完待续。。。)

背景

        在python开发中,经常需要导入不同的内容,在开发大型项目时尤其棘手。稍有不慎就报错,算下来在这个问题上我浪费了太多时间。与其继续陷入这种泥潭,还不如掘地三尺掌握这个知识点,一劳永逸地解决问题。

        本文按照“理论->习惯用法->示例”的思路组织语言。如果只是用一下,可以忽略理论部分(不过还是推荐至少把“路径”这一概念搞清楚)直接快速浏览习惯用法,寻找适合自己的方式,然后再照对应的示例去实现。

 

1. 理论补充

注:该部分非专业解释,以自己理解为主,没有权威性,专业解释详见相关链接并请自行查阅官方资料。

1.1 路径

工作目录:当前程序运行的类似于linux中使用pwd查询当前的工作目录一样,可以通过工程配置来决定。务必搞清楚这一点,这个是使用相对路径的基石(不同IDE,不同工程设置下这里都会不一样,最终导致各种问题)

绝对路径:文件存储的磁盘路径,例如:D:\Workspace\python\module_test\main.py。(只要文件的存储地址不变,则该路径总是对的)。

相对路径:文件存储相对于工作目录的层级关系(只要工作目录发生改变,相对路径的关系就被破坏了)

. 当前文件夹

.. 上一级文件夹

系统路径(sys.path):就是计算机的环境变量,主要用来处理系统默认路径,正是因为有了系统路径,你导入系统模块(eg: time, os, 或者安装的标准第三方模块)时,才不用纠结这个系统模块到底在哪里。(显然,你也可以把自定义的模块添加到系统路径下)

各种路径的查询和修改方法:

import sys
import os

print(sys.argv[0]) #获得当前待执行的那个模块(py文件)
print(os.getcwd()) #获得当前工作目录
os.chdir("目标目录")   #修改当前工作目录为目标目录
print(os.path.abspath('.')) #获得当前工作目录
print(os.path.abspath('..')) #获得当前工作目录的父目录
print(os.path.abspath(os.curdir)) #获得当前工作目录
print(sys.path) #获得系统路径

1.2 模块(module),包(package)

所谓模块,可以简单理解为“就是一个py文件”。

所谓包,可以简单理解为“多个想要组织在一起的py文件,放在了一个文件夹里”,而那个文件夹就是一个包。与普通的文件夹的不同之处在于,该文件夹里必须带一个"__init__.py"的文件(可以是空文件)。

一个简单的比方,试想如下的工程组织结构

package/
        __init__.py
        module1.py
        module2.py
        sub_package/
                    __init__.py
                    module11.py
                    module12.py

package文件夹就是一个包,module1和module2就是package的两个模块。其中package还有一个子包叫sub_package,它里面也有两个子模块分别叫module11和module12

1.3 对象,模块导入

        ——python的世界里,一切皆对象。不论是类,函数,变量,模块还是包,其名字只是这个对象的一个引用而已。

按照我的理解就是:我就是一个实体(对象),张三就是我的名字(引用)。你叫张三的时候其实本意是想叫我这个人过去,而不是对张三这个名字感兴趣,只不过你通过张三这个名字具体地叫到了我这个人。(也许你有一天也会惊奇地发现,我这个人其实有好多个小名的,这就意味着其他人叫我李四,王五,刘麻子我照样会答应的。。。)

        好了,我们通过import module 语句导入一个模块会发生什么?—— 你通过module这个名字实例化了一个对象(这个对象在成功import之后就存在于内存里了),一旦你成功实例化了这个module对象,你就可以用上module里面定义的内容了(变量,函数,类等)。

       导入一个sys模块时,首先是找到这个模块,然后会从头到尾执行这个模块,遇到def就创建一个函数对象,然后赋上函数名,遇到模块中被赋值的全局变量,那就创建这个赋值号右侧的对象,然后赋给变量。那我怎么知道这个module里到底导入了哪些资源呢?你可以这样查询

print(dir(module))
有可能得到如下的结果
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b'] 

1.4 命名空间

        接着上面的“我是张三”这个继续讲,如何建立起“实体->引用”的对应关系呢?看着挺眼熟,这不是字典的结构嘛?是的,python里就是这么实现的,不过取了一个叫做“命名空间”的术语而已。通过命名空间,就建立了引用和实体之间的映射关系了,所以你不用去操作内存,只要管这个对象叫啥名字就可以了。

        一个命名空间中不能有重名,但是不同的命名空间可以重名而没有任何影响。python中有3类命名空间

  • local:函数调用时创建(调用返回后即消失),记录了函数入参,内部变量等
  • global:模块加载时创建(除非人为手动卸载该模块,否则一直存在),记录了该模块所包含的资源
  • built-in:系统自带(一直存在),任何模块均可访问(通常放置内置函数和异常)

1.5 导入机制

a. 导入原则

摘抄Python标准库参考手册3.6.4中对import语句的一段说明:

The basic import statement (no from clause) is executed in two steps:

  1. find a module, loading and initializing it if necessary
  2. define a name or names in the local namespace for the scope where the import statement occurs.

When the statement contains multiple clauses (separated by commas) the two steps are carried out separately for each clause, just as though the clauses had been separated out into individual import statements.

        以import pandas as pd为例,模块导入过程首先是去寻找pandas模块,找到后将其与本地命名空间的资源进行比对(防止重复导入)。如果没有,则实例化pandas对象,在内存里创建相应的资源并为其初始化,如果有则跳过。最后将pandas这个资源和pd这个变量名绑定到一起,用户就可以通过pd.xxx来引用pandas的资源了

b. 模块搜索顺序(如果模块同名的话,此处可能会冲突)

When a module named spam is imported, the interpreter first searches for a built-in module with that name. If not found, it then searches for a file named spam.py in a list of directories given by the variable sys.pathsys.path is initialized from these locations:

  • The directory containing the input script (or the current directory when no file is specified).
  • PYTHONPATH (a list of directory names, with the same syntax as the shell variable PATH).
  • The installation-dependent default.

After initialization, Python programs can modify sys.path. The directory containing the script being run is placed at the beginning of the search path, ahead of the standard library path. This means that scripts in that directory will be loaded instead of modules of the same name in the library directory. This is an error unless the replacement is intended. 

可见,系统在搜索模块(module.py)时,优先去找built-in的内置模块,找不到再通过sys.path变量去搜索module.py这个文件。

可以做个实验,自定义一个os.py的模块并随便定义一个函数,你会发现无论如何你自定义的os模块都不起作用

小结:

  • 模块导入前首先确定自己的路径,再考虑用什么方法去导入。搞不清楚路径就不要谈导入了,一团乱麻
  • 模块就是一个py文件,包就是一个带__init__.py的文件夹
  • 成功导入一个对象意味着(有可能)实例化了一个对象并在内存中创建了与该模块相关的资源
  • 命名空间就是一个字典,它实现了上述从模块名到实际对象的映射关系,正是因为这层映射关系你才可以通过模块名来引用该资源,而不用关注内存细节
  • 模块导入的机制是:首先搜索到模块并对初始化(如果之前没导入过的话),然后将该模块的资源与一个名称绑定到命名空间中。例如import pandas as pd. 你可以利用pd来指代pandas,而pandas这个名字指代了实例化的pandas类所包含的一切资源
  • 模块搜索的顺序是:先在built-in中搜索,如果没有再找sys.path求助
  • sys.path本质上就是一个系统变量,它存储了一堆路径(工作路径,环境变量,site-package安装路径,其他默认路径等),特别地: sys.path[0] 指代了当前待执行脚本的路径

2. 常用导入方式汇总

备注:下文如无特别提示,统一main.py为当前待执行的脚本,且工作路径与其保持一致

2.1 同级模块导入,无包的导入(最简单)

文件层次结构示例:
test/
    main.py
    a.py    -> 定义了hello_a函数, para_a变量

方法1:
import a
a.hello_a()
print(a.para_a)

方法2:
from a import *
hello_a()
print(para_a)

2.2 不同级模块导入,无包的导入

文件层次结构示例:
test/
    main.py
    sub_module/
            a.py    -> 定义了hello_a函数, para_a变量

方法1:
import sub_module.a
sub_module.a.hello_a()
print(sub_module.a.para_a)

方法2:
from sub_module.a import *
hello_a()
print(para_a)

2.3 包的导入(最基础的做法)

文件层次结构示例:
test/
    main.py
    package1/
            __init__.py -> 空文件,但必须要有
            m1.py    -> 定义了hello_a和hello_b函数, para_a变量

导入方法:
import package1.m1 as tmp
tmp.hello_a()
print(tmp.para_a)

2.4 控制from module import *的导入内容(不推荐)

2.3 中的导入层次中,如果使用from module import *, 会发现导入失败。此时__init__.py文件的作用体现出来了。

只需在__init__.py文件中添加 __all__字段就可以实现导入内容的精确控制,例如

 

3. 简单示例

 

相关链接

工作路径:https://blog.csdn.net/qq_15188017/article/details/53991216

详解命名空间:https://www.cnblogs.com/zhangxinhe/p/6963462.html

python导入机制详解:https://www.cnblogs.com/qiaoxg-2018/p/usingimport.html

包/模块理解:https://www.cnblogs.com/kex1n/p/5977051.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章