写给新手的Python导入机制详解

Posted qiaoxg-2018

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了写给新手的Python导入机制详解相关的知识,希望对你有一定的参考价值。

原创作品,转载引用请注明出处及作者

开篇说明

正文

  ‘隐藏在导入背后的’对象

  命名空间、dir函数与代码复用

  import分两步操作

  同一模块只会加载一次

  模块搜索顺序

  sys.path[0]

  包机制

  包的实质

  from...import

  相对导入

总结

  

开篇说明

本文旨在帮助Python新手对Python导入机制应用层面做一个全面了解。由于我也是一个Python新手,所以通篇介绍的只是应用层面的内容,导入机制背后的函数等诸多细节没有涉及。如果要深究,请自行参考Python语言参考手册。本文基于Python3.6.4版本写就,由于版本的不同,文中某些地方可能会有所偏差,但对于了解Python导入机制全貌也无多大影响。强烈推荐各位新手朋友,从头开始阅读本文,并加以实践,相信能通篇看完并实践的读者会对使用Python导入产生更深刻的了解,用起来更加得心应手。文中某些用词和对概念的描述,以及引用资料的翻译,鉴于本人只是新手且非专业翻译,亦会出现不精确之处,请多多见谅,但我会尽量做到使大家“意会”。若出现不正确之处,还请批评指出。

让我们开始吧。

正文

Python通过导入机制,帮助程序员实现代码复用,也很好地解决了多命名空间内变量名可能相互冲突的问题。

‘隐藏在导入背后的’对象

为了理解好Python的导入机制,首先要了解一个重要概念:Python中一切皆对象,变量名只是对一个对象的引用。模块有对应的模块对象,我们可通过模块名引用这个对象;函数有对应的函数对象,通过函数名引用这个函数对象。

举例说明一下。通过在命令行界面执行python命令,我们进入Python解释器的交互模式。让我们定义一个函数,看一看函数对象是什么样。

C:\WINDOWS\system32>python
Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def func1(x):           #定义func1函数,返回x+1的值。
...     return x+1
...
>>> func1                   #查看函数名func1到底指代什么
<function func1 at 0x034CD660>   #说明func1指向内存中某处function对象
>>> type(func1)
<class function>               #function对象是function类的一个实例
>>> a=func1(2)              #执行函数
>>> a
3
>>> f=func1                 #把函数名赋值给变量f
>>> f
<function func1 at 0x034CD660>   #查看f的值,发现f和func1指向同一个function对象
>>> type(f)
<class function>#
>>> b=f(2)                       #通过变量f执行函数,结果和通过func1一样
>>> b
3

通过以上例子,我们发现,定义一个函数会产生一个函数对象,在内存中有自己的‘地盘’。调用函数实际上是通过函数名找到这个函数对象并执行,无论函数名是什么,只要让Python解释器知道,哪个函数名指向哪个函数对象,就没什么问题。因此,我们可以这样认为,def func1(x)这条语句被解释器执行时,解释器先实例化函数对象,给这个函数在内存中分好‘地盘’,然后把函数名func1这个变量指向这个函数对象,以后想调用这个函数时,直接用函数名func1就可以了。把func1再赋值给另一个变量f,那么使用被赋值的变量f也可以找到这个函数对象。

了解完函数对象,我们再来看一看模块对象。在Python中有一个内置模块,sys模块,我们先拿它来举例子。

>>> sys=在sys模块被导入前,我先叫sys~~~~     #先定义一个sys变量看一看
>>> sys
在sys模块被导入前,我先叫sys~~~~
>>> type(sys)                              #确认变量sys指向一个字符串实例
<class str>
>>> import sys                             #导入sys模块
>>> sys                                    #变量sys现在指向内建模块sys对象
<module sys (built-in)>
>>> type(sys)                              #sys模块对象是一个module实例          
<class module> 

我们通过import语句导入了一个内置模块sys。和定义一个函数时相类似,sys模块被导入会实例化一个模块对象,然后解释器会用sys变量指向这个模块对象。原本sys变量指向一个字符串,现在那个字符串没人引用了,会被当作垃圾回收。我们也可以像上面的函数对象一样,给这个模块对象再加一个名字,比如china或usa,都是可以的。当然了,这种重命名没什么意义,但至少是一种思路。

>>> import sys
>>> id(sys)
51619232
>>> china=sys
>>> usa=sys
>>> china,usa
(<module sys (built-in)>, <module sys (built-in)>)
>>> id(china)
51619232
>>> id(usa)                #通过id函数查看变量引用的对象
51619232
>>> sys is usa is china    #is根据id函数的返回值进行判断
True

命名空间、dir函数与代码复用

至此,我们依然没有见到导入模块能带来的最大好处是什么。一开始我们就提到,通过导入,程序员可以实现代码复用。那sys模块中有那些代码能被复用呢?我们如何引用这些可用的代码呢?其实,Python是这样来创建sys模块对象的,导入一个sys模块时,首先是找到这个模块,然后会从头到尾执行这个模块,遇到def就创建一个函数对象,然后赋上函数名,遇到模块中被赋值的全局变量,那就创建这个赋值号右侧的对象,然后赋给变量。这个过程其实就是sys模块对象创建的过程,这些sys模块中的变量和函数,是属于被导入的sys模块的,它们的名字在sys模块的命名空间中,而sys模块对象属于上一级模块,sys这个变量属于上一级模块的命名空间。上一级模块指的是执行import sys语句的地方。我们是用交互式的python解释器在执行这些代码,可以通过dir函数,看一看,顶层命名空间里有那些变量。

>>> dir()
[__annotations__, __builtins__, __doc__, __loader__, __name__, __package__, __spec__, a, b, china, f, func1, sys, usa] 

其中,a,b是两个指向int对象的变量,f和func1是两个指向同一个函数对象的函数名,sys、china和usa都是指向sys模块对象的模块名。

sys模块中定义了很多有用的变量和函数。我们通过dir函数看一下sys模块命名空间内的变量

>>> dir(sys)
[__displayhook__, __doc__, __excepthook__, __interactivehook__, __loader__, __name__, __package__, __spec__, __stderr__, __stdin__, __stdout__, _clear_type_cache, _current_frames, _debugmallocstats, _enablelegacywindowsfsencoding, _getframe, _git, _home, _xoptions, api_version, argv, base_exec_prefix, base_prefix, builtin_module_names, byteorder, call_tracing, callstats, copyright, displayhook, dllhandle, dont_write_bytecode, exc_info, excepthook, exec_prefix, executable, exit, flags, float_info, float_repr_style, get_asyncgen_hooks, get_coroutine_wrapper, getallocatedblocks, getcheckinterval, getdefaultencoding, getfilesystemencodeerrors, getfilesystemencoding, getprofile, getrecursionlimit, getrefcount, getsizeof, getswitchinterval, gettrace, getwindowsversion, hash_info, hexversion, implementation, int_info, intern, is_finalizing, maxsize, maxunicode, meta_path, modules, path, path_hooks, path_importer_cache, platform, prefix, ps1, ps2, set_asyncgen_hooks, set_coroutine_wrapper, setcheckinterval, setprofile, setrecursionlimit, setswitchinterval, settrace, stderr, stdin, stdout, thread_info, version, version_info, warnoptions, winver]

 

如何利用这些变量实现代码复用呢?我们可以通过sys.platform的形式引用了sys模块对象中platform变量指向的对象,一个表示系统平台的字符串对象。复用sys模块对象里的函数对象,也是很容易书写。下面的例子中没给函数参数,只是看一看它们长什么样。

