离线下载
PDF版 ePub版

qyuhen · 更新于 2018-11-28 11:00:43

模块

不同于 C++、Java、C# namespace 仅作为符号隔离前缀,Python 模块是运行期对象。模块对应同名源码文件,为成员提供全局名字空间。

模块对象

模块对象有几个重要属性:

  • name: 模块名 .,在 sys.modules 中以此为主键。
  • file: 模块完整文件名。
  • dict: 模块 globals 名字空间。

除使用 py 文件外,还可动态创建模块对象。

>>> import sys, types

>>> m = types.ModuleType("sample", "sample module.") # 用 type 创建对象。
>>> m
<module 'sample' (built-in)>

>>> m.__dict__
{'__name__': 'sample', '__doc__': 'sample module.'}

>>> "sample" in sys.modules     # 并没有添加到 sys.modules。
False

>>> def test(): print "test..."
>>> m.test = test       # 动态添加模块成员。
>>> m.test()
test...

为模块动态添加函数成员时,须注意函数所引用的是其定义模块的名字空间。

>>> def test(): print "test:", __name__
>>> test()
test: __main__

>>> m.test = test
>>> m.test()
test: __main__

imp.new_module() 也可用来动态创建模块对象,同样不会添加到 sys.modules。

>>> import imp
>>> m = imp.new_module("test")
>>> m
<module 'test' (built-in)>

>>> m.__dict__
{'__name__': 'test', '__doc__': None, '__package__': None}

reload

当模块源文件发生变更时,可使用内置函数 reload() 重新导入模块。新建模块对象依旧使用原内存地址,只是原先被引用的内部成员对象不会被同步刷新。

测试一下,为避免本地名字引用造成干扰,我们直接从 sys.modules 获取模块。

>>> import sys

>>> hex(id(sys.modules["string"]))
'0x10b4fc6e0'

>>> reload(sys.modules["string"])
<module 'string'>

>>> hex(id(sys.modules["string"]))  # reload 后的模块地址未曾改变,所以其他地方对
'0x10b4fc6e0'     # 该模块的引用就不会失效,且被 "刷新"。

如果改用手动方法重新载入,那么就会出现两个不同的模块对象了。

>>> del sys.modules["string"]
>>> sys.modules["string"] = __import__("string")

>>> hex(id(sys.modules["string"]))  # 地址变了。
'0x10bc17a98'

搜索路径

虚拟机按以下顺序搜索模块 (包):

  • 当前进程根目录。
  • PYTHONPATH 环境变量指定的路径列表。
  • Python 标准库目录列表。 -路径文件 (.pth) 保存的目录 (通常放在 site-packages 目录下)。

进程启动后,所有这些路径都被组织到 sys.path 列表中 (顺序可能会被修改)。任何 import 操作都按照 sys.path 列表查找目标模块。当然,可以用代码往 sys.path 添加自定义路径。

虚拟机按以下顺序匹配目标模块:

  • py 源码文件。
  • pyc 字节码文件。
  • egg 包文件或目录。
  • so、dll、pyd 等扩展文件。
  • 内置模块。
  • 其他。

要执行程序,源文件不是必须的。实际上,很多软件发布时都会删掉 py 文件,仅保留二进制 pyc 字节码文件。但要注意,字节码很容易被反编译,不能奢求它能带来安全。

find_module

可用 imp.find_module() 获取模块的具体文件信息。

>>> import imp

>>> imp.find_module("os")
(
    <open file '/System/.../2.7/lib/python2.7/os.py', mode 'U' at 0x1013aa420>,
    '/System/.../2.7/lib/python2.7/os.py',
    ('.py', 'U', 1)
)

导入模块

进程中的模块对象通常是唯一的。在首次成功导入后,模块对象被添加到 sys.modules,以后导入操作总是先检查模块对象是否已经存在。可用 sys.modules[name] 获取当前模块对象。

关键字 import 将包、模块或成员对象导入到当前名字空间中,可以是 globals,也可以是函数内部的 locals 名字空间。

>>> import pymongo, redis
>>> import pymongo.connection, pymongo.database
>>> import pymongo.connection as mgoconn, pymongo.database as mgodb

>>> from pymongo import connection
>>> from pymongo import connection, database
>>> from pymongo import connection as mgoconn, database as mgodb
>>> from pymongo import *
>>> from pymongo.connection import *

如果待导入对象和当前名字空间中已有名字冲突,可用 as 更换别名。需要注意,"import *" 不会导入模块私有成员 (以下划线开头的名字) 和 all 列表中未指定的对象。

在函数中使用 "import *" 会引发警告,虽然不影响使用,但应该避免引入用不到的名字。(Python 3 已经禁止该用法了)

def main():
    import test
    from test import add, _x
    from sys import *   # SyntaxWarning: import * only allowed at module level

all

