Fork me on GitHub

深入理解Python语言中import机制

目录

  • 背景
  • 第一部分 包和模块
  • 第二部分 Import 方法
  • 第三部分 命名空间(namespace)
  • 第四部分 Import的过程
  • 第五部分 将模块、包的路径加入检索路径
  • 参考文献及资料

背景

Python语言使用过程中,经常会import第三方包,使用过程中或多或少遇到一些问题。本文将从原理层面简介Pythonimport机制,明白原理后,在遇到问题时候就会自己排错了。

第一部分 Python中的包和模块

首先介绍Python中两个概念:包和模块。简单的理解(从文件系统角度),包(package)是一个文件夹,而模块(module)是一个Python源码文件(扩展名为.py)。

  • 包(package):文件夹(文件夹中含有文件__init__.py),包里面含有很多模块组成。

    __init__.py文件,在里面自定义初始化操作,或为空。

  • 模块(module):即python文件,文件中定义了函数、变量、常量、类等。

Python defines two types of packages, regular packages and namespace packages. Regular packages are traditional packages as they existed in Python 3.2 and earlier. A regular package is typically implemented as a directory containing an __init__.py file. When a regular package is imported, this __init__.py file is implicitly executed, and the objects it defines are bound to names in the package’s namespace. The __init__.py file can contain the same Python code that any other module can contain, and Python will add some additional attributes to the module when it is imported.

第二部分 Import 方法

2.1 Import 模块方法

可以被import语句导入的对象是以下类型:

  • 模块文件(.py文件)
  • C或C++扩展(已编译为共享库或DLL文件)
  • 包(包含多个模块)
  • 内建模块(使用C编写并已链接到Python解释器中)

先看一个例子。我们经常使用的模块math ,背后对应其实是一个python文件:math.py 。该文件在C:\Anaconda3\Lib\site-packages\pymc3目录里面(具体环境会有差异)。

1
2
3
import math
math.sqrt(2)
#1.4142135623730951

如果只要import math.py中具体的函数:

1
2
3
from math import sqrt,sin
sqrt(2)
sin(1)

另外可以将模块中所有内容导入:

1
2
from math import *
sqrt(2)

2.2 Import 包方法

包(package)可以简单理解为文件夹。该文件夹下须存在 __init__.py 文件, 内容可以为空。另外该主文件夹下面可以有子文件夹,如果也有 __init__.py 文件,这是子包。类似依次嵌套(套娃)。

例如Tensorflow的包(文件树):

1
2
3
4
5
6
7
8
9
10
11
12
root@vultr:~/anaconda3/lib/python3.6/site-packages/tensorflow# tree -L 1
.
├── aux-bin
├── contrib
├── core
├── examples
├── include
├── __init__.py
├── libtensorflow_framework.so
├── __pycache__
├── python
└── tools

__init__.py 文件在import包时,优先导入,作为import包的初始化。

我们以Tensorflow为例:

1
2
3
4
5
6
7
8
#导入包
import tensorflow as tf
#导入子包:contrib
import tensorflow.contrib as contrib
from tensorflow import contrib
#导入具体的模块:mnist
from tensorflow.examples.tutorials import mnist
import tensorflow.examples.tutorials.mnist