>>> sys.platform
win32  
>>> sys.gettrace
<built-in function gettrace>
>>> sys.getsizeof
<built-in function getsizeof>

这便是对Python中代码复用的基本理解。我们通过导入一个模块,形成一个模块对象,形成模块对象的过程也是被导入模块中代码执行的过程,通过这个过程,被导入模块中的各种对象(函数对象,全局字符串对象,列表对象等)也被实例化。调用时就用到sys.platform的方式调用。记住,一切都是对象,变量名只是对对象的引用。举个形象的例子加深理解。一个中学,名字叫做育才中学,位于A省B市C区,这个中学就相当于我们执行的主脚本或者Python交互模式。它本身有三个年级,有教务处,校长室等(成员),同时提供各项职能(函数)。有一天,上面下发通知,你这学校搞得不赖,给你并个小学部吧,于是import 育才小学,那么在育才中学的管理范围呢,多了育才小学这个名字。这个育才小学也是不简单啊,位于A省B市D区,本身有六个年级,有教务处,校长室等(成员),同时提供各项职能(函数)。这个新并进来的育才小学就类似被导入的sys模块。首先,这小学是个真实的学校(实例化的对象),有地址A省B市D区(在内存中有地盘),叫育才小学(模块名),有成员和职能(拥有自己的命名空间以及相应的各种对象)。要春节了,育才中学发布通知,育才小学率先放假,我们就可以把它想象成育才中学调用了育才小学.放假(全体学生)这个职能(函数)。

好了,有了以上说明形成的对Python导入的基本理解,我们就很容易更加细节和深入地掌握Python导入机制。

import分两步操作

看一看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.

 

不带from从句的基本import语句分成两步执行:

  1.找到指定的模块,必要时载入并初始化

  2.在import语句执行的地方为本地命名空间定义变量名

如果import语句包括由逗号分割的多个子句,那么为每个子句分别执行以上两步操作,类似把子句中的模块分别单独导入。

这段话很好地说明了我们举例导入sys模块的情况,但第二步的解释不够完整,我们再引用Python语言参考手册3.6.4中的一句话:

The import statement combines two operations; it searches for the named module, then it binds the results of that search to a name in the local scope. 

 

import语句包含两步操作,搜索指定的模块,然后在本地命名空间内把搜索的结果和一个变量名进行绑定。

综合这两处资料,正好符合我们对导入sys模块的说明。找到要导入的模块,载入并初始化该模块,在执行import语句的地方给实例化的模块对象绑定变量名以便引用。步骤一提到必要时载入并初始化被导入的模块,为什么要加‘必要时’这句话呢?

同一模块只会加载一次

我们知道,导入模块会创建模块对象,为其分配内存,其内部包含大量的函数,每个函数也对应一个函数对象的创建,因此,导入一个模块很消耗资源并减缓执行速度。一个常被用到的模块,比如random模块,可能在程序中的多个文件中都会被用到,难道每一次用到都给它创建一个全新的模块对象吗?那开销太大了。因此,Python的导入机制中,在整个程序执行周期内,同一模块只会被加载初始化一次,多个不同的模块都可以使用import random引入random模块到自己的命名空间里,但这些不同模块命名空间内的random变量指向的模块对象都是同一个(最先导入时创建的那个)。请看底下的介绍,提前说明,sys.modules是个字典对象。

The module cache

The first place checked during import search is sys.modules. This mapping serves as a cache of all modules that have been previously imported, including the intermediate paths. So if foo.bar.baz was previously imported, sys.modules will contain entries for foofoo.bar, and foo.bar.baz. Each key will have the corresponding module object as its value .

During import, the module name is looked up in sys.modules and if present, the associated value is the module satisfying the import, and the process completes. However, if the value is None, then a ModuleNotFoundError is raised. If the module name is missing, Python will continue searching for the module.

 

模块缓存

import查找操作检查的第一处地方是sys.modules变量。此映射作为以前被导入的所有模块的缓存,包括中间路径。因此,如果foo.bar.baz以前被导入过,那么sys.modules中会有模块foo,foo.bar和foo.bar.baz的入口。每个键会把其相应的模块对象作为其值。

导入过程中,查找的模块名如果在sys.modules的键中,那么键相应的值就会被认为是要找的模块对象,导入过程直接完成。如果,键对应的值为None,会报ModuleNotFoundError错误。如果没有和要导入的模块同名的键,Python才会进行下一步查找。

也就是说,Python使用一个字典类型的变量sys.modules来存储任何已经被导入过的模块对象的信息,形式是模块名:模块对象这样的键值对。然后遇到重复导入时,发现这个模块名在字典的键中已经有过了,那么就把这个键对应的值--模块对象直接交给重复导入语句执行的模块,让它在自己的名称空间新建属性,指向这个交过来的结果。这样,多个模块中对某个模块的引用,实际上都是指向唯一的一个模块对象。大家一定要实践一下,看看sys.modules到底长什么样。

可能有人会问这样一个问题,导入模块时要查询变量sys.modules,那我们import sys时向谁查询?到底是先有蛋,还是先有鸡?其实,Python中sys模块与一般模块有所不同,请大家往下看。

模块搜索顺序

前面我们一直在说,找到指定的模块,加载并初始化它,但怎么找到指定的模块呢?或者是按照什么顺序找到这个模块呢?怎么找我们不管,反正是Python解释器去找,我们只说按照什么顺序去找这个模块。

我们引用Python-3.6.4-docs中Tutorial的一段说明:

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. 

 

当一个名叫spam的模块被import时,解释器首先用这个名字搜索内置模块。如果没有发现叫做spam的内置模块,解释器会在一系列由sys.path变量提供的目录下搜索名叫spam.py的文件。sys.path变量从以下这些位置初始化:

  • 包含输入脚本的目录(如果没有指定文件,那么就是当前目录)
  • PYTHONPATH(和环境变量PATH用一样的语法组织的一系列目录路径)
  • 和安装有关的默认配置

完成初始化后,Python程序可以修改sys.path变量。包含运行脚本的目录会被放到搜索路径的开头,比标准库路径还要靠前,这便意味着在包含运行脚本的目录下的模块会屏蔽掉标准库里的标准模块。如果不是有意为之,这将是一个错误。

声明一下,因为版本的不同,这里的某些细节可能不大一致,大家可以自己搜索,这些doc其实在github上都是能找到修改历史的(doc的左边栏Show Source)。

我们仔细研究一下这些说明,首先看内置模块和标准模块。Python的标准模块有很多,其中一些用C语言实现的,编译到解释器中的,称为内置模块;另外的是用Python语言写成的,有源码文件随Python一起安装的模块,占标准模块的大部分。

看一下Python标准库参考手册中的说法:

The bulk of the library, however, consists of a collection of modules. There are many ways to dissect this collection. Some modules are written in C and built in to the Python interpreter; others are written in Python and imported in source form.

 

标准库的大部分是由一系列的模块构成。可以有多种方式分类这些标准模块。一些模块用C语言编写并被编译到Python解释器中,其余的用Python语言编写,通过源码的方式被导入。

以下是Python-3.6.4-docs中tutorial的一段对内置模块的说明:

Some modules are built into the interpreter; these provide access to operations that are not part of the core of the language but are nevertheless built in, either for efficiency or to provide access to operating system primitives such as system calls. The set of such modules is a configuration option which also depends on the underlying platform. For example, the winreg module is only provided on Windows systems. One particular module deserves some attention: sys, which is built into every Python interpreter. 

 