因为 import 实际导入的是目标模块 globals 名字空间中的成员,那么就有一个问题:目标模块也会导入其他模块,这些模块同样在目标模块的名字空间中。"import *" 操作时,所有这些一并被带入到当前模块中,造成一定程度的污染。建议在模块中用 all 指定可被批量导出的成员名单。

__all__ = ["add", "x"]

私有成员和 all 都不会影响显式导出目标模块成员。Python 并没有严格的私有权限控制,仅以特定的命名规则来提醒调用人员。

__import__

和 import 关键字不同,内置函数 import() 以字符串为参数导入模块。导入的模块会被添加到 sys.modules,但不会在当前名字空间中创建引用。

>>> import sys

>>> sys.modules.get("zlib") # 没有 zlib。

>>> __import__("zlib")  # 导入 zlib,返回模块对象。
<module 'zlib'>

>>> sys.modules.get("zlib") # zlib 添加到 sys.modules。
<module 'zlib'>

>>> "zlib" in globals()  # 名字空间中没有 zlib,除非将 __import__ 结果关联到某个名字。
False

import 导入 package.module 时,返回的是 package 而非 module。看下面的例子:

test <dir>
    |_ __init__.py
    |_ add.py
>>> m = __import__("test.add")

>>> m       # 返回的并不是 test.add 模块。
<module 'test' from 'test/__init__.pyc'>

>>> m.__dict__.keys()    # 还好 add 在 test 的名字空间中。
['__file__', ..., '__path__', 'add']

>>> m.add      # 得这样才能访问 add 模块。
<module 'test.add' from 'test/add.pyc'>

只有 fromlist 参数不为空时,才会返回目标模块。

>>> m = __import__("test.add", fromlist = ["*"])

>>> m
<module 'test.add' from 'test/add.pyc'>

>>> m.__dict__.keys()
['__builtins__', '__file__', '__package__', 'hi', 'x', '__name__', '__doc__']

import 太麻烦,建议用 importlib.import_module() 代替。

>>> import sys, importlib

>>> m = importlib.import_module("test.add")

>>> m        # 返回的是目标模块,而非 package。
<module 'test.add' from 'test/add.pyc'>

>>> sys.modules.get("test.add")    # 模块自然要添加到 sys.modules。
<module 'test.add' from 'test/add.pyc'>

>>> "test.add" in globals()    # 没有添加到当前名字空间中。
False

>>> importlib.import_module(".add", "test")  # 使用 "." 或 ".." 指定模块在多层
<module 'test.add' from 'test/add.pyc'>  # package 中位置。(必须)

注意:关键字 import 总是优先查找当前模块所在目录,而 import、import_module 则是优先查找进程根目录。所以用 import、import_module 导入包模块时,必须带上包前缀。

load_source

imp 另提供了 load_source()、load_compiled() 等几个函数,可用来载入不在 sys.path 搜索路径列表中的模块文件。优先使用已编译的字节码文件,模块对象会被添加到 sys.modules。

需要小心,这些函数类似 reload(),每次都会新建模块对象。

>>> imp.load_source("add", "./test/add.py")
<module 'add' from './test/add.pyc'>

构建包

将多个模块文件放到独立目录,并提供初始化文件 init.py,就形成了包 (package)。

无论是导入包,还是导入包中任何模块或成员,都会执行初始化文件,且仅执行一次。可用来初始化包环境,存储帮助、版本等信息。

all

"from import *" 仅导入 init.py 的名字空间,而该文件通常又只是个空文件,这意味着没有任何模块被导入。此时就需要用 all 指定可以被导入的模块名字列表,该定义无需将模块显式引入到 init.py 名字空间。

$ cat test/__init__.py
__all__ = ["add"]

有太多理由不建议使用 "import *",比如引入不需要的模块,意外 "覆盖" 当前空间同名对象等等。

换种做法,将要公开的模块和模块成员显式导入到 init.py 名字空间中,调用者只需 "import ",然后用 "." 就可访问所需的目标对象。如此可规避上述问题,还有助于隐藏包的实现细节,减少外部对包文件组织结构的依赖。

path

某些时候,包内的文件太多,需要分类存放到多个目录中,但又不想拆分成新的包或子包。这么做是允许的,只要在 init.py 中用 path 指定所有子目录的全路径即可 (子目录可放在包外)。

test <dir>
    |_ __init__.py
    |
    |_ a <dir>
    . |_ add.py
    |
    |_ b <dir>
        |_ sub.py
$ cat test/__init__.py
__path__ = ["/home/yuhen/py/test/a", "/home/yuhen/py/test/b"]

稍微改进一下。还可以用 os.listdir() 扫描全部子目录,自动形成路径列表。

from os.path import abspath, join
subdirs = lambda *dirs: [abspath(join(__path__[0], sub)) for sub in dirs]

__path__ = subdirs("a", "b")

pkgutil

如果要获取包里面的所有模块列表,不应该用 os.listdir(),而是 pkgutil 模块。

test <dir>
    |_ __init__.py
    |_ add.py
    |_ user.py
    |
    |_ a <dir>
    . |_ __init__.py
    . |_ sub.py
    |
    |_ b <dir>
        |_ __init__.py
        |_ sub.py
