在Python中,一个.py文件代表一个Module。在Module中可以是任何的符合Python文件格式的Python脚本。了解Module导入机制大有用处。
1 Module组成
一个.py文件就是一个module。Module中包括attribute, function等。 这里说的attribute其实是module的global variable。
在一个ModuleTests.py文件中:
#!python #-*- coding: utf-8 -*- """ 全局变量 """ # hello doc global moduleName moduleName = __name__ a = 1 def printModuleName(): print(a+1) print(__name__) print(moduleName) ''' if __name__ == '__main__' : print('current module name is "' + __name__+'"') ''' printModuleName() print(a) print(dir()) import __builtin__ print(__builtin__ == __builtins__) print(__doc__) print(__file__) print(__name__) print(__package__) __name__ = 'hello' print(__name__)
除了你自己定义的那些全局变量和函数外,每一个module还有一些内置的全局变量。在这个module就包括了三个attribute:a,moduleName,printModuleName。如果该模块被导入到另一个模块,在另个一模块中,就可以通过某种方式来访问这三个attribute。
1.1 Module 内置全局变量
每一个模块,都会有一些默认的attribute(全局变量)。dir()函数 是python中的一个顶级函数,勇于查看模块内容。例如上面的例子中,使用dir()查看结果是:
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a', 'moduleName', 'printModuleName']。其中a, moduleName, printModuleName 是由用户自定义的。其他的全是内置的。
1)__name__ :模块的名称。例如上面的ModuleTests.py,模块的名称默认就是ModuleTests。在运行时,如果一个module是程序入口,那么__name__就是”__main__”。它是最常用的。
2)__builtins__:在Python中有一个内置的module,叫做:__builtin__,它是一个Python的模块。而任何一个Python的模块都有一个__builtins__全局变量,它就是内置模块__builtin__的引用。可以通过如下代码测试:
import __builtin__ print(__builtin__ == __builtins__)
// 测试结果是True
在Python代码里,不需要我们导入就能直接使用的函数,类等,都是在这个内置模块里的。例如:range(),bytes(),dir()。
3)__doc__:module的文档说明。即便是Python的初学者都知道Python中的多行注释是用三对单引号或者双引号包含的。网上有人说__doc__其实就是注释,这句话呢说的太随意容易给人误解。经过测试,模块的__doc__应该是:文件头之后、代码(包含import)之前 的 第一个 多行注释。 方法的__doc__是方法前的那个注释。
在交换模式下,我们可以直接使用__doc__来查看方法的说明的。例如查看string.split方法的说明:str.split.__doc__就可以了。
4)__file__:当前module所在的文件的路径。
5)__package__:当前module所在的包名。如果没有,为None。
1.2 dir()的妙用
dir()是一个内置函数,用于查找指定的module中包括哪些attribute和method (或者function)。如果不指定参数,默认是当前module。上面说了range,dir,bytes等都是在内置模块里的,那么到底是不是呢?
dir(__builtins__)就可看到了:
2 Module导入
2.1 导入及其使用
一个Module可以导入(import)到其他的Python脚本中使用。导入方式有多种:
1)import module1
2)import module1 as m1
3)from module1 import xxx
4)from module1 import xxx as yyy
从包(package)导入,也分为类似的三种:
1)import p1.p2.p3.module1
2)import p1.p2.p3.module1 as m1
3)from p1.p2.p3.module1 import xxx
4)from p1.p2.p3.module1 import xxx as yyy
假设module1有两个attribue: a1,a2, 两个function: f1,f2下面来说明这几种导入方式的区别:
方式一是导入整个module1, 并将赋值给一个变量module1,来供使用。使用时,可以使用module1.a1, module1.a2, module1.f1(params), module.f2(params)
方式二是在方式一的基础上,重命名为m1,也就是说使用时得使用: m1.a1, m1.a2, m1.f1, m1.f2。
方式三是导入模块的部分内容(导入一个或者一些attribute或者function) 。例如 from module1 import a1,导入完成后,在当前的模块中创建了一个 a1的变量。调用是直接调用a1即可。
方式四对于导入一个attribute或者function时,可以重命名。例如 from module1 import a1 as msg,那么导入完毕,就是在当前的模块中创建了一个msg的变量,指向了module1.a1。 我们在调用时,只能通过msg来调用。
2.2 一次加载多次导入
对于上面的4种导入方式,不论哪一种,都有两个阶段:1)找到module对象,2)按需分配给变量。
模块本身就是为了复用的。在一个大的项目中,一些基础的、公共的模块通常会被大量使用,也就是说会被很多的module导入使用。我们也知道,module是放在py文件中的。如个一个module被大量导入时,难道要每一次导入,都去磁盘上找一py文件吗?
显然不能这样设计,如果真的这样设计,程序的性能将是极差的了。
对于同样的问题,Java中的做法是,使用ClassLoader加载类,并采用父加载器委托机制。尽可能的保证,同一个ClassLoader下,在多次引用一个类时,都是同一个。我们可以将该方式称为一次加载,多地使用。
Python的设计者,也考虑到这个问题。也采用了类似方案,被我称为一次加载,多次导入。我们假设它有一个Module Loader的存在,在首次加载(其实是首次import)时,执行流程如下:
1)由Module Loader从检索路径下找出相应的模块
2)编译或者找到合适的字节码文件(.pyc结尾)
3)解释执行要导入的Module,并放入缓存。
4)将导入的Module对象(或者其属性)分配给当前Module下的变量。
随后整个程序中再有执行import该moudle时,只需要从缓存中拿到该module,然后执行4)。
此外,对于过程2)有这样4种情况:
A: 若.py与.pyc都存在:会对.py文件的最后修改时间与.pyc文件的最后修改时间比较。执行时间靠后的那个。
B: 若.py与.pyc都不存在,继续找,如果最终都没有找到,出错。
C: 若.py存在,.pyc不存在:编译.py为.pyc。
D:若.py不存在,.pyc存在,直接执行.pyc。
再者还要说明2点:
1)一次加载,多次导入的机制,在Python程序包中提供的交互式命令行里使用import是不管用的。在交互式下,一次加载只能用于一次导入。
2)一般main py是不会被编译成pyc的,一个模块要想被编译成pyc,需要import到其他模块才行。
2.3 搜索路径
依据Java编程经验来看,通常程序会将文件放在不同的地方。Python必然也不例外。Python的搜索顺序为:
1) 已加载模块的缓存
2) 内置模块
3) sys.path
其中sys.path包含以下几部分:
1)入口程序的目录
2)系统环境变量PYTHONPATH代表的目录
3)标准Python库目录
4)任何.pth文件的内容(如果存在的话)
下面使用命令看一下sys.path的目录有哪些:
['', 'C:\windows\SYSTEM32\python27.zip', 'D:\Program Files\Python\Python27\DLLs', 'D:\Program Files\Python\Python27\lib', 'D:\Program Files\Python\Python27\lib\plat-win', 'D:\Program Files\Python\Python27\lib\lib-tk', 'D:\Program Files\Python\Python27', 'D:\Program Files\Python\Python27\lib\site-packages']
如果要加载的module不在上述目录下,可以通过3钟手段:
1) 配置环境变量PYTHONPATH,配置是与环境变量PATH的风格一样。
2) 程序动态修改sys.path
3) 放到site-packages目录下。
2.4 reload()
有些情况下,我们需要在程序运行是对程序代码做修改。例如我们需要监控某一方法执行快慢,是否存在性能问题时。我们需要在function的开始、结束部分记录一个startTime,endTime,依此来判定执行性能时。像这样的场景下,因为Module的加载一次,多长调用的机制,我们修改完代码,也不会生效。此时就需要一种机制来重新加载module,以达到期望效果。reload() 就可以解决这个问题。
reload(module) 是一个函数,参数是一个module对象。执行reload,就会等于再一次进行加载。
3 Package
3.1 __init__.py
每一个package下必须有一个__init__.py文件,该文件用于表明当前目录可以作为一个package。
__init__.py 也是一个python,当首次加载相应的package时,会执行__init__.py。
__init__.py文件可以什么也没有,也可以指定__all__或(和)__path。
3.2 __all__
__all__的值是一个列表,用于当程序中使用 from pkg1.pkg2.pkg3 import * 时。
就拿Python_HOME/Lib/下的json包来做实验,由于该文件比较大,我就写出主要部分:
__version__ = '2.0.9' __all__ = [ 'dump', 'dumps', 'load', 'loads', 'JSONDecoder', 'JSONEncoder', ] __author__ = 'Bob Ippolito <bob@redivi.com>' from .decoder import JSONDecoder from .encoder import JSONEncoder def dump(params): pass def dumps(params): pass def load(params): pass def loads(params): pass
目录结构如下:
当程序中使用 from json import * 引起json包首次加载时,执行过程如下:
1)找到json目录,执行__init__.py 执行完毕后:包下会暴漏出:load,loads,dump.dumps, JSONDecoder, JSONEncoder
2)查找* ,即从__all__找出要导出的变量。
当程序使用Import json.encoder引起json包首次加载时,执行过程如下:
1)找到json目录,执行__init__.py 执行完毕后:包下会暴漏出:load,loads,dump.dumps, JSONDecoder, JSONEncoder (以供from json import * 使用)
2)导入json包到一个变量里(此后程序可以直接使用json.encoder, json.decoder,json.scanner)
上述结论,来源于下面的测试用例:
#!python #-*- coding: utf-8 -*- """ Package Import Test """ #from json import * from json import encoder import json print(json.encoder == encoder) print(json.decoder is None) print(json.scanner is None) print(dir())
3.3 __path__
该变量用于配置包下的搜索位置。例如:
在Utils下增加2个目录Linux和Windows, 并各有一个echo.py文件, 目录如下
Sound/Utils/ |-- Linux 目录下没有__init__.py文件, 不是包, 只是一个普通目录 | `-- echo.py |-- Windows 目录下没有__init__.py文件, 不是包, 只是一个普通目录 | `-- echo.py |-- __init__.py |-- echo.py |-- reverse.py `-- surround.py
如果__init__.py是空的,当使用import Sound.Utils.echo导入echo时,会导入的是Sound/Utils/echo.py。
接下来我将__init__.py做如下修改:
import sys import os print "Sound.Utils.__init__.__path__ before change:", __path__ dirname = __path__[0] if sys.platform[0:5] == 'linux': __path__.insert( 0, os.path.join(dirname, 'Linux') ) else: __path__.insert( 0, os.path.join(dirname, 'Windows') ) print "Sound.Utils.__init__.__path__ AFTER change:", __path__
在Linux上执行import Sound.Utils.echo,那么搜索路径就会变成了: 'Sound/Utils/Linux', 'Sound/Utils'。以此来达到自动化的按需加载响应的module的功能。