一些模块被编译到解释器当中去,这些模块虽然不是Python语言的核心组成,但出于效率考虑或为了支持系统调用等操作系统原语操作,这些模块也被编译进了核心。这些模块同时也是取决于底层平台的配置选项。例如,winreg模块仅在windows系统上被编译进核心。一个特殊的模块值得注意,sys模块,它被编译到每一个Python解释器当中。

提到sys模块,我又想起介绍sys.modules时提到的先有鸡,还是先有蛋的问题。既然导入一个模块要先检查sys.modules以确定是否已经导入过,那import sys时检查什么?sys模块还没被导入,又哪来的sys.modules变量?其实,有一些模块是例外,比如sys模块,由于它提供了一些Python运行必要的环境,所以它们随着解释器的启动而被自动加载,import sys,只是在命名空间内使得sys变量指向已被自动加载的模块对象。自动加载对象和内置模块还不完全一样,大家也不要深究了。

我们可以通过sys.builtin_module_names变量查看自己系统上内置模块有那些。

>>> import sys
>>> sys.builtin_module_names
(_ast, _bisect, _blake2, _codecs, _codecs_cn, _codecs_hk, _codecs_iso2022, _codecs_jp, _codecs_kr, _codecs_tw, _collections, _csv, _datetime, _functools, _heapq, _imp, _io, _json, _locale, _lsprof, _md5, _multibytecodec, _opcode, _operator, _pickle, _random, _sha1, _sha256, _sha3, _sha512, _signal, _sre, _stat, _string, _struct, _symtable, _thread, _tracemalloc, _warnings, _weakref, _winapi, array, atexit, audioop, binascii, builtins, cmath, errno, faulthandler, gc, itertools, marshal, math, mmap, msvcrt, nt, parser, sys, time, winreg, xxsubtype, zipimport, zlib)

 

上面是windows系统下的执行结果,Linux下的执行结果与之有所区别。

>>> import sys
>>> sys.builtin_module_names
(_ast, _codecs, _collections, _functools, _imp, _io, _locale, _operator, _signal, _sre, _stat, _string, _symtable, _thread, _tracemalloc, _warnings, _weakref, atexit, builtins, errno, faulthandler, gc, itertools, marshal, posix, pwd, sys, time, xxsubtype, zipimport)

 

当import spam时,解释器并没有首先去查找spam.py,而是按照sys.modules[‘spam‘]==>内置模块==>sys.path的顺序一步步搜索过来。

上面资料引用提到sys.path变量从以下这些位置初始化:

  • 包含输入脚本的目录(如果没有指定文件,那么就是当前目录)
  • PYTHONPATH(和环境变量PATH用一样的语法组织的一系列目录路径)
  • 和安装有关的默认配置

sys.path[0]

这里面比较令人困惑的是第一条--包含输入脚本的目录(如果没有指定文件,那么就是当前目录)。这里我们还是要引用官方文档的说明:

sys.path

A list of strings that specifies the search path for modules. Initialized from the environment variable PYTHONPATH, plus an installation-dependent default.

As initialized upon program startup, the first item of this list, path[0], is the directory containing the script that was used to invoke the Python interpreter. If the script directory is not available (e.g. if the interpreter is invoked interactively or if the script is read from standard input), path[0] is the empty string, which directs Python to search modules in the current directory first. Notice that the script directory is inserted before the entries inserted as a result of PYTHONPATH.

 

 

sys.path是由字符串构成的一个列表,明确了搜索模块的路径。这个列表通过环境变量PYTHONPATH被初始化,另外还要依赖于安装时的默认配置。

这个列表的第一个元素,sys.path[0],在程序启动时被设置为启动Python解释器的脚本的所在目录。如果脚本所在目录不可用(比如,解释器通过交互模式启动或者脚本来自于标准输入),那么path[0]会设置成空字符串,这个空字符串指示解释器首先在当前目录下搜索模块。注意,脚本所在目录插入在PYTHONPATH的内容之前。

 这段解释还是很详细的,再通过我们的验证,很容易就能理解了。

首先,我们为系统添加一个环境变量PYTHONPATH,然后命令行中用set命令查看一下其内容

PYTHONPATH=D:\py\included;  

接下来,我们在D盘的根目录下新建一个文件test01.py,其内代码为:

import sys
print(sys.path)  

再操作命令行窗口,执行这个脚本,注意观察执行脚本的当前目录和脚本的路径。

C:\WINDOWS\system32>python D:\test01.py
[D:\\, D:\\py\\included, C:\\WINDOWS\\system32, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]  

我们是在C:\WINDOWS\system32目录下,通过python D:\test01.py的方式执行了这个脚本,而sys.path[0]是‘D:\\‘,这个就属于上面提到的情况--“这个列表的第一个元素,sys.path[0],在程序启动时被设置为启动Python解释器的脚本所在目录。“;同时,环境变量PYTHONPATH的值‘D:\\py\\included‘紧跟sys.path[0];还要注意一点,我们当前启动命令行窗口的目录‘C:\\WINDOWS\\system32‘被放到了PYTHONPATH之后,默认的搜索路径之前,文档中没提到这种情况,在linux平台上也没有这一情况,不知道是平台的缘故还是版本的原因了。

再验证一个事情:

C:\Windows>cd /D D:

D:\>python test01.py
[D:\\, D:\\py\\included, D:\\, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]

我们在包含测试脚本的目录下启动命令行,执行python test01.py命令,发现和在其他目录下执行类似命令对sys.path的影响是一致的。为什么要做这样一个测试呢,因为有另一种情况和这个相类似,但对sys.path的影响不同,时常令人迷惑。请看下面测试:

D:\>python
Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import test01
[‘‘, ‘D:\\py\\included‘, ‘D:\\‘, ‘C:\\Program Files (x86)\\Python36-32\\python36.zip‘, ‘C:\\Program Files (x86)\\Python36-32\\DLLs‘, ‘C:\\Program Files (x86)\\Python36-32\\lib‘, ‘C:\\Program Files (x86)\\Python36-32‘, ‘C:\\Program Files (x86)\\Python36-32\\lib\\site-packages‘]  

我们在包含test01.py的D:\目录下,执行python,启动了交互模式,通过import test01,我们查看到sys.path[0]为空字符串--这个符合“如果脚本所在目录不可用(比如,解释器通过交互模式启动或者脚本来自于标准输入),那么path[0]会设置成空字符串,这个空字符串指示解释器首先在当前目录下搜索模块。”

回头想一下在D:\下执行python test01.py不就等于python D:\test01.py吗?而在D:\下启动python的交互模式,我们import test01,test01.py一定要位于D:\吗?不需要的,这个时候就是说脚本所在目录不可用(你无法确定)。启动python解释器的方式不同,对sys.path[0]的设置就有不同的影响。还是引用上面的说明--这个列表的第一个元素sys.path[0],在程序启动时被设置为启动python解释器的脚本的所在目录;如果脚本所在目录不可用(比如,解释器通过交互模式启动或者脚本来自于标准输入),那么path[0]会设置成空字符串,这个空字符串指示解释器首先在当前目录下搜索模块。

继续举例加深理解。我们在D:\新建一个文件test02.py

print(in D:\\test02.py)

在D:\test01.py中导入test02

import test02
import sys
print(sys.path)  

命令行中执行

C:\WINDOWS\system32>python D:\test01.py
in D:\test02.py
[‘D:\\‘, ‘D:\\py\\included‘, ‘C:\\WINDOWS\\system32‘, ‘C:\\Program Files (x86)\\Python36-32\\python36.zip‘, ‘C:\\Program Files (x86)\\Python36-32\\DLLs‘, ‘C:\\Program Files (x86)\\Python36-32\\lib‘, ‘C:\\Program Files (x86)\\Python36-32‘, ‘C:\\Program Files (x86)\\Python36-32\\lib\\site-packages‘]  

我们知道,test01导入test02时,test02中的可执行代码会执行,打印出in D:\test02.py。test01中import test02,python解释器就是根据sys.path[0]为‘D:\\‘率先找到了test02的。

再以交互模式执行一下。

C:\WINDOWS\system32>python
Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import test01
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named test01  

在C:\WINDOWS\system32中是找不到module ‘test01‘的,更别说‘test02‘了。

跑到D:\下执行交互模式。

D:\>python
Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import test01
in D:\test02.py
[‘‘, D:\\py\\included, D:\\, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]  

是可以的,但应当看到,python解释器虽然也是在sys.path[0]中率先找到了test02的,但此时为空字符串,背后是基于空字符串指示的当前目录。

我们假设一种情况,假如test01和test02所在的目录D:\也在PYTHONPATH中,但属于最后一位;同时呢,属于PYTHONPATH的D:\py\included目录中也有一个文件叫做test02.py,它的内容是:

print(rin D:\py\included\test02.py)  

我们在C:\WINDOWS\system32中执行python D:\test01.py是什么结果?执行交互模式又会是什么结果呢?在D:\或者D:\py\included下执行交互模式呢?

C:\WINDOWS\system32>set PYTHONPATH         #查看PYTHONPATH的值
PYTHONPATH=D:\py\included;D:\;

C:\WINDOWS\system32>python D:\test01.py    #C盘目录下执行脚本
in D:\test02.py                            #test01.py所在目录被放到搜索路径第一位,test02沾光了
[D:\\, D:\\py\\included, D:\\, C:\\WINDOWS\\system32, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]

C:\WINDOWS\system32>python                 #C盘下执行交互模式
Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import test01                          #这一步是在PYTONPATH提供的D:\目录下找到了test01.py
in D:\py\included\test02.py     #sys.path[0]指向的是C:\WINDOWS\system32,而‘D:\\py\\included‘次之,所以这里的test02.py先被找到
[‘‘, D:\\py\\included, D:\\, C:\\WINDOWS\\system32, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]
>>> ^Z


C:\WINDOWS\system32>cd /D D:               #切换到D:\目录

D:\>python
Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import test01 #因为这里sys.path[0]指的就是D:\,所以D:\test01.py根据sys.path中的首项即被找到
in D:\test02.py   #和test01一样,也是根据首项空字符串指向当前进入交互模式的目录被找到
[‘‘, D:\\py\\included, D:\\, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]
>>> ^Z


D:\>cd .\py\included  #切换到D:\py\include目录

D:\py\included>python
Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import test01  #根据PYTHONPATH提供的D:\目录被找到
in D:\py\included\test02.py  #根据首项空字符串指向当前进入交互模式的目录被找到
[‘‘, D:\\py\\included, D:\\, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]
>>> ^Z


D:\py\included>python D:\test01.py #在included目录下以执行脚本的形式启动
in D:\test02.py #因为是通过执行脚本的方式启动的解释器,所以sys.path[0]被设置成D:\,D:\test02.py因此被找到
[D:\\, D:\\py\\included, D:\\, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]

至此,import sys这种最简单最基本的导入形式,我们弄明白了。但我们又意识到潜在的多个问题。就像上面举例的情况,test01.py希望导入的就是自己同级目录下的模块test02,它不希望导入其它的搜索路径下的test02,难道每次都把test01.py当作脚本运行,使得sys.path[0]为‘D:\’?难道每次都cd到它俩所在的目录去执行交互模式?显然是不可能的,要知道,导入这种操作,太常用了,不同的模块我导入你,你导入他,来回能嵌套很多层,不可能每次都是当作脚本运行,也不可能交互模式下运行,那样到底怎么解决?

包机制

Python引入包的概念来解决这一问题,package机制使得我们可以有层次地组织和管理我们的模块。包思想在我们生活中有很多的体现。比如一个学校的学生可能成百上千人,同名同姓的情况也是时有发生,我们把每个学生想象成一个模块,如果从全校学生中直接喊名字找到某个人,就会出现我们上面提到的问题。实际上,每个学校都是按照年级,班级的形式组织的,这样,喊某个年级某个班的某某同学,是不是发生冲突的概率就小了呢?我们再合理分配一下,使得相同名字的同学去不同的班,那么不会再找不到某个人了吧?包机制实际上降低了命名冲突的概率,使得我们更容易控制,同时,对于一个班的学生A和B,我们是不是对A说,你们班B怎么怎么,这种方式被Python实现为包内模块的相对引用。

让我们看一下,Python中包的庐山真面目吧。下面多处引用是我综合Python语言参考手册、Tutorial和标准库参考重新组织的:

You can think of packages as the directories on a file system and modules as files within directories, but don’t take this analogy too literally since packages and modules need not originate from the file system. For the purposes of this documentation, we’ll use this convenient analogy of directories and files. Like file system directories, packages are organized hierarchically, and packages may themselves contain subpackages, as well as regular modules.

The __init__.py files are required to make Python treat the directories as containing packages; this is done to prevent directories with a common name, such as string, from unintentionally hiding valid modules that occur later on the module search path. In the simplest case, __init__.py can just be an empty file, but it can also execute initialization code for the package

 

你可以认为包是系统中的目录,而模块是这些目录里的文件,但不要把这种类比想的太绝对,太字面化,因为包和模块不一定非要通过文件系统来组织。为了本文档的编写,我们就采用这种方便的目录和文件的类比。就像文件系统中的目录一样,包是按层次组织的,包可以包含常规的模块,本身也可以包含子包。

目录中需要有一个名叫__init__.py的文件,这样,Python解释器才会把目录当作包来对待。之所以这样要求,是为了防止命名为string这种通用名称的目录,在之后的搜索上,无意间屏蔽了合法的模块。最简单的情况下__init__.py文件可以什么都没有,就是个空文件(只用来表示这是一个包),另外的情况下它也可以为包执行一些初始化操作。

一个sound文件夹,里面有一个空的__init__.py文件,告诉解释器,sound是一个包;还有3个子文件夹,每个子文件夹里也有空的__init__.py文件和几个普通的模块文件,ok,解释器理解,这三个是子包,子包内有普通模块。这是包组织的最简单形式,也是最常见的。同时,我们也看到,具有相同功能的模块被放到了同一个子包里,这是不是为我们管理代码提供了方便呢?包不仅解决命名冲突的问题,也帮助我们能按功能分类管理代码。

sound/                          Top-level package
      __init__.py               Initialize the sound package
      formats/                  Subpackage
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              ...
      effects/                  Subpackage
              __init__.py
              echo.py
              surround.py
              reverse.py
              ...
      filters/                  Subpackage
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py

sound文件夹需要放到Python解释器可以搜索到的地方去,也就是我们前面提到的sys.path,其中sys.path[0]的规则,PYTHONPATH和默认位置对这个包都可用。比如,我电脑上的E盘根目录没有被添加到PYTHONPATH变量中,然后我们把sound文件夹放到E盘根目录下,在E盘新建一个test01.py,其内容是:

import sound.effects.echo
import sys
print(我是sound包==>,sound)
print(我是sound包的子包effects==>,sound.effects)
print(我是sound包的子包effects内的模块echo==>,sound.effects.echo)
print(我们都被导入到,__file__)
print(看一下sys.path,再温习一下sys.path[0]的规则吧)
print(sys.path)

通过命令行以脚本的形式执行test01.py