第三部分 命名空间(namespace

Namespace是字典数据,供编译器、解释器对源代码中函数名、变量名、模块名等信息进行关联检索(这是一个名称资源登记簿)。

3.1 定义

Python语言使用namespace(命名空间)来存储变量,namespace是一个mapping(映射)。namespace可以理解是一个字典(dict)数据类型,其中键名(key)为变量名,而键值(value)为变量的值。

A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries。

  • 每一个函数拥有自己的namespace称为local namespace(局部命名空间),记录函数的变量。
  • 每一个模块(module)拥有自己的namespace。称为global namespace(全局命名空间),记录模块的变量,包括包括模块中的函数、类,其他import(导入)的模块,还有模块级别的变量和常量。
  • 每一个包(package)拥有自己的namespace 也是global namespace ,记录包中所有子包、模块的变量信息。
  • Python的built-in names(内置函数、内置常量、内置类型)。 即内置命名空间。在Python解释器启动时创建,任何模块都可以访问。当退出解释器后删除。

3.2 命名空间的检索顺序

当代码中需要访问或获取变量时(还有模块名、函数名),Python解释器会对命名空间进行顺序检索,直到根据键名(变量名)找到键值(变量值)。查找的顺序为(LEGB):

  1. local namespace,即当前函数或者当前类。如找到,停止检索。
  2. enclosing function namespace,嵌套函数中外部函数的namespace
  3. global namespace,即当前模块。如找到,停止检索。
  4. build-in namespace,即内置命名空间。如果前面两次检索均为找到,解释器才会最后检索内置命名空间。如果仍然未找到就会报NameRrror(类似:NameError: name 'a' is not defined)。

3.3 举栗子

讲完了理论介绍,我们来举栗子,直观感受一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
#进入python环境
Python 3.5.3 |Anaconda custom (64-bit)| (default, May 11 2017, 13:52:01) [MSC v.
1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> print(globals())
{'__name__': '__main__', '__doc__': None, '__spec__': None, '__package__': None,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__builtins__': <mod
ule 'builtins' (built-in)>}
>>> x=1
>>> print(globals())
{'__name__': '__main__', '__doc__': None, '__spec__': None, '__package__': None,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__builtins__': <mod
ule 'builtins' (built-in)>, 'x': 1}

上面的例子我们查看了global namespace的字典(dict),其中'__builtins__'就是内置命名空间。新建变量x=1后,全局命名空间会新增这个K-V对('x': 1)。

还可以通过下面的方法查看import模块、包的namespace

当我们import一个module(模块)或者package(包)时,伴随着新建一个global namespace(全局命名空间)。

1
2
3
4
5
6
7
8
9
10
11
import math
math.__dict__
{'__name__': 'math', 'tanh': <built-in function tanh>, 'nan': nan, 'atanh': <bui
lt-in function atanh>,'acosh': <built-in function acosh>,
#中间略
'trunc': <built-in function trunc>, 'acos': <built-in function acos>, 'sqrt': <built-in
function sqrt>, 'floor': <built-in function floor>, 'gamma': <built-in function
gamma>, 'cosh': <built-in function cosh>}
import tensorflow
tensorflow.__dict__
#包的所有模块、函数等命名空间信息。大家可以试一下。

大家可以动手试试其他的场景,比如函数内部查看locals() 。函数内部的变量global声明后,查看globals()字典会有怎样变化。这里就不再一一验证举栗了。

对于包,我们以tensorflow为例:

1
2
3
4
5
6
import tensorflow
tensorflow.__dict__
##中间略,只摘取部分信息。命名空间中包含module和function的信息。
'angle': <function tensorflow.python.ops.math_ops.angle>,
'app': <module 'tensorflow.python.platform.app' from '/root/anaconda3/lib/python3.6/site-packages/tensorflow/python/platform/app.py'>,
'arg_max': <function tensorflow.python.ops.gen_math_ops.arg_max>,

第四部分 Import的过程

当我们执行import 模块、包时,主要有三个过程:检索、加载、名字绑定。

第一步:检索(Finder)

Python解释器会对模块所属位置进行搜索:

(1)检索:内置模块(已经加载到缓存中的模块)

内置模块(已经加载到缓存中的模块),即在 sys.modules 中检索。Python已经加载到内存中的模块均会在这个字典中进行登记。如果已经登记,不再重复加载。直接将模块的名字加入正在import的模块的namespace。可以通过下面方法查看:

1
2
3
4
5
6
7
8
9
>>> import sys
>>> print(sys.modules)
{'_signal': <module '_signal' (built-in)>, 'os.path': <module 'ntpath' from 'C:
\Anaconda3\\lib\\ntpath.py'>,pickle': <module 'pickle' from 'C:\\Anaconda3\\lib\\pickle.py'>,
#中间略
'subprocess':module 'subprocess' from 'C:\\Anaconda3\\lib\\subprocess.py'>, 'sys': <module '
ys' (built-in)>, 'ctypes.util': <module 'ctypes.util' from 'C:\\Anaconda3\\lib\
ctypes\\util.py'>, '_weakref': <module '_weakref' (built-in)>, '_imp': <module
_imp' (built-in)>}

如果不是built-in,value中会有模块的绝对路径信息。

通过key查找模块位置,如果value为None,就会抛出错误信息:ModuleNotFoundError

如果key不存在,就会进入下一步检索。

如果我们导入过包,例如tensorflow。当我们要使用其中模块,需要该模块的全名(即全路径信息),例如:tensorflow.examples.tutorials.mnist.input_data ,因为sys.modules中只有全路径的key。

1
2
3
4
import tensorflow
print(sys.modules)
##这个字典中会有tensorflow所有子包、模块的信息和具体的路径。
#'tensorflow.examples.tutorials.mnist.input_data': <module 'tensorflow.examples.tutorials.mnist.input_data' from '/root/anaconda3/lib/python3.6/site-packages/tensorflow/examples/tutorials/mnist/input_data.py'>

(2)检索 sys.meta_path

逐个遍历其中的 finder 来查找模块。否则进入下一步检索。

(3)检索模块所属包目录

如果模块Module在包(Package)中(如import Package.Module),则以Package.__path__为搜索路径进行查找。

(4)检索环境变量

如果模块不在一个包中(如import Module),则以 sys.path 为搜索路径进行查找。

如果上面检索均为找到,抛出错误信息:ModuleNotFoundError

第二步:加载(Loader)

加载完成对模块的初始化处理:

  • 设置属性。包括__name____file____package____loader__

  • 编译源码。编译生成字节码文件(.pyc文件),如果是包,则是其对应的__init__.py文件编译为字节码(*.pyc)。如果字节码文件已存在且仍然是最新的(时间戳和py文件一致),则不会重新编译。

  • 加载到内存。模块在第一次被加载时被编译,载入内存,并将信息加入到sys.modules中。

也可以强制用reload()函数重新加载模块(包)。

第三步:名字绑定

将模块和包的命名空间信息导入到当前执行Python文件的namespace(命名空间)。

第五部分 将模块、包的路径加入检索路径

讲完了枯燥的理论背景,下面我们来介绍实际应用。当你写好一个模块文件,如何正确完成import模块?主要有下面两类方法:

5.1 动态方法(sys.path中添加)

我们知道检索路径中sys.path,所以可以在import模块之前将模块的绝对路径添加到sys.path中。同样导入包需要加入包的文件夹绝对路径。具体方法如下:

1
2
3
4
5
import sys
##sys.path.append(dir)
sys.path.append('your\module(package)\file\path')
##sys.path.insert(pos,dir)
sys.path.insert(0,'your\module(package)\file\path')

注意:`

1、这里pos参数是插入sys.path这个list数据的位置,pos=0,即list第一位,优先级高。

2、需要注意有效范围,python程序向sys.path添加的目录只在此程序的生命周期之内有效。程序结束,失效。所以这是一种动态方法。

1
2
3
4
5
6
7
8
9
#win7
import sys
print(sys.path)
#输出
['', 'C:\\Python27\\lib\\site-packages\\pip-8.1.1-py2.7.egg', 'C:\\windows\\syst
em32\\python27.zip', 'C:\\Python27\\DLLs', 'C:\\Python27\\lib', 'C:\\Python27\\l
ib\\plat-win', 'C:\\Python27\\lib\\lib-tk', 'C:\\Python27', 'C:\\Users\\rongxian
g\\AppData\\Roaming\\Python\\Python27\\site-packages', 'C:\\Python27\\lib\\site-
packages']

5.2 静态方法

(1)另外检索路径还有系统环境变量,所以可以将模块(包)路径添加在系统环境变量中。

(2)粗暴一点直接将模块(包)拷贝到sys.path的其中一个路径下面。但是这种管理比较乱。

(3)Python在遍历sys.path的目录过程中,会解析 .pth 文件,将文件中所记录的路径加入到sys.path ,这样.pth 文件中的路径也可以找到了。例如我们在C:\Python27\lib\site-packages 中新建一个.pth文件。例如:

1
2
# .pth file for the your module or package
'your\module(package)\file\path'

这样在模块(包)上线时,我们只需要将模块(包)的目录或者文件绝对路径放在新建的.path文件中即可。

参考文章

1、http://www.cnblogs.com/russellluo/p/3328683.html#

2、https://github.com/Liuchang0812/slides/tree/master/pycon2015cn

本文标题:深入理解Python语言中import机制

文章作者:rong xiang

发布时间:2018年04月14日 - 10:04

最后更新:2022年10月25日 - 23:10

原始链接:https://zjrongxiang.github.io/posts/ad8441/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%