鍍金池/ 教程/ Python/ 模塊
附錄
進程通信
操作系統(tǒng)
迭代器
模塊
描述符
裝飾器
第三部分 擴展庫
內置類型
數(shù)據(jù)存儲
數(shù)據(jù)類型
基本環(huán)境
文件與目錄
異常
程序框架
數(shù)學運算
函數(shù)
元類
字符串
表達式

模塊

不同于 C++、Java、C# namespace 僅作為符號隔離前綴,Python 模塊是運行期對象。模塊對應同名源碼文件,為成員提供全局名字空間。

模塊對象

模塊對象有幾個重要屬性:

  • name: 模塊名 .,在 sys.modules 中以此為主鍵。
  • file: 模塊完整文件名。
  • dict: 模塊 globals 名字空間。

除使用 py 文件外,還可動態(tài)創(chuàng)建模塊對象。

>>> import sys, types

>>> m = types.ModuleType("sample", "sample module.") # 用 type 創(chuàng)建對象。
>>> 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       # 動態(tài)添加模塊成員。
>>> m.test()
test...

為模塊動態(tài)添加函數(shù)成員時,須注意函數(shù)所引用的是其定義模塊的名字空間。

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

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

imp.new_module() 也可用來動態(tài)創(chuàng)建模塊對象,同樣不會添加到 sys.modules。

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

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

reload

當模塊源文件發(fā)生變更時,可使用內置函數(shù) reload() 重新導入模塊。新建模塊對象依舊使用原內存地址,只是原先被引用的內部成員對象不會被同步刷新。

測試一下,為避免本地名字引用造成干擾,我們直接從 sys.modules 獲取模塊。

>>> import sys

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

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

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

如果改用手動方法重新載入,那么就會出現(xiàn)兩個不同的模塊對象了。

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

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

搜索路徑

虛擬機按以下順序搜索模塊 (包):

  • 當前進程根目錄。
  • PYTHONPATH 環(huán)境變量指定的路徑列表。
  • Python 標準庫目錄列表。 -路徑文件 (.pth) 保存的目錄 (通常放在 site-packages 目錄下)。

進程啟動后,所有這些路徑都被組織到 sys.path 列表中 (順序可能會被修改)。任何 import 操作都按照 sys.path 列表查找目標模塊。當然,可以用代碼往 sys.path 添加自定義路徑。

虛擬機按以下順序匹配目標模塊:

  • py 源碼文件。
  • pyc 字節(jié)碼文件。
  • egg 包文件或目錄。
  • so、dll、pyd 等擴展文件。
  • 內置模塊。
  • 其他。

要執(zhí)行程序,源文件不是必須的。實際上,很多軟件發(fā)布時都會刪掉 py 文件,僅保留二進制 pyc 字節(jié)碼文件。但要注意,字節(jié)碼很容易被反編譯,不能奢求它能帶來安全。

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,以后導入操作總是先檢查模塊對象是否已經(jīng)存在??捎?sys.modules[name] 獲取當前模塊對象。

關鍵字 import 將包、模塊或成員對象導入到當前名字空間中,可以是 globals,也可以是函數(shù)內部的 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 列表中未指定的對象。

在函數(shù)中使用 "import *" 會引發(fā)警告,雖然不影響使用,但應該避免引入用不到的名字。(Python 3 已經(jīng)禁止該用法了)

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 并沒有嚴格的私有權限控制,僅以特定的命名規(guī)則來提醒調用人員。

__import__

和 import 關鍵字不同,內置函數(shù) import() 以字符串為參數(shù)導入模塊。導入的模塊會被添加到 sys.modules,但不會在當前名字空間中創(chuàng)建引用。

>>> 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__ 結果關聯(lián)到某個名字。
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 參數(shù)不為空時,才會返回目標模塊。

>>> 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 總是優(yōu)先查找當前模塊所在目錄,而 import、import_module 則是優(yōu)先查找進程根目錄。所以用 import、import_module 導入包模塊時,必須帶上包前綴。

load_source

imp 另提供了 load_source()、load_compiled() 等幾個函數(shù),可用來載入不在 sys.path 搜索路徑列表中的模塊文件。優(yōu)先使用已編譯的字節(jié)碼文件,模塊對象會被添加到 sys.modules。

需要小心,這些函數(shù)類似 reload(),每次都會新建模塊對象。

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

構建包

將多個模塊文件放到獨立目錄,并提供初始化文件 init.py,就形成了包 (package)。

無論是導入包,還是導入包中任何模塊或成員,都會執(zhí)行初始化文件,且僅執(zhí)行一次。可用來初始化包環(huán)境,存儲幫助、版本等信息。

all

"from import *" 僅導入 init.py 的名字空間,而該文件通常又只是個空文件,這意味著沒有任何模塊被導入。此時就需要用 all 指定可以被導入的模塊名字列表,該定義無需將模塊顯式引入到 init.py 名字空間。

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

有太多理由不建議使用 "import *",比如引入不需要的模塊,意外 "覆蓋" 當前空間同名對象等等。

換種做法,將要公開的模塊和模塊成員顯式導入到 init.py 名字空間中,調用者只需 "import ",然后用 "." 就可訪問所需的目標對象。如此可規(guī)避上述問題,還有助于隱藏包的實現(xiàn)細節(jié),減少外部對包文件組織結構的依賴。

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

函數(shù) iter_modules() 和 walk_packages() 的區(qū)別在于:后者會迭代所有深度的子包。

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

將包壓縮成單個文件,以便于分發(fā)和安裝。類似 Java JAR 那樣。

  1. 安裝 setuptools。
$ sudo easy_install setuptools
  1. 創(chuàng)建空目錄,將包目錄完整拷貝到該目錄下。

  2. 創(chuàng)建 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. 創(chuàng)建 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 環(huán)境變量就可使用。更最常見的做法是將其安裝到 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 文件。

上一篇:迭代器下一篇:函數(shù)