C:\WINDOWS\system32>python E:\test01.py
我是sound包==> <module sound from E:\\sound\\__init__.py>
我是sound包的子包effects==> <module sound.effects from E:\\sound\\effects\\__init__.py>
我是sound包的子包effects内的模块echo==> <module sound.effects.echo from E:\\sound\\effects\\echo.py>
我们都被导入到 E:\test01.py
看一下sys.path,再温习一下sys.path[0]的规则吧
[E:\\, D:\\py\\included, D:\\, C:\\WINDOWS\\system32, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]

信息量很大,一一说来。

我们使用import sound.effects.echo的形式导入包中的独立模块echo,实际上echo这个标识符对于test01模块是不可见的,就好像整个学校管理中,校长主要管理

全校的各个年级,年级组长管理本年级的各个班,班主任才知道班中每个同学的名字。校长找某个同学,一般也是一级一级的通过年级.班级的形式找过来的,这就是层次的命名空间的概念。我们在交互模式下用dir(test01)函数能够查看test01模块对象有那些它可见的标识符(可以理解成它的属性)。

C:\WINDOWS\system32>set PYTHONPATH=%PYTHONPATH%E:\;   #为了交互模式下导入test01,所以把E;\添加到PYTHONPATH中

C:\WINDOWS\system32>set PYTHONPATH
PYTHONPATH=D:\py\included;D:\;E:\;

C:\WINDOWS\system32>python
Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import test01
我是sound包==> <module sound from E:\\sound\\__init__.py>
我是sound包的子包effects==> <module sound.effects from E:\\sound\\effects\\__init__.py>
我是sound包的子包effects内的模块echo==> <module sound.effects.echo from E:\\sound\\effects\\echo.py>
我们都被导入到 E:\test01.py
看一下sys.path,再温习一下sys.path[0]的规则吧              #sys.path[0]之所以为空字符串,和前面介绍过的规则有关
[‘‘, D:\\py\\included, D:\\, E:\\, C:\\WINDOWS\\system32, C:\\Program Files (x86)\\Python36-32\\python36.zip, C:\\Program Files (x86)\\Python36-32\\DLLs, C:\\Program Files (x86)\\Python36-32\\lib, C:\\Program Files (x86)\\Python36-32, C:\\Program Files (x86)\\Python36-32\\lib\\site-packages]
>>> dir(test01)    #sound标识符是在test01的命名空间里的
[__builtins__, __cached__, __doc__, __file__, __loader__, __name__, __package__, __spec__, sound, sys]
>>> dir(test01.sound)    #effects标识符在包sound的命名空间里的
[__builtins__, __cached__, __doc__, __file__, __loader__, __name__, __package__, __path__, __spec__, effects]
>>> dir(test01.sound.effects)   #echo标识符是在子包effects的命名空间里的
[__builtins__, __cached__, __doc__, __file__, __loader__, __name__, __package__, __path__, __spec__, echo]
>>>

 

在本文较早的地方,我们提到过1-Python中一切皆对象,还说到2-import操作分两步,找到并实例化指定的模块,在本地命名空间用一个名称绑定它。现在,大家应该对这两点有更深的了解了吧?

这种思想在涉及到包的导入时依然可用。导入sound包会实例化一个包对象(我们暂且称之为包对象),然后在导入这个包的地方,即test01的命名空间里,我们用sound变量指向这个包对象。在sound这个包对象里,又有个变量effects指向effects子包对象,effects子包对象里有个echo变量指向了echo模块对象。上面的例子结果恰恰验证了我们所说的,dir(test01)中只有sound,没有effects和echo,在test01模块中引用echo模块对象,我们只能使用sound.effects.echo的形式。可能有人想起来我们导入sys模块后,故意将sys变量赋给china和usa的事情,当时看起来这种赋值没什么利用价值,china和usa变量即便指向sys模块对象,也没什么好处啊。好处在这里体现了:test01.py中import sound.effects.echo后,如果echo模块中有个属性叫做a,我们在test01.py中若想用这个属性,必须用sound.effects.echo.a的形式才能使用a指向的对象,这个名字也太长了,如果代码中多次用到,光敲这一串名字都会令你无限憋屈。类似china=sys的操作,我们可以给sound.effects.echo在test01.py中赋个短一点的别名,比如echo,用到变量a时,我们直接使用echo.a,是不是方便很多呢?还可以拿学校的例子来比喻,校长一般不知道具体学生叫什么,可某个学生太牛了,全国竞赛一等奖获得者,给学校和校长挣足了脸面,相信校长也会认识他的,那位同学就像这个echo一样,大名在校长的圈子里流传了。

Python为我们提供了实现这种操作的简单方法,as从句。我们不需要import过后再执行赋值,可以直接import sound.effects.echo as echo,这样就能起到我们上面说的效果了。但有一点要注意,这种方法,sound包和effects子包对象虽然创建了(通过sys.modules查看),但在test01的命名空间里我们不能再引用了,test01的命名空间内只是新增了echo变量,echo指向sound.effects.echo模块对象。

对于基本import形式:

If the requested module is retrieved successfully, it will be made available in the local namespace in one of three ways:

  • If the module name is followed by as, then the name following as is bound directly to the imported module.
  • If no other name is specified, and the module being imported is a top level module, the module’s name is bound in the local namespace as a reference to the imported module
  • If the module being imported is not a top level module, then the name of the top level package that contains the module is bound in the local namespace as a reference to the top level package. The imported module must be accessed using its full qualified name rather than directly

 

如果指定的模块被成功取回,解释器将通过以下三种方式中的一种使之在本地(import执行的模块)命名空间中变得可用。

  • 如果模块名后跟有as从句,那么as从句后的名称直接绑定被导入的模块。(就是我们刚才说的import sound.effects.echo as echo)
  • 如果没有其他名称被特别的指定,同时被导入的模块又是顶层模块(指的是不是包内的模块),那么在本地命名空间里模块名称将作为被导入模块实例的引用而被绑定。(import sys就符合这个规定)
  • 如果被导入的模块不是顶层模块,那么包含这个模块的最顶层包的名称将被绑定在本地命名空间,作为顶层包的引用。要想使用被导入的模块,必须使用自己的"全描述名称",而不是直接用模块名。(echo模块的全描述名称就是sound.effects.echo,这种情况说的就是我们没使用as之前对echo的使用方法,本地命名空间只能接触到sound)

上面用到的import sys、import a.b.c、import a.b.c as c这些形式,最终导入的只能是模块,对于模块中定义的的函数、类或变量,使用这种最基本形式的import语句是不能导入的。

>>> import sound.effects.echo.func1
Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    import sound.effects.echo.func1
ModuleNotFoundError: No module named sound.effects.echo.func1; sound.effects.echo is not a package
>>> import sound.effects.echo
>>> sound.effects.echo.func1
<function func1 at 0x05881810>

echo模块下的func1函数不能使用import sound.effects.echo.func1的形式被导入,只能当作模块对象的属性被引用,这多少有点不方便,因为代码复用的精髓就是函数调用。既然有那个函数对象存在,为什么不能像模块对象一样,直接赋值给本地命名空间的名称呢?强大优雅高效全能的python是不会不提供这种支持的,实现这种支持就是通过带from从句的import语句。在说from...import形式的导入之前,我们需要再回顾一下包-Package。

包的实质

一切皆对象,那“包对象”(这是我编的,前面说过,暂且称为包对象)长什么样?其实,我们执行test01.py的时候已经打印出包对象了,不知道大家注意到没。

我是sound包==> <module ‘sound‘ from ‘E:\\sound\\__init__.py‘>
我是sound包的子包effects==> <module ‘sound.effects‘ from ‘E:\\sound\\effects\\__init__.py‘>
我是sound包的子包effects内的模块echo==> <module ‘sound.effects.echo‘ from ‘E:\\sound\\effects\\echo.py‘>