>>> import pkgutil, test

>>> for _, name, ispkg in pkgutil.iter_modules(test.__path__, test.__name__ + "."):
...     print "name: {0:12}, is_sub_package: {1}".format(name, ispkg)
...
name: test.a , is_sub_package: True
name: test.add , is_sub_package: False
name: test.b , is_sub_package: True
name: test.user , is_sub_package: False

>>> for _, name, ispkg in pkgutil.walk_packages(test.__path__, test.__name__ + "."):
    print "name: {0:12}, is_sub_package: {1}".format(name, ispkg)
...
name: test.a , is_sub_package: True
name: test.a.sub , is_sub_package: False
name: test.add , is_sub_package: False
name: test.b , is_sub_package: True
name: test.b.sub , is_sub_package: False
name: test.user , is_sub_package: False

函数 iter_modules() 和 walk_packages() 的区别在于:后者会迭代所有深度的子包。

pkgutil.get_data() 可读取包内任何文件内容。

>>> pkgutil.get_data("test", "add.py")
'#coding=utf-8\n\nx = 1\n\ndef hi():\n pass\n\n\nprint "add init"\n'

egg

将包压缩成单个文件,以便于分发和安装。类似 Java JAR 那样。

  1. 安装 setuptools。
$ sudo easy_install setuptools
  1. 创建空目录,将包目录完整拷贝到该目录下。

  2. 创建 setup.py 文件。(http://docs.python.org/2/distutils/setupscript.html)
from setuptools import setup, find_packages

setup (
    name = "test",
    version = "0.0.9",
    keywords = ("test", ),
    description = "test package",

    url = "http://github.com/qyuhen",
    author = 'Q.yuhen',
    author_email = "qyuhen@hotmail.com",

    packages = find_packages(),
)
  1. 创建 egg 压缩文件。
$ python setup.py bdist_egg

running bdist_egg
running egg_info
creating test.egg-info
... ...
zip_safe flag not set; analyzing archive contents...
creating dist
creating 'dist/test-0.0.9-py2.7.egg' and adding 'build/.../egg' to it
removing 'build/bdist.macosx-10.8-intel/egg' (and everything under it)

生成的 egg 文件存放在 dist 目录。

$ tar tvf dist/test-0.0.9-py2.7.egg
-rwxrwxrwx 0 0 0 1 12 30 00:40 EGG-INFO/dependency_links.txt
-rwxrwxrwx 0 0 0 226 12 30 00:40 EGG-INFO/PKG-INFO
-rwxrwxrwx 0 0 0 228 12 30 00:40 EGG-INFO/SOURCES.txt
-rwxrwxrwx 0 0 0 5 12 30 00:40 EGG-INFO/top_level.txt
-rwxrwxrwx 0 0 0 1 12 30 00:40 EGG-INFO/zip-safe
-rwxrwxrwx 0 0 0 21 12 30 00:15 test/__init__.py
-rwxrwxrwx 0 0 0 137 12 30 00:40 test/__init__.pyc
-rwxrwxrwx 0 0 0 60 12 30 00:15 test/add.py
-rwxrwxrwx 0 0 0 305 12 30 00:40 test/add.pyc
-rwxrwxrwx 0 0 0 0 12 30 00:15 test/user.py
-rwxrwxrwx 0 0 0 133 12 30 00:40 test/user.pyc
-rwxrwxrwx 0 0 0 0 12 30 00:15 test/a/__init__.py
-rwxrwxrwx 0 0 0 139 12 30 00:40 test/a/__init__.pyc
-rwxrwxrwx 0 0 0 8 12 30 00:15 test/a/sub.py
-rwxrwxrwx 0 0 0 151 12 30 00:40 test/a/sub.pyc
-rwxrwxrwx 0 0 0 0 12 30 00:15 test/b/__init__.py
-rwxrwxrwx 0 0 0 139 12 30 00:40 test/b/__init__.pyc
-rwxrwxrwx 0 0 0 8 12 30 00:15 test/b/sub.py
-rwxrwxrwx 0 0 0 151 12 30 00:40 test/b/sub.pyc

将 test-0.0.9-py2.7.egg 全路径添加到路径文件 (.pth) 或 PYTHONPATH 环境变量就可使用。更最常见的做法是将其安装到 site_packages 目录。

$ sudo easy_install dist/test-0.0.9-py2.7.egg

Processing test-0.0.9-py2.7.egg
Copying test-0.0.9-py2.7.egg to /Library/Python/2.7/site-packages
Adding test 0.0.9 to easy-install.pth file

Installed /Library/Python/2.7/site-packages/test-0.0.9-py2.7.egg
Processing dependencies for test==0.0.9
Finished processing dependencies for test==0.0.9

安装后的搜索路径被自动添加到 site-packages/easy-install.pth 文件。

上一篇: 迭代器 下一篇: