SECTION 21 模块(二)


本章为"模块(一)"的扩展内容,有兴趣可以了解

21.1 扩展功能

21.1.1 关于 future

回首 Python 2.0 , 我们认识到了由于改进新特性, 以及当前特性增强, 某些变化会影响到当前功能。 所以为了让 Python 程序员为新事物做好准备, Python 实现了 __future__ 指令。
使用 from-import 语句"导入"新特性, 用户可以尝试一下新特性或特性变化, 以便在特性固定下来的时候修改程序。 它的语法是:
from __future__ import <featureName>

import __future__不会有任何变化,所以这是被禁止的。 (事实上这是允许的, 但它不会如你所想的那样启用所有特性。) 你必须显示地导入指定特性

>>> print 10/3
3
>>> from __future__ import division
>>> print 10/3
3.33333333333

上面division的新特性就是为整数相除得出更精确的值,在python2.7版本中,如果print 10/3. output为3。但是如果from __future__ import division之后再print 10/3,则会输出3.33333333333。
__future__工作原理

21.1.2 警告框架(简单介绍)

__future__ 指令类似, 有必要去警告用户不要使用一个即将改变或不支持的操作, 这样他们会在新功能正式发布前采取必要措施。 这个特性是很值得讨论的, 我们这里分步讲解一下。

首先是应用程序(员)接口(Application programmers’ interface , API)。 程序员应该有从Python 程序(通过调用 warnings 模块)或是 C 中(通过 PyErr_Warn() 调用)发布警告的能力。这个框架的另一个部分是一些警告异常类的集合。 Warning 直接从 Exception 继承, 作为所有警告的基类: UserWarning , DeprecationWarning , SyntaxWarning , 以及 RuntimeWarning 。

另一个组件是警告过滤器, 由于过滤有多种级别和严重性, 所以警告的数量和类型应该是可控制的。 警告过滤器不仅仅收集关于警告的信息(例如行号, 警告原因等等), 而且还控制是否忽略警告, 是否显示——自定义的格式——或者转换为错误(生成一个异常)。警告会有一个默认的输出显示到 sys.stderr , 不过有钩子可以改变这个行为, 例如,当运行会引发警告的 Python 脚本时,可以记录它的输出记录到日志文件中,而不是直接显示给终端用户。Python 还提供了一个可以操作警告过滤器的 API 。

最后, 命令行也可以控制警告过滤器。 你可以在启动 Python 解释器的时候使用 -W 选项。
详细讲解
使用示例

21.1.3 从ZIP文件中导入模块

在 2.3 版中, Python 加入了从 ZIP 归档文件导入模块的功能。 如果你的搜索路径中存在一个包含 Python 模块(.py, .pyc, or .pyo 文件)的 .zip 文件, 导入时会把 ZIP 文件当作目录处理, 在文件中搜索模块。
如果要导入的一个 ZIP 文件只包含 .py 文件, 那么 Python 不会为其添加对应的 .pyc(pyc是一种二进制文件,是由Python文件经过编译后所生成的文件,它是一种byte code,Python文件变成pyc文件后,加载的速度有所提高,而且pyc还是一种跨平台的字节码,由python的虚拟机来执行的。pyc的内容与python的版本是相关的,不同版本编译后的pyc文件是不同的)文件,这意味着如果一个 ZIP 归档没有匹配的 .pyc 文件时, 导入速度会相对慢一点。
同时你也可以为 .zip 文件加入特定的(子)目录, 例如 /tmp/yolk.zip/lib 只会从 yolk 归档的 lib/ 子目录下导入。

[root@k8s-node-02 yurq]# ls
echo1.py  myexc.py  zip1.py  zipimp.zip

[root@k8s-node-02 yurq]# cat echo1.py
#!/usr/bin/python2.7

def foo():
        print 'echo foo'

[root@k8s-node-02 yurq]# cat zip1.py
#!/usr/bin/python2.7
import sys, glob
import zipfile

files = glob.glob("*.py")
print (files)
zf = zipfile.PyZipFile('zipimp.zip', mode='w')
for file in files:
        zf.write(file)
