4-4-高级模块话题
1. 模块设计理念
- 总是在 Python 的模块内编写代码。
- 最小化模块耦合性:全局变量。
- 最大化模块粘合性:统一目标。
- 模块尽量不改变其他模块的变量。
2. 在模块中隐藏数据
Python 模块会导出其文件顶层所赋值的所有变量名,没有对某一个变量名进行声明,使其在模块内可见或不可见这种概念。
2.1 最小化 from * 的破坏:_X
和 __all__
把下划线放在变量名前面(例如,_X),可以防止客户端使用 from * 语句导入模块名时,把其中的那些变量名复制出去。但还是可以使用其他导入形式看见并修改这些变量名,例如 import 语句。
# unders.py
a, _b, c, _d = 1, 2, 3, 4
from unders import * # 仅载入非 _X 变量名
a, c
(1, 3)
_b
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-2-8352e74fe22a> in <module>()
----> 1 _b
NameError: name '_b' is not defined
import unders
unders._b
2
此外,也可以在模块顶层把变量名的字符串列表赋值给变量 __all__
,以达到类似于 _X
命名惯例的隐藏效果。使用此功能时,from * 语句只会把列在 __all__
列表中的这些变量名复制出来。
# alls.py
__all__ = ['a', '_c']
a, b, _c, _d = 1, 2, 3, 4
from alls import * # 仅载入 __all__ 中的变量名
a, _c
(1, 3)
b
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-5-89e6c98d9288> in <module>()
----> 1 b
NameError: name 'b' is not defined
from alls import a, b, _c, _d # 其他导入可以获得所有变量名
a, b, _c, _d
(1, 2, 3, 4)
import alls
alls.a, alls.b, alls._c, alls._d
(1, 2, 3, 4)
3. 启用未来的语言特性:future
代码语言方面的变动可能破坏现有代码,使用以下形式的特定的 import 语句开启扩展功能:
from __future__ import featurename
这个语句一般应该出现在模块文件的顶端(也许在 docstring 或注释之后)。能够实验今后的语言变化。
4. 混合用法模式:__name__
和 __main__
这个技巧可以把文件作为模块导入,并以独立式程序的形式运行。每个模块都有个名为 __name__
的内置属性,Python 会自动设置该属性:
- 如果文件是以顶层程序文件执行,在启动时,
__name__
就会设置为字符串"__main__"
。 - 如果文件被导入,
__name__
就会改设成客户端所了解的模块名。
结果就是模块可以检测自己的 __name__
,来确定它是在执行还是在导入。
例如,建立名为 runme.py 的模块文件,它只导出了一个名为 tester 的函数:
def tester():
print("It's Christmas in Heaven...")
if __name__ == '__main__': # 执行时运行,导入时不运行
tester()
模块定义了一个函数,让用户可以正常地导入并使用:
import runme
runme.tester()
It's Christmas in Heaven...
模块也在末尾包含了当此文件以程序执行时,就会调用该函数的代码:
! python runme.py
It's Christmas in Heaven...
也许使用 __name__
测试最常见的就是自我测试代码。简而言之,可以在文件末尾加个 __name__
测试,把测试模块导出的程序代码放在模块中。这样可以继续导入,在客户端使用该文件,而且可以通过检测其逻辑在系统 shell 中运行它。
5. 示例:双模代码
如下的模块 formats.py,为导入者定了字符串格式化工具,还检查其名称看它是否作为一个顶层脚本在运行;如果是这样的话,它测试并使用系统命令行上列出的参数来运行一个定制的或传入的测试。在 Python 中,sys.argv 列表包含了命令行参数,它是反映在命令行上录入的单词的一个字符串列表,其中,第一项总是将要运行的脚本的名称:
"""
Various specialized string display formatting utilities.
Test me with canned self-test or command-line arguments.
"""
def commas(N):
"""
format positive integer-like N for display with
commas between digit groupings: xxx, yyy, zzz
"""
digits = str(N)
assert(digits.isdigit())
result = ''
while digits:
digits, last3 = digits[:-3], digits[-3:]
result = (last3 + ',' + result) if result else last3
return result
def money(N, numwidth=0, currency='$'):
"""
format number N for display with commas, 2 decimal digits,
leading $ and sign, and optional padding: '$ -xxx, yyy.zz'.
numwidth=0 for no space padding, currency='' to omit symbol,
and non-ASCII for others(e.g., pound=u'\xA3' or u'\u00A3').
"""
sign = '-' if N < 0 else ''
N = abs(N)
whole = commas(int(N))
fract = ('%.2f' % N)[-2:]
number = '%s%s.%s' % (sign, whole, fract)
return '%s%*s' % (currency, numwidth, number)
if __name__ == '__main__':
def selftest():
tests = 0, 1
tests += 12, 123, 1234, 12345, 123456, 1234567
tests += 2 ** 32, 2 ** 100
for test in tests:
print(commas(test))
print('')
tests = 0, 1, -1, 1.23, 1., 1.2, 3.14159
tests += 12.34, 12.344, 12.345, 12.346
tests += 2 ** 32, (2 ** 32 + .2345)
tests += 1.2345, 1.2, 0.2345
tests += -1.2345, -1.2, -0.2345
tests += -(2 ** 32), -(2 ** 32 + .2345)
tests += (2 ** 100), -(2 ** 100)
for test in tests:
print('%s [%s]' % (money(test, 17), test))
import sys
if len(sys.argv) == 1:
selftest()
else:
print(money(float(sys.argv[1]), int(sys.argv[2])))
! python formats.py 99999 0
$99,999.00
! python formats.py -12345 25
$ -12,345.00
! python formats.py
0
1
12
123
1,234
12,345
123,456
1,234,567
4,294,967,296
1,267,650,600,228,229,401,496,703,205,376
$ 0.00 [0]
$ 1.00 [1]
$ -1.00 [-1]
$ 1.23 [1.23]
$ 1.00 [1.0]
$ 1.20 [1.2]
$ 3.14 [3.14159]
$ 12.34 [12.34]
$ 12.34 [12.344]
$ 12.35 [12.345]
$ 12.35 [12.346]
$ 4,294,967,296.00 [4294967296]
$ 4,294,967,296.23 [4294967296.2345]
$ 1.23 [1.2345]
$ 1.20 [1.2]
$ 0.23 [0.2345]
$ -1.23 [-1.2345]
$ -1.20 [-1.2]
$ -0.23 [-0.2345]
$-4,294,967,296.00 [-4294967296]
$-4,294,967,296.23 [-4294967296.2345]
$1,267,650,600,228,229,401,496,703,205,376.00 [1267650600228229401496703205376]
$-1,267,650,600,228,229,401,496,703,205,376.00 [-1267650600228229401496703205376]
由于这段代码针对双模式用法编写,我们一般也可以把这些工具作为库的部分导入到其他的环境中:
from formats import money, commas
money(123.456)
'$123.46'
6. 修改模块搜索路径
模块搜索路径是一个目录列表,可以通过环境变量 PYTHONPATH 以及可能的 .pth 路径文件进行定制。Python 程序本身通过修改名为 sys.path 的内置列表来修改搜索路径。sys.path 在程序启动时就会进行初始化,在那之后,可以随意对其元素进行删除、附加和重设:
import sys
sys.path
['',
'E:\\Anaconda\\python36.zip',
'E:\\Anaconda\\DLLs',
'E:\\Anaconda\\lib',
'E:\\Anaconda',
'E:\\Anaconda\\lib\\site-packages',
'E:\\Anaconda\\lib\\site-packages\\win32',
'E:\\Anaconda\\lib\\site-packages\\win32\\lib',
'E:\\Anaconda\\lib\\site-packages\\Pythonwin',
'E:\\Anaconda\\lib\\site-packages\\IPython\\extensions',
'C:\\Users\\lan\\.ipython']
sys.path.append('C:\\sourcedir')
sys.path
['',
'E:\\Anaconda\\python36.zip',
'E:\\Anaconda\\DLLs',
'E:\\Anaconda\\lib',
'E:\\Anaconda',
'E:\\Anaconda\\lib\\site-packages',
'E:\\Anaconda\\lib\\site-packages\\win32',
'E:\\Anaconda\\lib\\site-packages\\win32\\lib',
'E:\\Anaconda\\lib\\site-packages\\Pythonwin',
'E:\\Anaconda\\lib\\site-packages\\IPython\\extensions',
'C:\\Users\\lan\\.ipython',
'C:\\sourcedir']
sys.path = [r'd:\temp']
sys.path.append('c:\\lp4d\\examples')
sys.path.insert(0, '..')
sys.path
['..', 'd:\\temp', 'c:\\lp4d\\examples']
可以使用这个技巧,在 Python 程序中动态配置搜索路径。sys.path 的设置方法只在修改的 Python 会话或程序中才会存续。
7. Import 语句和 from 语句的 as 扩展
import 和 from 语句都可以扩展,让模块可以在脚本中给予不同的变量名:
import modulename as name
相当于:
import modulename
name = modulename
del modulename
from 语句也可以这么用:
from modulename import attrname as name
包导入功能也可为整个目录路径提供简短的名称:
import dir1.dir2.mod as mod
8. 用名称字符串导入模块
有时候,我们的程序可以在运行时以一个字符串的形式获取要导入的模块的名称。使用特殊的工具,从运行时生成的一个字符串来动态地载入一个模块。最通用的方法是,把一条导入语句构建为 Python 代码的一个字符串,并且将其传递给 exec 内置函数以运行:
exec('import string')
string
<module 'string' from 'E:\\Anaconda\\lib\\string.py'>
使用内置的 __import__
函数来从一个名称字符串载入的话,代码可能会运行得更快:
string = __import__('string')
string
<module 'string' from 'E:\\Anaconda\\lib\\string.py'>
新的导入调用普遍更受喜爱:
import importlib
string = importlib.import_module('string')
string
<module 'string' from 'E:\\Anaconda\\lib\\string.py'>
9. 示例:过渡性模块重载
当重载一个模块时,Python 只重载特定的模块文件,它不会自动重载那些导入的模块。
例如,要重载某个模块 A,并且 A 导入模块 B 和 C,重载只适用于 A,而不适用于 B 和 C。
9.1 递归重载
通过扫描模块的 __dict__
属性并检查每一项的 type 以找到要重新载入的嵌套模块:
#!python
"""
reloadall.py: transitively reload nested modules (2.X + 3.X).
Call reload_all with one or more imported module module objects.
"""
import types
from imp import reload
def status(module):
print('reloading ' + module.__name__)
def tryreload(module):
try:
reload(module)
except:
print('FAILED: %s' % module)
def transitive_reload(module, visited):
if not module in visited:
status(module)
tryreload(module)
visited[module] = True
for attrobj in module.__dict__.values():
if type(attrobj) == types.ModuleType: # 如果是模块就递归
transitive_reload(attrobj, visited)
def reload_all(*args):
visited = {}
for arg in args:
if type(arg) == types.ModuleType:
transitive_reload(arg, visited)
def tester(reloader, modname): # 自测试代码
import importlib, sys # 仅在测试时导入
if len(sys.argv) > 1: modname = sys.argv[1]
module = importlib.import_module(modname) # 通过名称字符串导入
reloader(module)
if __name__ == '__main__':
tester(reload_all, 'reloadall')
! python reloadall.py
reloading reloadall
reloading types
reloading functools
reloading collections.abc
! python reloadall.py string
reloading string
reloading _string
reloading re
reloading enum
reloading sys
reloading sre_compile
reloading _sre
reloading sre_parse
reloading functools
reloading _locale
reloading copyreg
10. 模块陷阱
- 顶层代码的语句次序的重要性。
- from 复制变量名,而不是连接。
- from * 会让变量语义模糊。
- reload 不会影响 from 导入。
- reload、from 以及交互式模式测试。
- 递归形式的 from 导入无法工作。