Python基础:模块

一、概述

模块(module)和 包(package)是Python用于组织大型程序的利器。

模块 是一个由 变量函数 等基本元素组成的功能单元,设计良好的模块通常是高内聚、低耦合、可复用、易维护的。 是管理模块的容器,它具有 可嵌套性:一个包可以包含模块和其他包。从文件系统的视角来看,包就是目录,模块就是文件。

从本质上讲,一个模块就是一个独立的名字空间(namespace),单纯的多个模块只能构成扁平结构的名字空间集;而包的可嵌套性,使得多个模块可以呈现出多层次结构的名字空间树。

二、导入语句

如果要在一个模块A中使用另一个模块B(即访问模块B的属性),则必须首先 导入 模块B。此时模块A称为导入模块(即importer),而模块B称为被导入模块(即importee)。

导入语句(import statement)有两种风格:import <>from <> import。对模块的导入同时支持这两种风格。

1、基本语法

1)导入模块module(重命名为name)

import module [as name]

>>> import sys
>>> sys.version
'2.7.3 (default, Apr 10 2013, 05:46:21) \n[GCC 4.6.3]'
>>> import sys as s
>>> s.version
'2.7.3 (default, Apr 10 2013, 05:46:21) \n[GCC 4.6.3]'

2)导入模块module1(重命名为name1),模块module2(重命名为name2),等等

import module1 [as name1], module2 [as name2], ...

>>> import sys, os
>>> sys.platform, os.name
('linux2', 'posix')
>>> import sys as s, os as o
>>> (s.platform, o.name)
('linux2', 'posix')

3)从模块module中导入属性attribute(重命名为name)

from module import attribute [as name]

>>> from sys import executable
>>> executable
'/usr/bin/python'
>>> from sys import executable as exe
>>> exe
'/usr/bin/python'

4)从模块module中导入属性attribute1(重命名为name1),属性attribute2(重命名为name2),等等

from module import attribute1 [as name1], attribute2 [as name2], ...

>>> from sys import platform, executable
>>> platform, executable
('linux2', '/usr/bin/python')
>>> from sys import platform as plf, executable as exe
>>> plf, exe
('linux2', '/usr/bin/python')

5)从模块module中导入属性attribute1(重命名为name1),属性attribute2(重命名为name2),等等

from module import (attribute1 [as name1], attribute2 [as name2], ...)

>>> from sys import (platform, executable)
>>> platform, executable
('linux2', '/usr/bin/python')
>>> from sys import (platform as plf, executable as exe)
>>> plf, exe
('linux2', '/usr/bin/python')

6)从模块module中导入所有属性

from module import *

>>> from sys import *
>>> platform, executable
('linux2', '/usr/bin/python')

2、推荐风格

以下是在Python程序中推荐使用的导入语句:

  • import module [as name](导入单个模块)
  • from module import attribute [as name](导入单个属性)
  • from module import attribute1 [as name1], attribute2 [as name2], ...(导入较少属性时,单行书写)
  • from module import (attribute1 [as name1], attribute2 [as name2], ...)(导入较多属性时,分行书写)

应当尽量避免使用的导入语句是:

  • import module1 [as name1], module2 [as name2], ...

    它会降低代码的可读性,应该用多个import module [as name]语句代替。

  • from module import *

    它会让importer的名字空间变得不可控(很可能一团糟)。

三、模块

1、模块名

一个 模块 就是一个Python源码文件。如果文件名为mod.py,那么模块名就是mod。

模块的导入和使用都是借助模块名来完成的,模块名的命名规则与变量名相同。

2、模块属性

模块属性 是指在模块文件的全局作用域内,或者在模块外部(被其他模块导入后)可以访问的所有对象名字的集合。这些对象名字构成了模块的名字空间,这个名字空间其实就是全局名字空间(参考 名字空间与作用域)。

模块的属性由两部分组成:固有属性新增属性。可以通过 M.__dict__dir(M) 来查看模块M的属性。

1)固有属性

固有属性 是Python为模块默认配置的属性。

例如,新建一个空文件mod.py:

$ touch mod.py
$ python
...
>>> import mod # 导入模块mod
>>> mod.__dict__ # 模块mod的属性全貌
{'__builtins__': {...}, '__name__': 'mod', '__file__': 'mod.pyc', '__doc__': None, '__package__': None} 
>>> dir(mod) # 只查看属性名
['__builtins__', '__doc__', '__file__', '__name__', '__package__']

上述示例中,空模块mod的所有属性都是固有属性,包括:

  • __builtins__ 内建名字空间(参考 名字空间
  • __file__ 文件名(对于被导入的模块,文件名为绝对路径格式;对于直接执行的模块,文件名为相对路径格式)
  • __name__ 模块名(对于被导入的模块,模块名为去掉“路径前缀”和“.pyc后缀”后的文件名,即os.path.splitext(os.path.basename(__file__))[0];对于直接执行的模块,模块名为__main__
  • __doc__ 文档字符串(即模块中在所有语句之前第一个未赋值的字符串)
  • __package__ 包名(主要用于相对导入,请参考 PEP 366

2)新增属性

新增属性 是指在模块文件的顶层(top-level),由赋值语句(如import、=、def和class)创建的属性。

例如,修改文件mod.py为:

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

'''this is a test module'''

import sys

debug = True

_static = ''

class test_class(object): 
    def say(self): pass

def test_func(): 
    var = 0

再次查看模块mod的属性:

>>> import mod
>>> dir(mod)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '_static', 'debug', 'sys', 'test_class', 'test_func']

对比上一小节可知,除开固有属性外的其他属性都是新增属性,包括:

  • sys(由“import”创建)
  • debug和_static(均由“=”创建)
  • test_class(由“class”创建)
  • test_func(由“def”创建)

这些属性的共同点是:它们都在模块文件的顶层创建。相比之下,类方法say(在类test_class内部创建)和局部变量var(在函数test_func内部创建)都不在顶层,因此不在新增属性之列。(作为一个例子,”’this is a test module”’就是模块mod的文档字符串,即mod.__doc__的值)

3、可导出的公有属性

在 『导入语句』 中描述的基本语法可以归纳为三类:

  • import module 在导入模块(importer)中,可以通过module.*的形式访问模块module中的所有属性
  • from module import attribute 只能通过名字attribute访问模块module中的指定属性module.attribute
  • from module import * 可以直接通过属性名访问模块module中的所有 公有属性

换句话说,模块的 公有属性 就是那些可以通过from module import *被导出给其他模块直接使用的属性。

模块的公有属性有以下特点:

  • 可以在模块中定义一个特殊列表__all__,其中包含所有可导出的公有属性的字符串名称,从而实现对公有属性的定制
  • 如果没有定义__all__,那么默认所有不以下划线“_”开头的属性都是可导出的公有属性

以 『新增属性』 中的mod.py为例,没有定义__all__的公有属性:

>>> dir() # 导入模块mod前的名字空间
['__builtins__', '__doc__', '__name__', '__package__']
>>> from mod import * # 导入模块mod中的所有公有属性
>>> dir() # 导入模块mod后的名字空间
['__builtins__', '__doc__', '__name__', '__package__', 'debug', 'sys', 'test_class', 'test_func']

对比导入模块mod前后的情况可知,模块mod中的sys、debug、test_class和test_func都属于公有属性,因为它们的名字不以“_”开头;而其他以“_”开头的属性(包括所有“固有属性”,以及“新增属性”中的_static)都不在公有属性之列。

如果在模块文件mod.py中顶层的任何位置,增加定义一个特殊列表__all__ = ['sys', '_static', 'test_func'](此时__all__也是模块属性),那么此时的公有属性:

>>> dir() # 导入模块mod前的名字空间
['__builtins__', '__doc__', '__name__', '__package__']
>>> from mod import * # 导入模块mod中的所有公有属性
>>> dir() # 导入模块mod后的名字空间
['__builtins__', '__doc__', '__name__', '__package__', '_static', 'sys', 'test_func']

可以看出,只有在__all__中指定的属性才是公有属性。

4、直接执行

模块可以在命令行下被直接执行,以模块mod(对应文件mod.py)为例:

1)以脚本方式执行

python mod.py <arguments>

2)以模块方式执行

python -m mod <arguments>

四、包

一个 就是一个含有__init__.py文件的目录。

包与模块之间的包含关系是:一个包可以包含子包或子模块,但一个模块却不能包含子包和子模块。

1、包名

与模块名类似,包名的命名规则也与变量名相同。

此外,需要特别注意的是:如果在同一个目录下,存在两个同名的包和模块,那么导入时只会识别包,而忽略模块。(参考 specification for packages 中的 『What If I Have a Module and a Package With The Same Name?』)

例如,在目录dir下新建一个文件spam.py(即模块spam),此时import spam会导入模块spam:

$ cd dir/
$ touch spam.py
$ python
...
>>> import spam
>>> spam
<module 'spam' from 'spam.py'>

如果在目录dir下再新建一个含有__init__.py文件的目录spam(即包spam),此时import spam则会导入包spam(而不再是模块spam):

$ mkdir spam && touch spam/__init__.py
$ python
...
>>> import spam
>>> spam
<module 'spam' from 'spam/__init__.py'>

2、包属性

包属性与模块属性非常相似,也分为 固有属性新增属性

1)固有属性

与模块相比,包的 固有属性 仅多了一个__path__属性,其他属性完全一致(含义也类似)。

__path__属性即包的路径(列表),用于在导入该包的子包或子模块时作为搜索路径;修改一个包的__path__属性可以扩展该包所能包含的子包或子模块。(参考 Packages in Multiple Directories

例如,在dir目录下新建一个包pkg(包含一个模块mod),显然在包pkg中只能导入一个子模块mod:

$ mkdir pkg && touch pkg/__init__.py
$ touch pkg/mod.py
$ python
...
>>> import pkg.mod # 可以导入子模块mod
>>> import pkg.mod_1 # 不能导入子模块mod_1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named mod_1

如果在dir目录下再新建一个包pkg_1(包含一个模块mod_1):

$ mkdir pkg_1 && touch pkg_1/__init__.py
$ touch pkg_1/mod_1.py

并且在pkg/__init__.py中修改包pkg的__path__属性:

print('before:', __path__)
__path__.append(__path__[0].replace('pkg', 'pkg_1')) # 将“包pkg_1所在路径”添加到包pkg的__path__属性中
print('after:', __path__)

此时,在包pkg中就可以导入子模块mod_1(仿佛子模块mod_1真的在包pkg中):

$ python
...
>>> import pkg.mod # 可以导入子模块mod
('before:', ['pkg'])
('after:', ['pkg', 'pkg_1'])
>>> import pkg.mod_1 # 也可以导入子模块mod_1

2)新增属性

包的 新增属性 包括两部分:静态的新增属性和动态的新增属性。

静态的新增属性是指:在__init__.py的顶层(top-level),由赋值语句(如import、=、def和class)创建的属性。这部分与模块的新增属性一致。

动态的新增属性是指:在执行导入语句后动态添加的新增属性。具体而言,如果有一个导入语句导入了某个包pkg中的子模块submod(或子包subpkg),那么被导入的子模块submod(或子包subpkg)将作为一个属性,被动态添加到包pkg的新增属性当中。

以包含模块mod的包pkg为例:

>>> import pkg
>>> dir(pkg)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__']
>>> import pkg.mod # 该语句导入了包pkg中的模块mod
>>> dir(pkg) # mod成为了包pkg的“动态的新增属性”
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'mod']

3、可导出的公有属性

在公有属性方面,包与模块的行为完全一致,当然别忘了包还有 动态的新增属性

4、其他

1)导入语句

加入包的概念以后,导入语句的风格(与仅有模块时相比)不变,但是语法上有一些细微差异:用“.”来表示包与模块之间的包含关系;可操作的对象扩充为 模块属性

下面是涉及包时,一些典型的导入语句:

  • import package
  • import package.module
  • import package.subpackage
  • import package.subpackage.module
  • from packagae import module
  • from packagae import subpackagae
  • from packagae import subpackagae.modulefrom packagae.subpackagae import module
  • from packagae.module import attribute
  • from packagae.subpackagae.module import attribute

2)__init__.py

关于包的__init__.py,有以下几点总结:

  • 一般为空即可
  • 有时也可以放置一些初始化代码,用于在包加载时执行
  • 少数情况下,还可以用于定制包的一些属性(如__all____path__等)

五、导入原理

模块与包的导入原理几乎完全一致,因此下面以模块为主进行讨论,仅在有显著差异的地方对包作单独说明。

1、导入依赖