观察一下sound包对象<module ‘sound‘ from ‘E:\\sound\\__init__.py‘>,sound包的子包对象effects<module ‘sound.effects‘ from ‘E:\\sound\\effects\\__init__.py‘>,是不是感到很奇怪,怎么这么像模块对象呢!它俩和模块对象echo<module ‘sound.effects.echo‘ from ‘E:\\sound\\effects\\echo.py‘>打印出来的效果一样,只是包显示的初始化文件都是自己目录下的__init__.py,而模块显示的初始化文件是自己。我们不禁要疑惑,难道包和模块在Python中属于同一种类型module?

答案是肯定的。

Python has only one type of module object, and all modules are of this type.To help organize modules and provide a naming hierarchy, Python has a concept of packages.

It’s important to keep in mind that all packages are modules, but not all modules are packages. Or put another way, packages are just a special kind of module. Specifically, any module that contains a __path__ attribute is considered a package.

Packages support one more special attribute, __path__. This is initialized to be a list containing the name of the directory holding the package’s __init__.py before the code in that file is executed. This variable can be modified; doing so affects future searches for modules and subpackages contained in the package.

All modules have a name. Subpackage names are separated from their parent package name by dots, akin to Python’s standard attribute access syntax. Thus you might have a module called sys and a package called email, which in turn has a subpackage called email.mime and a module within that subpackage called email.mime.text.

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.

When a submodule is loaded,a binding is placed in the parent module’s namespace to the submodule object. For example, if package spam has a submodule foo, after importing spam.foospam will have an attribute foo which is bound to the submodule. 

Importing sound.effects.echo will implicitly execute sound/__init__.py , sound/effects/__init__.py and sound/effects/echo.py. Subsequent imports of sound.filters or sound.formats will execute sound/filters/__init__.py and sound/formats/__init__.py respectively.

 

python中模块对象只有一种类型,所有的模块都是这种类型。为了帮助组织模块并提供有层次的命名体系,Python实现了包的思想。

很重要的一点是要在脑海中时刻保留这样一种认识,“所有的包都是模块,但不是所有的模块都是包”。换一种说法就是包是一种特殊的模块。需要特别指出的是,一个带有__path__属性的模块就会被认为是包。

包提供一个特殊的属性__path__。这个属性会在__init__.py执行前被初始化为一个列表,这个列表包含这个__init__.py文件所在的目录字符串。这个属性可以被改变,这样的话会影响到以后对这个包内部的子包和模块的搜索。

所有的模块都有一个名字,子包的名字和父包的名字之间用.小圆点分隔,类似于Python语法中对对象属性的读取。因此,你可能会有一个模块叫做sys,还有个包叫做email,email中有个子包叫做email.mime,子包里又有个模块叫做email.mime.text。

Python定义了两种类型的包,常规包和命名空间包。常规包是在Python3.2版本之前就支持的传统包。一个常规包典型的实现形式就是包含__init__.py的目录。当导入一个常规包时,就隐含执行__init__.py文件,__init__.py中定义的对象都会在这个包的命名空间中被绑定到某些名称(赋值给这个包命名空间里的一些变量)。__init__.py文件中可以写有其他模块可以写有的任何Python代码,当这个__init__.py文件被导入时,Python解释器会为它添加一些额外的属性(比如__path__)。

当子模块被加载(创建了一个子模块对象),在父模块的命名空间里会有一个变量指向这个子模块对象。例如,如果spam包有一个子模块foo,导入spam.foo后,spam会有一个属性foo指向子模块。

导入sound.effects.echo将隐含执行sound/__init__.py , sound/effects/__init__.py and sound/effects/echo.py文件。随后如果需要导入sound.filters包或者sound.formats包将会分别执行sound/filters/__init__.py和sound/formats/__init__.py文件(sound/__init__.py已经导入过了,在sys.modules中可找到)。

简单而言就是包是一种特殊的模块,具体表现为包内的__init__模块,不过这个模块比一般的模块多了一个__path__属性。

sound包的__init__模块其实代表了sound包,它的__path__属性就是sound目录的路径‘E:\\sound‘。当导入sound.effects时,sound包先被实例化,然后sys.modules[‘sound‘]会被赋值;接着解释器知道effects是sound包内的模块,所以解释器会跑到sound包的__init__模块的__path__属性目录下去找effects包。如果我们导入一个E盘的sound包后,把sound包模块的这个__path__属性给它改了,那么,接下来再导入sound包中的东西,甚至可以是C盘D盘中的模块。

>>> import sound.effects.echo
>>> sound
<module sound from E:\\sound\\__init__.py>
>>> sound.__path__
[E:\\sound]
>>> sound.effects
<module sound.effects from E:\\sound\\effects\\__init__.py>
>>> sound.effects.__path__
[E:\\sound\\effects]
>>> sound.effects.echo
<module sound.effects.echo from E:\\sound\\effects\\echo.py>
>>> sound.effects.echo.__path__
Traceback (most recent call last):
  File "<pyshell#16>", line 1, in <module>
    sound.effects.echo.__path__
AttributeError: module sound.effects.echo has no attribute __path__

 

文档中提到包还可以分成常规包和命名空间包,我们就不管命名空间包了,用到时再查资料。

包和一般模块都是模块,所以,在python的文档中有很多地方把包当作module来用,说到的submodule可能是subpackage,说的module可适用于package。

 from...import...

让我们回到开始回顾包的地方。“echo模块下的func1函数不能使用import sound.effects.echo.func1的形式被导入,只能当作模块对象的属性被引用,这多少有点不方便,因为代码复用的精髓就是函数调用。既然有那个函数对象存在,为什么不能像模块对象一样,直接赋值给本地命名空间的名称呢?强大优雅高效全能的Python是不会不提供这种支持的,实现这种支持就是通过带from从句的import语句。”但可能有些人又会说,为什么还要多搞个from,不能都一个import + as 搞定吗?这个问题我也不知道怎么回答,其中的设计哲学也许只能去问Python的发明者Guido了。还好,我们现在只是用Python,而不是创造Python。我们可以找一些表面理由来说服自己。

如果能直接用基本形式的import导入模块的属性,比如导入m模块的a1,a2,a3属性,就应该写成

import m.a1    #注意,不能直接这样导入属性(函数、类、变量)
import m.a2    #这些个例子不对
import m.a3  

看起来是很整齐,但是不是感到很别扭?

Python中实现这一需求真正用的是from...import的形式

from m import a1,a2,a3

看起来很舒服吧,一目了然,读起来甚至朗朗上口,从某处导入某某某。感谢Guido。

我们已经知道包实际上是带__path__的__init__模块,那么看来import sound是可行的,相当于导入sound/__init__模块,用dir(sound)函数看一下这个包模块的命名空间,果然有__path__属性,其他的双下划线属性我们不去管。那接着import sound.effects,再dir(sound),会多出来一个effects属性指向effects子包对象,都很正常,说的这些在前面例子中都有验证。__init__.py既然跟普通的模块没多大区别,里面也能写其他模块可写的代码,那我们在sound/__init__.py里写上代码:

effects=我是init-effects

重启交互模式,再来一次import sound。果然,dir(sound)中多了一个effects属性,打印它会显示我们定义的字符串。原本import sound.effects会给sound的命名空间增加一个effects属性指向子包,现在__init__.py中定义的effects属性是个字符串,矛盾出现了,这个属性在import sound.effects被执行后到底指向谁。

>>> import sound.effects
>>> dir(sound)
[__builtins__, __cached__, __doc__, __file__, __loader__, __name__, __package__, __path__, __spec__, effects]
>>> sound.effects
<module sound.effects from E:\\sound\\effects\\__init__.py>

原来指向了子包对象,想一想也对,是先导入的sound包,再sound包基础上找到的effects包,后面的属性绑定把原本的字符串覆盖了。因为包是一种特殊的模块,那只能有唯一一个effects属性,如果import sound.effects.echo.func1的形式可用,就必须保证包的__init__.py里面不能有和其同级的模块名字一样的变量,但谁能保证呢?为了能取到__Init__.py中的effects字符串,我们要用到from...import。

>>> from sound import effects
>>> effects
我是init-effects
>>> import sound.effects
>>> effects
我是init-effects
>>> sound.effects
<module sound.effects from E:\\sound\\effects\\__init__.py>

我们先一步用from...import的形式把字符串绑定到本地命名空间,这样就不会被覆盖了。

以上两个例子说明,from...import...这种形式还是很有必要的。当然了,例子可能很牵强,但请注意,表面理由嘛,嘎嘎。

Note that when using from package import item, the item can be either a submodule (or subpackage) of the package, or some other name defined in the package, like a function, class or variable. The import statement first tests whether the item is defined in the package; if not, it assumes it is a module and attempts to load it. If it fails to find it, an ImportError exception is raised.

Contrarily, when using syntax like import item.subitem.subsubitem, each item except for the last must be a package; the last item can be a module or a package but can’t be a class or function or variable defined in the previous item.

 

记住,当使用from package import item的形式时,item可以是包中的子模块(或者子包),也可以是在包中(__init__.py中)定义的其它名称,比如函数,类或变量。导入语句先验证item在包中是否被定义,如果没有,解释器会把item当作一个模块,然后试图加载它。如果没有找到,会产生ImportError报错。

与之相反的,当使用形如import item.subitem.subsubitem的import语法时,除了最后那一个item,其余每一级item都必须是个包,最后的item可以是一个模块,也可以是一个包,但绝不能是前一个item中定义的类、函数或者变量。

下面我们仔细读一下带from从句的import语句如何执行的。

The from form uses a slightly more complex process:

  1. find the module specified in the from clause, loading and initializing it if necessary;
  2. for each of the identifiers specified in the import clauses:
    1. check if the imported module has an attribute by that name
    2. if not, attempt to import a submodule with that name and then check the imported module again for that attribute
    3. if the attribute is not found, ImportError is raised.
    4. otherwise, a reference to that value is stored in the local namespace, using the name in the as clause if it is present, otherwise using the attribute name

 

from...import...形式会使用一个较为复杂的过程。

1、找到from从句中指定的模块,加载并初始化它。

2、对于跟在import后的每个标识符(名称):

  • 验证from从句中加载的模块是否有这个名称的属性。(如果有,直接执行最后一步的名称绑定)
  • 如果没有,尝试导入一个以这个名称为名字的子模块,然后再验证一下from从句中加载的模块是否有这个名称的属性。
  • 如果from从句中加载的模块此时再没有这个名称的属性,则报 ImportError 错误。
  • 如果没有报错,本地命名空间会产生对标识符指向的对象的一个引用,如果有as从句,用as从句中的名称表示这个引用,如果没有as从句,用属性名即标识符表示这个引用。

这个过程就是先验证from module import的是不是module(包括代表包的__init__模块)中的一个属性(变量,函数,类),如果是,本地命名空间里绑定这个属性,就是我们前面说的,直接把模块中的函数对象导入。如果发现from的module中没有这个import的属性,就认为它可能是包中的一个子模块,尝试导入它,如果确实是子模块的话并且导入成功了,父包的名称空间中应该有一个属性指向这个子模块对象,所以验证一下,确保正确。如果验证失败,报错,如果验证成功,本地命名空间里绑定这个属性,这时候导入的是一个模块的对象。

也就是说,from ...import...有这几种形式:

from module import attribute(function、class)

from package import module

from package import attribute(function、class)

from package import subpackage

归根到底一句话,这些形式背后都是因为Python中包是模块的一种,可以在自己的__init__中定义属性。

其实就两种

from package import module

from module import attribute(function、class)

我们只要记住,在from...import...形式下,from 后面跟着的肯定是模块(包,子包,普通模块都是模块),而import后面可以是模块,可以是属性。

相比较,不带from的import语句,import的只能是模块(包,子包,普通模块都是模块)。

这些例子,请大家自行思索。

Examples:

import sound                        # sound imported and bound locally
import sound.effects.echo           # sound.effects.echo imported, sound bound locally
import sound.effects.echo as echo1  # sound.effects.echo imported and bound as echo1
from sound.effects import echo      # sound.effects.eccho imported and bound as echo
from sound import attr              # sound imported and sound.attr bound as attr

至此,我们算是搞清楚了import的两种形式,from ... import ...和import ...。也知道了as从句作用于名称绑定,理解成起别名即可。但提到过的包间相对引用还没说过呢。

相对导入

 When specifying what module to import you do not have to specify the absolute name of the module. When a module or package is contained within another package it is possible to make a relative import within the same top package without having to mention the package name. By using leading dots in the specified module or package after from you can specify how high to traverse up the current package hierarchy without specifying exact names. One leading dot means the current package where the module making the import exists. Two dots means up one package level. Three dots is up two levels, etc. So if you execute from import mod from a module in the pkg package then you will end up importing pkg.mod. If you execute from..subpkg2 import mod from within pkg.subpkg1 you will import pkg.subpkg2.mod. The specification for relative imports is contained within PEP 328.

 

当指定导入的模块时,不一定必须指出这个模块的绝对路径。当一个模块或者包包含在另一个包之内时,如果它们有相同的顶级包,我们有可能不提这个顶级包的名字而做一个相对导入。在from关键字之后,我们可以在指定模块或包里用领头小圆点来指出在这个包层次下,你要向上追溯的深度,而不需要使用明确的包名。一个领头小圆点意味着执行import语句的模块所在的当前包,两个领头小圆点代表上一级包,三个小圆点代表上两级包,依次类推。所以,如果在pkg包中的一个模块中执行from . import mod语句,实际上会导入pkg.mod。如果在pkg.subpkg1中的一个模块执行from ..subpkg2 import mod,实际是导入pkg.subpkg2.mod.。有关相对导入的规范说明包含在在PEP328中。

 

When packages are structured into subpackages (as with the sound package in the example), you can use absolute imports to refer to submodules of siblings packages. For example, if the module sound.filters.vocoder needs to use the echo module in the sound.effects package, it can use from sound.effects import echo.

You can also write relative imports, with the from module import name form of import statement. These imports use leading dots to indicate the current and parent packages involved in the relative import. From the surround module for example, you might use:

from . import echo
from .. import formats
from ..filters import equalizer

Note that relative imports are based on the name of the current module. Since the name of the main module is always "__main__", modules intended for use as the main module of a Python application must always use absolute imports.

 

当包被组织成子包(如示例中的sound包内的effects,formats),你可以使用绝对导入来引用兄弟包的子模块。例如,如果模块sound.filters.vocoder需要使用sound.effects中的echo模块,它可以使用from sound.effects import echo.。

你也可以利用from module import name 形式的import语句写一个相对导入。这些导入使用领头小圆点指示相对导入中涉及的当前包和父包。在effects下的surround模块里,我们可以用这些相对导入:

from . import echo   #导入的是sound.effects.echo
from .. import formats #导入的是sound.formats
from ..filters import equalizer #导入的是sound.filters.equalizer