zf.close()
>>> import zipimport
>>>importer = zipimport.zipimporter('zipimp.zip')
>>> ret=importer.find_module('echo1') #查找模块
>>> ret
<zipimporter object "zipimp.zip">
>>> mod=importer.load_module('echo1') #加载模块
>>> mod.foo()
echo foo

使用zip1.py打包当前目录的py文件,包括echo1.py,然后从zip中加载echo1,调用echo1中的foo()

21.2 模块内建函数

系统还为模块提供了一些功能上的支持. 现在我们将详细讨论他们.

21.2.1 __import__()

__import__()函数作为实际上导入模块的函数, 这意味着import语句调用__import__()函数完成它的工作。提供这个函数是为了让有特殊需要的用户覆盖它, 实现自定义的导入算法。
__import__() 的语法是:
__import__(module_name[, globals[, locals[, fromlist]]])

module_name 变量是要导入模块的名称,
globals 是包含当前全局符号表的名字的字典,
locals 是包含局部符号表的名字的字典
fromlist 是一个使用 from-import 语句所导入符号的列表。

globals , locals , 以及 fromlist 参数都是可选的, 默认分别为 globals() , locals() 和[] 。

调用 import sys 语句可以使用下边的语句完成:

sys = __import__('sys')
21.2.2 globals() 和 locals()

globals() 和 locals() 内建函数分别返回调用者全局和局部名称空间的字典。 在一个函数内部, 局部名称空间代表在函数执行时候定义的所有名字, locals() 函数返回的就是包含这些名字的字典。 globals() 会返回函数可访问的全局名字。在全局名称空间下, globals() 和 locals() 返回相同的字典, 因为这时的局部名称空间就是全局空间。

下边这段代码演示这两个函数的了使用:

def foo():
	print '\ncalling foo()...'
	aString = 'bar'
	anInt = 42
	print "foo()'s globals:", globals().keys()
	print "foo()'s locals:", locals().keys()
print "__main__'s globals:", globals().keys()
print "__main__'s locals:", locals().keys() 
foo()

我们只在这里访问了字典的键 , 因为它的值在这里没有影响(而且他们会让行变得更长更难懂)。
执行这个脚本, 我们得到如下的输出:

$ namespaces.py
__main__'s globals: ['__doc__', 'foo', '__name__', '__builtins__']
__main__'s locals: ['__doc__', 'foo', '__name__', '__builtins__']
calling foo()...
foo()'s globals: ['__doc__', 'foo', '__name__', '__builtins__']
foo()'s locals: ['anInt', 'aString']
21.2.3 reload()

reload() 内建函数可以重新导入一个已经导入的模块。 它的语法如下:
reload(module)
module 是你想要重新导入的模块自身,而非模块名的字符串

作用:重新加载(reload)包括最初导入模块时应用的分析过程和初始化过程。这样就允许在不退出解释器的情况下重新加载已更改的Python模块

>>>import sys
>>> sys.getdefaultencoding()            # 当前默认编码
'ascii'
>>> sys.setdefaultencoding('utf-8')
Traceback (most recent call last):		#早期版本可以直接sys.setdefaultencoding('utf-8'),新版本需要先reload一下
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'setdefaultencoding'
>>> reload(sys)                         # 使用 reload
<module 'sys' (built-in)>
>>> sys.setdefaultencoding('utf8')      # 设置编码
>>> sys.getdefaultencoding()
'utf8'
>>>

使用 reload() 的时候有一些标准:

  1. 模块必须是全部导入(不是使用 from-import), 而且它必须被成功导入。
  2. reload() 函数的参数必须是模块自身而不是包含模块名的字符串。 也就是说必须类似 reload(sys) 而不是 reload(‘sys’)。

21.3 包

包是一个有层次的文件目录结构, 它定义了一个由模块和子包组成的 Python 应用程序执行环境。Python 1.5 加入了包, 用来帮助解决如下问题:

为平坦的名称空间加入有层次的组织结构
允许程序员把有联系的模块组合到一起
允许分发者使用目录结构而不是一大堆混乱的文件
帮助解决有冲突的模块名称

与类和模块相同,包也使用句点属性标识来访问他们的元素。使用标准的import和from-import 语句导入包中的模块。

21.3.1 目录结构

假定我们的包的例子有如下的目录结构:

Phone/
	__init__.py
	common_util.py
	Voicedta/
		__init__.py
		Pots.py
		Isdn.py
	Fax/
		__init__.py
		G3.py
	Mobile/
		__init__.py
		Analog.py
		igital.py
	Pager/
		__init__.py
		Numeric.py

Phone 是最顶层的包, Voicedta 等是它的子包。 我们可以这样导入子包:

import Phone.Mobile.Analog
Phone.Mobile.Analog.dial()

你也可使用 from-import 实现不同需求的导入:
第一种方法是只导入顶层的子包, 然后使用属性/点操作符向下引用子包树:

from Phone import Mobile
Mobile.Analog.dial('555-1212')

此外, 我们可以还引用更多的子包:

from Phone.Mobile import Analog
Analog.dial('555-1212')

事实上, 你可以一直沿子包的树状结构导入:

from Phone.Mobile.Analog import dial
dial('555-1212')

在我们上边的目录结构中, 我们可以发现很多的 __init__.py 文件。 这些是初始化模块,from-import 语句导入子包时需要用到它。 如果没有用到, 他们可以是空文件。 程序员经常忘记为它们的包目录加入__init__.py文件,所以从Python2.5开始,这将会导致一个ImportWarning 信息。

不过, 除非给解释器传递了 -Wd 选项, 否则它会被简单地忽略。

21.3.2 使用 from-import 导入包

包同样支持 from-import all 语句:

from package.module import *

然而, 这样的语句会导入哪些文件取决于操作系统的文件系统. 所以我们在__init__.py 中加入 __all__ 变量. 该变量包含执行这样的语句时应该导入的模块的名字. 它由一个模块名字符串列表组成.。

21.3.3 绝对导入

包的使用越来越广泛, 很多情况下导入子包会导致和真正的标准库模块发生(事实上是它们的名字)冲突。 包模块会把名字相同的标准库模块隐藏掉, 因为它首先在包内执行相对导入, 隐藏掉标准库模块。

为此,所有的导入现在都被认为是绝对的,也就是说这些名字必须通过Python路径(sys.path 或是 PYTHONPATH )来访问。这个决定的基本原理是子包也可以通过 sys.path 访问, 例如 import Phone.Mobile.Analog 。在这个变化之前, 从 Mobile 子包内模块中导入 Analog 是合理的。作为一个折中方案, Python 允许通过在模块或包名称前置句点实现相对导入。

绝对导入通过使用被导入资源在项目根目录的完整路径进行导入

21.3.4 相对导入

如前所述, 绝对导入特性限制了模块作者的一些特权。失去了 import 语句的自由, 必须有新的特性来满足程序员的需求。这时候, 我们有了相对导入。 相对导入特性稍微地改变了 import 语法, 让程序员告诉导入者在子包的哪里查找某个模块。因为 import 语句总是绝对导入的, 所以相对导入只应用于 from-import 语句。

相对导入指定了被导入资源是相对于当前的位置 - 也就是,这个位置就是导入语句所在的地方

from Phone.Mobile.Analog import dial
from .Analog import dial
from ..common_util import setup
from ..Fax import G3.dial.

单个点表示模块或者包的引用是在同一个位置的同一个目录下。两个点表示它是在当前位置的父目录中 - 意思是指上一级目录。三个点表示它位于祖父母目录中,以此类推。如果你使用类Unix系统的话,一定有熟悉的感觉!

21.4 模块的其他特性

21.4.1 自动载入的模块

当 Python 解释器在标准模式下启动时, 一些模块会被解释器自动导入, 用于系统相关操作。唯一一个影响你的是 __builtin__ 模块, 它会正常地被载入, 这和 __builtins__ 模块相同。

sys.modules 变量包含一个由当前载入(完整且成功导入)到解释器的模块组成的字典, 模块名作为键, 它们的位置作为值。

例如在 Windows 下, sys.modules 变量包含大量载入的模块, 我们这里截短它, 只提供他们的模块名, 通过调用字典的 keys() 方法:

>>> import sys
>>> sys.modules.keys()
['os.path', 'os', 'exceptions', '__main__', 'ntpath','strop', 'nt', 'sys', '__builtin__', 'site',
'signal', 'UserDict', 'string', 'stat']

Unix 下载入的模块很类似:

>>> import sys
>>> sys.modules.keys()
['os.path', 'os', 'readline', 'exceptions','__main__', 'posix', 'sys', '__builtin__', 'site',
'signal', 'UserDict', 'posixpath', 'stat']
21.4.2 阻止属性导入

如果你不想让某个模块属性被 “from module import *” 导入 , 那么你可以给你不想导入的属性名称加上一个下划线( _ )。 不过如果你导入了整个模块或是你显式地导入某个属性(例如 import foo._bar ), 这个隐藏数据的方法就不起作用了。

21.4.3 不区分大小的导入

有一些操作系统的文件系统是不区分大小写的。 Python 2.1 前, Python 尝试在不同平台下导入模块时候"做正确的事情", 但随着 MacOS X 和 Cygwin 平台的流行, 这样的不足已经不能再被忽视, 而需要被清除。在 Unix(区分大小写)和 Win32(不区分大小写)下, 一切都很明了, 但那些新的不区分大小写的系统不会被加入区分大小写的特性。 PEP 235 指定了这个特性, 尝试解决这个问题, 并避免那些其他系统上"hack"式的解决方法。

底线就是为了让不区分大小写的导入正常工作, 必须指定一个叫做 PYTHONCASEOK 的环境变量。 Python 会导入第一个匹配模块名( 使用不区分大小写的习惯 )。否则 Python 会执行它的原生区分大小写的模块名称匹配, 导入第一个匹配的模块。

21.4.4 源代码编码

从 Python 2.3 开始, Python 的模块文件开始支持除 7 位 ASCII 之外的其他编码。 当然ASCII 是默认的, 你只要在你的 Python 模块头部加入一个额外的编码指示说明就可以让导入者使用指定的编码解析你的模块, 编码对应的 Unicode 字符串。 所以你使用纯 ASCII 文本编辑器的
时候不需要担心了(不需要把你的字符串放入 “Unicode 标签” 里) 。

一个 UTF-8 编码的文件可以这样指示:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

如果你执行或导入了包含非 ASCII 的 Unicode 字符串而没有在文件头部说明, 那么你会在Python 2.3 得到一个 DeprecationWarning , 而在 2.5 中这样做会导致语法错误。你可以在 PEP263 中得到更多关于源文件编码的相关内容。

21.4.5 导入循环

实际上,在使用 Python 时, 你会发现是能够导入循环的。 如果你开发了大型的 Python 工程,那么你很可能会陷入这样的境地。我们来看一个例子。 假定我们的产品有一个很复杂的命令行接口( command-line interface ,CLI)。 其中将会有超过一百万的命令, 结果你就有了一个“超冗余处理器”(overly massive handler,OMH)子集。 每加入一个新特性, 将有一到三条的新命令加入, 用于支持新的特性。

下边是我们的omh4cli.py 脚本:

from cli4vof import cli4vof
# command line interface utility function
def cli_util():
	pass
# overly massive handlers for the command line interface
def omh4cli():
:
	cli4vof()
:
omh4cli()

假定大多控制器都要用到这里的(其实是空的)工具函数。命令行接口的 OMH 都被封装在omh4cli() 函数里。 如果我们要添加一个新的命令, 那么它会被调用。现在这个模块不断地增长, 一些聪明的工程师会决定把新命令放入到隔离的模块里, 在原始模块中只提供访问新东西的钩子。 这样, 管理代码会变得更简单, 如果在新加入内容中发现了 bug ,那么你就不必在一个几兆的 Python 文件里搜索。

在我们的例子中, 有一个兴奋的经理要我们加入一个 “非常好的特性”。我们将创建一个新的cli4vof.py脚本 , 而不是把新内容集成到omh4cli.py 里:

import omh4cli
# command-line interface for a very outstanding feature
def cli4vof():
	omh4cli.cli_util()

前边已经提到, 工具函数是每个命令必须的, 而且由于不能把代码从主控制器复制出来, 所以我们导入了主模块, 在我们的控制器中添加对 omh , omh4cli() 的调用。问题在于主控制器 omh4cli 会导入我们的 cli4vof 模块(获得新命令的函数), 而 cli4vof也会导入 omh4cli (用于获得工具函数)。模块导入会失败, 这是因为 Python 尝试导入一个先前没有完全导入的模块:

$ python omh4cli.py
Traceback (most recent call last):
File "omh4cli.py", line 3, in ? from cli4vof import cli4vof
File "/usr/prod/cli4vof.py", line 3, in ?
import omh4cli
File "/usr/prod/omh4cli.py", line 3, in ?
from cli4vof import cli4vof

ImportError: cannot import name cli4vof

注意跟踪返回消息中显示的对 cli4vof 的循环导入。 问题在于要想调用工具函数, cli4vof 必须导入 omh4cli 。 如果它不需要这样做, 那么 omh4cli 将会成功导入 cli4vof , 程序正常执行。但在这里, omh4cli 尝试导入 cli4vof , 而 cli4vof 也试着导入 omh4cli 。 最后谁也不会完成
导入工作, 引发错误。 这只是一个导入循环的例子。 事实上实际应用中会出现更复杂的情况。

解决这个问题几乎总是移除其中一个导入语句。 你经常会在模块的最后看到 import 语句。作为一个初学者, 你只需要试着习惯它们, 如果你以前遇到在模块底部的 import 语句,现在你知道是为什么了。在我们的例子中, 我们不能把 import omh4cli 移到最后, 因为调用 cli4vof() 的时候 omh4cli() 名字还没有被载入。

$ python omh4cli.py
Traceback (most recent call last): File "omh4cli.py", line 3, in ? from cli4vof import
cli4vof
File "/usr/prod/cli4vof.py", line 7, in ?
import omh4cli
File "/usr/prod/omh4cli.py", line 13, in ?
omh4cli()
File "/usr/prod/omh4cli.py", line 11, in omh4cli cli4vof()
File "/usr/prod/cli4vof.py", line 5, in cli4vof omh4cli.cli_util()
NameError: global name 'omh4cli' is not defined

我们的解决方法只是把 import 语句移到 cli4vof() 函数内部:

def cli4vof():
	import omh4cli
	omh4cli.cli_util()

这样, 从 omh4cli() 导入 cli4vof() 模块会顺利完成, 在 omh4cli() 被调用之前它会被正确导入。 只有在执行到 cli4vof.cli4vof() 时候才会导入 omh4cli 模块。

21.5 相关模块

下边这些模块可能是你在处理Python模块导入时会用到的辅助模块。

模块描述
imp- 这个模块提供了一些底层的导入者功能。
modulefinder- 该模块允许你查找 Python 脚本所使用的所有模块。你可以使用其中的ModuleFinder 类或是把它作为一个脚本执行, 提供你要分析的(另个) Python 模块的文件名。
pkgutil- 该模块提供了多种把 Python 包打包为一个"包"文件分发的方法。 类似 site模块, 它使用 *.pkg 文件帮助定义包的路径, 类似 site 模块使用的 *.pth 文件。
site- 和 *.pth 文件配合使用, 指定包加入 Python 路径的顺序, 例如 sys.path ,PYTHONPATH 。你不需要显式地导入它, 因为 Python 导入时默认已经使用该模块。你可能需要使用 -S 开关在 Python 启动时关闭它。你也可以完成一些 site 相关的自定义操作,例如在路径导入完成后在另个地方尝试。
zipimport- 你可以使用该模块导入 ZIP 归档文件中的模块。 需要注意的是该功能已经"自动"开启, 所以你不需要在任何应用中使用它。在这里我们提出它只是作为参考。
distutils- 该模块提供了对建立、 安装、分发 Python 模块和包的支持。 它还可以帮助建立使用 C/C++ 完成的 Python 扩展。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值