对于模块M而言,根据导入语句的不同(指明了模块M是否在一个包中),可能存在导入依赖的问题:

  • import M

    模块M不在一个包中,因此无导入依赖:直接以“M”为 完整名(fully qualified name)导入模块M

  • import A.B.M或者from A.B import M

    模块M在一个子包B中,而子包B又在一个包A中,因此存在导入依赖:会首先以“A”为 完整名 导入包A,接着以“A.B”为 完整名 导入子包B,最后以“A.B.M”为 完整名 导入模块M。

2、导入过程

一个模块的导入过程主要分三步:搜索加载名字绑定。(具体参考 The import statement

1)搜索

搜索 是整个导入过程的核心,也是最为复杂的一步。对于被导入模块M,按照先后顺序,搜索的处理步骤为:

  • 在缓存 sys.modules 中查找模块M,若找到则直接返回模块M
  • 否则,顺序搜索 sys.meta_path,逐个借助其中的 finder 来查找模块M,若找到则加载后返回模块M
  • 否则,如果模块M在一个包P中(如import P.M),则以P.__path__为搜索路径进行查找;如果模块M不在一个包中(如import M),则以 sys.path 为搜索路径进行查找

2)加载

正如 『搜索』 步骤中所述,对于找到的模块M:如果M在缓存 sys.modules 中,则直接返回;否则,会加载M。

加载 是对模块的初始化处理,包括以下步骤:

  • 设置属性:包括__name____file____package____loader__(对于包,则还有__path__
  • 编译源码:将模块文件(对于包,则是其对应的__init__.py文件)编译为字节码(*.pyc),如果字节码文件已存在且仍然是最新的,则不会重编
  • 执行字节码:执行编译生成的字节码(即模块文件或__init__.py文件中的语句)

有一点值得注意的是,加载不只是发生在导入时,还可以发生在 reload() 时。

3)名字绑定

加载完importee模块后,作为最后一步,import语句会为 导入的对象 绑定名字,并把这些名字加入到importer模块的名字空间中。其中,导入的对象 根据导入语句的不同有所差异:

  • 如果导入语句为import obj,则对象obj可以是包或者模块
  • 如果导入语句为from package import obj,则对象obj可以是package的子包、package的属性或者package的子模块
  • 如果导入语句为from module import obj,则对象obj只能是module的属性

3、更多细节

根据 The import statement 中的描述,以下是导入原理对应的Python伪码:

import sys
import os.path

def do_import(name):
    '''导入'''

    parent_pkg_name = name.rpartition('.')[0]
    if parent_pkg_name:
        parent_pkg = do_import(parent_pkg_name)
    else:
        parent_pkg = None
    return do_find(name, parent_pkg)

def do_find(name, parent_pkg):
    '''搜索'''

    if not name:
        return None

    # step 1
    if name in sys.modules:
        return sys.modules[name]
    else:
        # step 2
        for finder in sys.meta_path:
            module = do_load(finder, name, parent_pkg)
            if module:
                return module
        # step 3
        src_paths = parent_pkg.__path__ if parent_pkg else sys.path
        for path in src_paths:
            if path in sys.path_importer_cache:
                finder = sys.path_importer_cache[path]
                if finder:
                    module = do_load(finder, name, parent_pkg)
                    if module:
                        return module
                else:
                    # handled by an implicit, file-based finder
            else:
                finder = None
                for callable in sys.path_hooks:
                    try:
                        finder = callable(path)
                        break
                    except ImportError:
                        continue
                if finder:
                    sys.path_importer_cache[path] = finder
                elif os.path.exists(path):
                    sys.path_importer_cache[path] = None
                else:
                    sys.path_importer_cache[path] = # a finder which always returns None
                if finder:
                    module = do_load(finder, name, parent_pkg)
                    if module:
                        return module
        raise ImportError

def do_load(finder, name, parent_pkg):
    '''加载'''

    path = parent_pkg.__path__ if parent_pkg else None
    loader = finder.find_module(name, path)
    if loader:
        return loader.load_module(name)
    else:
        return None

4、sys.path

正如 『导入过程』 中所述,sys.path是 不在包中的模块(如import M)的“搜索路径”。在这种情况下,控制sys.path就能控制模块的导入过程。

sys.path 是一个路径名的列表,按照先后顺序,其中的路径主要分为以下四块:

  • 程序主目录(默认定义):如果是以脚本方式启动的程序,则为 启动脚本所在目录;如果在交互式命令行中,则为 当前目录
  • PYTHONPATH目录(可选扩展):以 os.pathsep 分隔的多个目录名,即环境变量os.environ['PYTHONPATH'](类似shell环境变量PATH)
  • 标准库目录(默认定义):Python标准库所在目录(与安装目录有关)
  • .pth文件目录(可选扩展):以“.pth”为后缀的文件,其中列有一些目录名(每行一个目录名),用法参考 site

为了控制sys.path,可以有三种选择:

  • 直接修改sys.path列表
  • 使用PYTHONPATH扩展
  • 使用.pth文件扩展

六、重新加载

关于导入,还有一点非常关键:加载只在第一次导入时发生。这是Python特意设计的,因为加载是个代价高昂的操作。

通常情况下,如果模块没有被修改,这正是我们想要的行为;但如果我们修改了某个模块,重复导入不会重新加载该模块,从而无法起到更新模块的作用。有时候我们希望在 运行时(即不终止程序运行的同时),达到即时更新模块的目的,内建函数 reload() 提供了这种 重新加载 机制。

关键字reloadimport不同:

  • import是语句,而reload是内建函数
  • import使用 模块名,而reload使用 模块对象(即已被import语句成功导入的模块)

重新加载(reload(module))有以下几个特点:

  • 会重新编译和执行模块文件中的顶层语句
  • 会更新模块的名字空间(字典 M.__dict__):覆盖相同的名字(旧的有,新的也有),保留缺失的名字(旧的有,新的没有),添加新增的名字(旧的没有,新的有)
  • 对于由import M语句导入的模块M:调用reload(M)后,M.x新模块 的属性x(因为更新M后,会影响M.x的求值结果)
  • 对于由from M import x语句导入的属性x:调用reload(M)后,x仍然是 旧模块 的属性x(因为更新M后,不会影响x的求值结果)
  • 如果在调用reload(M)后,重新执行import M(或者from M import x)语句,那么M.x(或者x)为 新模块 的属性x

七、相对导入

严格来说,模块(或包)的导入方式分为两种:绝对导入相对导入。以上讨论的导入方式都称为 绝对导入,这也是Python2.7的默认导入方式。相对导入是从Python2.5开始引入的,主要用于解决“用户自定义模块可能会屏蔽标准库模块”的问题(参考 Rationale for Absolute Imports)。

相对导入 使用前导的“.”来指示importee(即被导入模块或包)与importer(当前导入模块)之间的相对位置关系。相对导入 只能使用from <> import风格的导入语句,import <>风格的导入语句只能用于 绝对导入。(相对导入的更多细节,请参考 PEP 328

1、导入语句

例如有一个包的布局如下:

pkg/
    __init__.py
    subpkg1/
        __init__.py
        modX.py
        modY.py
    subpkg2/
        __init__.py
        modZ.py
    modA.py

假设当前在文件modX.py或subpkg1/__init__.py中(即当前包为subpkg1),那么下面的导入语句都是相对导入:

from . import modY            # 从当前包(subpkg1)中导入模块modY
from .modY import y           # 从当前包的模块modY中导入属性y
from ..subpkg2 import modZ    # 从当前包的父包(pkg)的包subpkg2中导入模块modZ
from ..subpkg2.modZ import z  # 从当前包的父包的包subpkg2的模块modZ中导入属性z
from .. import modA           # 从当前包的父包中导入模块modA
from ..modA import a          # 从当前包的父包的模块modA中导入属性a

2、导入原理

与绝对导入不同,相对导入的导入原理比较简单:根据 模块的__name__属性 和 由“.”指示的相对位置关系 来搜索并加载模块(参考 Relative Imports and __name__)。

3、直接执行

由于相对导入会用到模块的__name__属性,而在直接执行的主模块中,__name__值为__main__(没有包与模块的信息),所以在主模块中:尽量全部使用绝对导入。

如果非要使用相对导入,也可以在顶层包(top-level package)的外部目录下,以模块方式执行主模块:python -m pkg.mod(假设顶层包为pkg,mod为主模块,其中使用了相对导入)。(具体参考 PEP 366

点赞