记住,相对导入基于当前模块的名字。因为主模块的名字总是叫做 "__main__",因此,一个Python应用中作为主脚本使用的模块一定要使用绝对导入。

 
C:\WINDOWS\system32>python E:\sound\effects\echo.py
Traceback (most recent call last):
  File "E:\sound\effects\echo.py", line 5, in <module>
    from ..formats import wavwrite
ValueError: attempted relative import beyond top-level package

 

C:\WINDOWS\system32>python E:\sound\effects\echo.py
Traceback (most recent call last):
  File "E:\sound\effects\echo.py", line 5, in <module>
    from .surround import abcd
ModuleNotFoundError: No module named __main__.surround; __main__ is not a package

 

假如echo.py中有代码from ..formats import wavread

>>> from sound.effects import echo
>>> import sys
>>> sys.modules[sound]
<module sound from D:\\sound\\__init__.py>
>>> sys.modules[sound.effects]
<module sound.effects from D:\\sound\\effects\\__init__.py>
>>> sys.modules[sound.effects.echo]
<module sound.effects.echo from D:\\sound\\effects\\echo.py>
>>> sys.modules[sound.formats]
<module sound.formats from D:\\sound\\formats\\__init__.py>
>>> sys.modules[sound.formats.wavread]
<module sound.formats.wavread from D:\\sound\\formats\\wavread.py>
>>> echo.__name__
sound.effects.echo

可见相对导入from ..formats import wavread还是要变成from sound.formats import echo的形式。我们前面提到的from的规则,相对导入也是要遵守的。好在因为sys.modules的存在,任何已经导入过的模块(包括包),在一次Python历程中不会再导入,这也保证了相对引用能正确的找到引用的内容(相对导入基于当前模块的名字)。

以前了解相对路径,在命令行敲过cd .或cd ..的对这种相对引用应该感到很熟悉吧。

相对导入常被用在__init__.py文件中用来初始化包。

只有from形式的导入才能用相对导入,而且只有同在一个顶层包内才可用,相对导入也叫包内相对导入。

到此,这篇随笔算是接近完成了。还有一种from ... import * 的用法,以及__all__属性的意义,大家可以自己了解。

总结

请大家一定要明确以下几点:

Python中一切皆对象,变量名称只是对对象的引用;

命名空间的概念。属于某对象命名空间的变量,我们可以用dir()函数查看。如果顶层模块topmodule命名空间内的变量a指向一个模块对象submodule,submodule命名空间里有变量b,在顶层模块topmodule中直接使用b变量是不行的,要通过a.b的形式;

导入模块隐含执行模块内的代码,生成各种对象;

无论import形式的导入还是from...import形式的导入,其本质都是引入对象(模块对象,函数对象,类),然后设置你希望用来引用这些对象的方式(是a.b还是直接一个b);

同样的,无论import形式的导入还是from...import形式的导入,其操作都是上面提到的本质的实现,我们可以认为是‘两步走‘战略,一步是尝试找到模块,加载并初始化,生成各种对象;第二步,在导入语句执行的模块的命名空间里,按你希望的方式给这些对象命名,或者说是你想怎么引用这些对象,就怎么设置一些变量指向它们,是直达呢?还是中途换个乘遛一遛。

基本import形式和from ... import形式的区别在于,基本import导入的对象最低也应该是个模块对象,而from...import导入的对象可以是模块,也可以是模块中的函数,类或变量。如果找上面a、a.b和b的例子做比较,那就是基本的import导入只能做到变量a可见,a.b可用的程度,而from...import也能做到这一步,还能做到b可见,直接可用的程度(指的是用b直接代替a.b)。

在from...import...形式下,from 后面跟着的肯定是模块(包,子包,普通模块都是模块),而import后面可以是模块,可以是属性。

相比较,不带from的import语句,import的只能是模块(包,子包,普通模块都是模块)。

之所以区别基本import形式和from ... import形式,我们可以找一些表面理由说服自己(不是为了创造Python,这个问题不值得深究)。第一:from module import a,b,c,d,e,更优雅可读。第二:Python中包的实质为模块对象,包初始化时可以自定义属于自己的属性,从而可能会和目录下的同名模块发生冲突,因此,以模块为界,弄出两套方法。

包为Python提供了多层次的命名空间体系,包含__init__.py的目录被认为是包。包的实质是带有__path__属性的模块,因此,submodule或module可以指包,但package或subpackage并不适用于所有的模块。

导入模块的第一步,找到模块值得注意。每个Python解释器都带有一个叫做sys的内置模块,这个模块会在Python启动时自动加载,但仍然要执行import sys完成名称绑定。这个模块中sys.path变量是个列表,sys.modules是个字典,两者在这个过程中发挥很大作用。如果导入的模块,在sys.modules字典中有相应的键值,代表这个模块已经被导入过了,所以,接下来不会再寻找,加载,初始化它了。

寻找一个模块(当然,包也是模块)是按照一定顺序的,先查询内置模块,再根据sys.path列表中的一个个路径去查找,sys.path[0]比path[1]优先,path[1]比path[2]优先,依次类推。内置模块是编译到解释器的一部分标准模块,优先于sys.path。剩下的标准模块提供源码,位于sys.path列表中的某个路径下,所以有可能被屏蔽掉(比如这个路径不是path[0])。

有个系统变量PYTHONPATH,它可以包含一系列我们自己定义的路径,然后当sys.path列表生成时,这个PYTHONPATH变量会按照规则被拆分成一个个单独的路径,放到sys.path的开头,比上面说的那个标准模块源码路径要靠前。PYTHONPATH提供的路径仍然不是最靠前的,sys.path[0]根据我们调用Python解释器的方式的不同而按照不同机制被初始化。如果是启动交互模式,那么path[0]是一个空字符串(指向我们启动Python所在的文件目录);如果是执行python XXX\script.py(运行python脚本),那么path[0]是XXX代表的路径(包含script.py文件的目录)。

对搜索路径的要求适用于模块和包。搜索时如果在sys.path靠前位置找到了符合的条件的模块,后面目录中的同名模块会被屏蔽,因此,模块命名要慎重。涉及到包的搜索时,请注意,如果A包在sys.path靠后位置,python搜索A.B.C时,如果sys.path靠前位置有个A.py的模块,那么A包会被屏蔽,然后报错,因为A.py不可能有import A.B.C这种形式的导入。在涉及包的按层次导入和执行__init__.py的顺序上,Python也是步步推进的。先找到A,执行A包的__init__.py,实例化A包模块,给sys.modules[‘A‘]赋值,再去管A.B包,然后是A.B.C。

包机制还有一个好用的地方,那就是包内相对导入。相对导入只能用在包内的模块,只能用from...import的形式,from后一个小圆点代表包含这个模块的包,两个小圆点代表包含这个模块所在包的包,三个点,四个点。。。注意下面的形式。

from .b import attr

如果我们当前执行的模块文件为a.py,那么这个b模块和我们a.py在同一个目录下,b有个属性attr被a拿去用。b可能是b.py,b也可能是一个子包,attr是在它的__init__.py文件中定义的。虽然attr是在__init__.py中定义的,但这种定义背后可能是来自于一个导入操作。所以,掌握Python导入的本质尤能帮助理解。

 

以上是关于写给新手的Python导入机制详解的主要内容,如果未能解决你的问题,请参考以下文章

python 模块导入详解

收藏 | 写给新手炼丹师:2021版调参上分手册

Python中verbaim标签使用详解

Python模块导入详解

深入探讨 Python 的 import 机制:实现远程导入模块

有条件地导入 python 类的片段