第5部分 模块与包

第22章 模块

模块是最高级别的程序组织单元,它将程序代码和数据封装起来以便重用。

每个Python源文件都是一个模块。

模块可以由import语句和from语句,以及内置函数imp.reload进行处理:

  • import:使客户端(导入者)以一个整体获得一个模块。
  • from:允许客户端从一个模块文件中获取特定的变量名。
  • imp.reload:在不终止Python程序的情况下,提供了一种重新载入模块文件代码的方法。

22.1 为什么使用模块

在一个模块文件顶层定义的所有的变量名都成了被导入的模块对象的属性。也就是说,导入操作给予了对模块的全局作用域中的变量名的读取权。

在模块导入时,模块文件的全局作用域变成了模块对象的命名空间。

从抽象的视角来看,模块至少有3个角色:

  • 代码重用

  • 系统命名空间的划分

  • 实现共享服务和数据

22.2 Python程序架构

一个程序就是一个模块的系统。它有一个顶层脚本文件(启动后运行程序)以及多个模块文件(用来导入工具库)。

脚本模块 都是包含了Python语句的文本文件,尽管在模块中的语句通常都是创建之后使用的对象。

Python 标准库提供了一系列的预先编写好的模块。

如何组织一个程序

一般来说,一个Python程序包括了多个含有Python语句的文本文件。程序是作为一个主体的、顶层的文件来构造的。这个顶层的主体文件可能配合有多个模块文件来提供支持。

在Python中,顶层文件包含了程序的主要的控制流程。模块文件就是工具的库。

在Python中,一个文件导入了一个模块来获得这个模块定义的工具的访问权,这些工具被认作是这个模块的属性。

导入和属性

下图是一个包含有三个文件的Python程序的草图。文件a是顶层文件,在运行时将会从上至下执行其中的语句;文件b和文件c是模块,通常模块中的语句并不直接运行。

导入的概念贯穿了Python,任何文件都能从任何其他文件中导入其工具。例如,模块a导入了模块b,模块b导入了模块c,模块c又导入了其他模块。这就形成了 导入链

标准库模块

Python自带了很多实用的莫夸,成为标准链接库。这些模块包含了平台不相关的常见程序设计任务。

22.3 import如何工作

在Python中,导入 操作其实是运行时的运算。

程序第一次import指定文件时,会执行3个步骤: 1. 搜索模块文件:找到import语句所引用的模块文件。 2. 编译(可选):Python会检查文件的时间戳,如果发现字节码文件比源文件旧,会自动重新编译字节码;否则,跳过编译步骤。如果Python在搜索路径上只发现了字节码文件,而没有源文件,就会直接加载字节码文件。 3. 运行:import操作的最后步骤是执行模块的字节码。文件中所有语句会从头至尾依次执行。

注意:以上3个步骤仅会在模块第一次导入时进行;在这之后,重复导入相同模块时,会跳过这3个步骤,而只提取内存中已加载的模块对象。

因为导入操作的最后一步(第3步)实际上是执行文件的程序代码,所以如果模块文件中任何顶层代码确实做了什么实际的工作,你就会在导入时看见其结果。

从技术上讲,Python把载入的模块存储到一个名为sys.modules的字典中,并在一次导入操作的开始检查该表。如果模块不存在,将会执行这3个步骤。

实际上,如果想要看看已经导入了哪些模块,可以导入sys模块,并打印list(sys.modules.keys())

22.4 字节码文件:__pycache__ in Python 3.2+

第五版,暂略

22.5 模块搜索路径

sys.path是Python的模块搜索路径,它由以下4个路径组件构成: 1. 程序的主目录 2. PYTHONPATH目录(如果已经设置):如果设置了PYTHONPATH,Python会从左至右搜索PYTHONPATH环境变量中所有的目录。 3. 标准链接库目录 4. 任何.pth文件的内容(如果存在的话):Python允许用户把有效的目录在后缀名为.pth的文本文件中一行一行地列出目录,然后依次对其中的目录进行搜索。 5. 第三方扩展的Lib/site-packages目录:Python会自动添加其标准库的site-packages子目录到模块搜索路径。

Python在程序启动时,会自动根据以上4个路径组件对sys.path进行配置。同时,也可以手动对sys.path进行调整,以修改模块搜索路径。

配置搜索路径

搜索路径的变动

sys.path列表

可以通过打印sys.path列表来查看搜索路径的实际配置。

在导入模块时,Python会由左至右搜索这列表中的每个目录。

模块文件选择

记住,文件名的后缀(例如,.py.pyc)是刻意从import语句中省略的。

Python会选择在搜索路径中第一个符合导入文件名的文件。

如果在相同目录中找到b.pyb.so,会发生什么事?

答:建议在同一目录中保持模块名唯一!

导入钩子(import hook)和ZIP文件

使用导入钩子(import hook)刻意重新定义 Python 中 import 操作所做的事。例如,使用钩子可以在导入时自动解压ZIP文件并归档文件。

更多细节参考Python标准库中关于内置函数__import__的说明。

优化字节码文件(Optimized byte code files)

Python 也支持优化字节码文件.pyo。这种文件在创建和执行时要加上-O命令行标志。这种字节码文件比普通的.pyc字节码文件运行速度稍快一点(通常快5%),但不常使用。

Python的第三方扩展通常使用标准链接库中的distutils工具来自动安装,所以不需要路径设置,就能使用它们的代码。


第23章 模块编码基础

23.1 模块的创建

任何保存有Python源代码,且以.py为后缀名的文本文件,都被认为是Python模块。所以保存Python源代码到以.py为后缀名的文本文件就是在创建模块。

因为模块名在Python程序中会变成变量名,因此,模块的命名应遵循变量名的命名规则。

23.2 模块的使用

import语句

import语句使用一个变量名引用整个模块对象,所以必须通过模块名称来得到该模块的属性:

>>> import module1               # Get module as a whole (one or more)
>>> module1.printer('Hello world!')      # Qualify to get names
Hello world!

from语句

from语句会把变量名复制到另一个作用域,所以它就可以让我们直接在脚本中使用复制后的变量名,而不需要通过模块:

>>> from module1 import printer # Copy out a variable (one or more)
>>> printer('Hello world!') # No need to qualify name
Hello world!

from * 语句

使用*时,会取得模块顶层所有赋了值的变量名的拷贝。

>>> from module1 import * # Copy out _all_ variables
>>> printer('Hello world!')
Hello world!

import和from是赋值语句

就像def一样,importfrom是可执行的语句,而不是编译期间的声明,而且它们可以嵌套在if测试中,出现在函数def之中等,直到Python程序执行到这些语句时才会进行解析。

def一样,importfrom都是隐性的赋值语句: - import将整个模块对象赋值给一个变量名。 - from将一个或多个变量名赋值给另一个模块中同名的对象。

之前讨论过的关于赋值语句方面的内容,也适用于模块的导入。例如,以from复制的变量名会变成对共享对象的引用。思考下面的small.py模块。

x = 1
y = [1, 2]
% python
>>> from small import x, y # Copy two names out
>>> x = 42 # Changes local x only
>>> y[0] = 42 # Changes shared mutable in place

此处,x并不是一个共享的可变对象,但y是。导入者中的变量名y和被导入者都引用相同的列表对象,所以在其中一个地方的修改,也会影响另一个地方的这个对象。

>>> import small # Get module name (from doesn't)
>>> small.x # Small's x is not my x
1
>>> small.y # But we share a changed mutable
[42, 2]

跨文件变量名的改变

% python
>>> from small import x, y # Copy two names out
>>> x = 42 # Changes my x only

以from复制而来的变量名和其来源的的文件之间是没有关系的。为了实际修改另一个文件中的全局变量名,必须使用import:

>>> import small # Get module name
>>> small.x = 42 # Changes x in other module

注意:这与前一小节中对y[0]的修改是不同的。这里修改了一个对象small,而不是一个变量名。

import和from的等价性

from只是把变量名从一个模块复制到另一个模块,并不会对模块名本身进行赋值。所以我们需要在from后执行import语句,来获取模块的变量名。从概念上来讲,一个像这样的from语句:

from module import name1, name2 # Copy these two names out (only)

与下面这些语句是等效的:

import module # Fetch the module object
name1 = module.name1 # Copy names out by assignment
name2 = module.name2
del module # Get rid of the module name

from语句潜在的陷阱

from module import *形式会把一个命名空间融入到另一个,所以会使得模块的命名空间的分割特性失效。

23.3 模块的命名空间

简而言之,模块就是命名空间,而存在于模块之内的变量名就是模块对象的属性。

文件生成命名空间

那么,文件是如何变为命名空间的呢?简而言之,在模块文件顶层(也就是不在函数或类的主体内)每一个赋值了的变量名都会变成该模块的属性。

  • 模块语句会在首次导入时执行
  • 顶层的赋值语句(例如,=def)会变为模块的属性
  • 模块的命名空间能够通过属性__dict__dir(Module)获取:由导入而建立的模块的命名空间是字典。可通过模块对象相关联的内置属性__dict__来读取,也能通过dir函数查看。
  • 模块是一个独立的作用域(本地变量就是全局变量):在模块导入后,模块文件的作用域就变成了模块对象的属性的命名空间。

命名空间字典:__dict__

第五版,暂略

属性名的点号运算

在Python中,可以使用点号运算语法object.attribute获取任意的object的attribute属性。点号运算是一个表达式,传回和对象相匹配的属性名的值。

以下是点号运算的规则:

  • 点号运算:X.Y是指在当前范围内搜索X,然后搜索对象X之中的属性Y。
  • 多层点号运算:X.Y.Z指的是寻找对象X之中的变量名Y,然后再找对象X.Y之中的Z
  • 点号运算的通用性:点号运算可用于任何具有属性的对象,如模块、类、C扩展类型等。

导入和作用域

导入操作不会赋予被导入文件中的代码对上层代码(进行导入操作的文件)的可见度,即,被导入文件无法看见进行导入文件内的变量名。更确切的说法:

  • 函数绝对无法看见其他函数内的变量名,除非它们从物理上处于这个函数内。
  • 模块程序代码绝对无法看见其他模块内的变量名,除非明确地进行导入。

例如,有一个模块moda.py

X = 88
def f():
    global X
    X = 99

有另一个模块modb.py

X = 11

import modea
modea.f()
print(X, modea.X)

执行modb.py时,moda.f修改模块moda中的X,而不是modb中的Xmoda.f的全局作用域一定是其所在的文件,无论这个函数是由哪个文件调用的:

% python modeb.py
11 99

命名空间的嵌套

利用属性的点号运算路径,有可能深入到任意嵌套的模块中并读取模块属性。

例如,有一个模块mod3.py如下:

X = 3

另一个模块mod2.py如下:

X = 2
import mod3

print(X, end=' ') # My global X
print(mod3.X) # mod3's X

还有一个模块mod1.y如下:

X = 1
import mod2

print(X, end=' ') # My global X
print(mod2.X, end=' ') # mod2's X
print(mod2.mod3.X) # Nested mod3's X

利用mod2.mod3.X变量名路径,就可以深入到所导入的mod2内嵌套了的mod3。结果就是mod1可以看见三个文件内的X,因此,可以读取这三个全局范围。

23.4 重载模块

imp模块的reload函数会强制已加载的模块的代码重新载入并重新执行。此文件中新的代码的赋值语句会在适当的地方修改现有的模块对象。

In Python 3.7.2, the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses.

为什么要这么麻烦去重载模块?reload函数可以修改程序的一些部分,而无须停止整个程序。因此,利用reload函数,可以立即看到对组件的修改的效果。

reload基础

importfrom不同的是:

  • reload是Python中的内置函数,而不是语句。
  • 传给reload的必须是已经成功导入的模块对象,而不是变量名。
  • reload在Python 3.0中位于imp模块之中,并且必须先导入。

当调用reload时,Python会重读模块文件的源代码,重新执行其顶层语句。

realod会在适当的地方修改模块对象,而不会删除并重建模块对象。因此,程序中任何引用该模块对象的地方,自动会受到reload的影响:

  • reload会在模块当前命名空间内执行模块文件的新代码。
  • 文件中顶层赋值语句会使得变量名换成新值。
  • 重载会影响所有使用import读取了模块的客户端。
  • 重载只会对以后使用from的客户端造成影响。

第24章 模块包

除了模块名之外,导入也可以指定目录路径。Python代码的目录就称为 ,因此,这类导入就称为 包导入

实际上,包导入是把计算机上的目录变成另一个Python命名空间,而属性则对应于目录中所包含的子目录和模块文件。

24.1 包导入基础

import dir1.dir2.mod
from dir1.dir2.mod import x

假设,目录dir1所在的目录(即dir1的父目录)是dir0,则目录dir0必须存在于sys.path模块搜索路径中。

__init__.py包文件

使用包导入必须遵循一条约束:包导入语句的路径中的每个目录内都必须存在__init__.py这个文件,否则导入包会失败。

也就是说,上面的例子中:

  • 目录dir1dir2内都必须包含__init__.py这个文件。
  • 而目录dir1的父目录dir0则不需要包含__init__.py文件;如果包含,也会被忽略。
  • 目录dir0必须存在于模块搜索路径sys.path中。

结果就是,这个例子的目录结构应该是这样的:

root@localhost# tree dir0 -v
dir0
└── dir1
    ├── __init__.py
    └── dir2
        ├── __init__.py
        └── mod.py

2 directories, 3 files
root@localhost#

__init__.py文件可以包含Python程序代码,就像普通模块文件一样,但也可以是空文件。

通常情况下,__init__.py文件扮演了包初始化的钩子、替目录产生模块命名空间以及使用目录导入时实现from *(即from xxx import *)行为的角色:

  • 包的初始化:Python首次导入某个目录时,会自动执行该目录下__init__.py文件中的所有程序代码。所以,该文件可以放置包内文件所需要初始化的代码。
  • 模块命名空间初始化
  • from *语句的行为:作为一个高级功能,你可以在__init__.py文件内使用__all__列表来定义包(目录)以from *形式导入时应该导入什么。如果没有设定__all__from *语句不会自动加载嵌套于该目录内的子模块,而是只加载该目录的__init__.py文件中赋值语句定义的变量名(包括该文件中程序代码明确导入的子模块)。

24.2 包导入实例

有如下目录结构:

root@localhost# tree dir0 -v
dir0
└── dir1
    ├── __init__.py
    └── dir2
        ├── __init__.py
        └── mod.py

2 directories, 3 files
root@localhost#

其中文件dir1\__init__.py包含如下内容:

# dir1\__init__.py
print('dir1 init')
x = 1

其中文件dir1\dir2\__init__.py包含如下内容:

# dir1\dir2\__init__.py
print('dir2 init')
y = 2

其中文件dir1\dir2\mod.py包含如下内容:

# dir1\dir2\mod.py
print('in mod.py')
z = 3

dir1目录的父目录dir0添加入模块搜索路径:

Python 3.7.2 (default, Mar 25 2019, 20:38:07)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path.append('/mnt/c/dir0')

开始导入包(目录)中的模块:

>>> import dir1.dir2.mod      # 第一次导入时,会遍历包(目录),并运行各个包(目录)中的__init__.py
dir1 init
dir2 init
in mod.py
>>> import dir1.dir2.mod         # 重复导入则不会运行__init__.py
>>> import imp
__main__:1: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
>>> imp.reload(dir1)              # 使用reload函数重载包(目录)dir1
dir1 init
<module 'dir1' from '/mnt/c/dir0/dir1/__init__.py'>
>>>
>>> imp.reload(dir1.dir2)          # 使用reload函数重载包(目录)dir2
dir2 init
<module 'dir1.dir2' from '/mnt/c/dir0/dir1/dir2/__init__.py'>
>>> imp.reload(dir1.dir2.mod)      # 使用reload函数重载模块mod
in mod.py
<module 'dir1.dir2.mod' from '/mnt/c/dir0/dir1/dir2/mod.py'>

也可以单独导入某个包(目录):

>>> import dir1         # 首次单独导入包(目录)时,只会触发该目录的__init__.py
dir1 init
>>> import dir1.dir2    # 首次单独导入包(目录)时,只会触发该目录的__init__.py
dir2 init
>>> import dir1.dir2.mod   # 首次导入包中的模块也会执行该模块
in mod.py
>>> import dir1         # 重复导入包(目录),则不会触发执行该目录的__init__.py
>>> import dir1.dir2    # 重复导入包(目录),则不会触发执行该目录的__init__.py
>>> import dir1.dir2.mod   # 重复导入则不会执行该模块
>>>

包(目录)的重载不会遍历其子包(目录):

>>> import imp
__main__:1: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
>>> imp.reload(dir1)
dir1 init
<module 'dir1' from '/mnt/c/dir0/dir1/__init__.py'>
>>>
>>> imp.reload(dir1.dir2)
dir2 init
<module 'dir1.dir2' from '/mnt/c/dir0/dir1/dir2/__init__.py'>
>>> imp.reload(dir1.dir2.mod)
in mod.py
<module 'dir1.dir2.mod' from '/mnt/c/dir0/dir1/dir2/mod.py'>

Since Python 3.4, the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses.

注意:从Python 3.4开始,imp模块已经被标记为 is pending deprecation。在Python 3.7.2中已被标记为 deprecated,建议使用importlib模块的reload函数。

rootlocalhost:/mnt/c# python
Python 3.7.2 (default, Mar 25 2019, 20:38:07)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path.append('/mnt/c/dir0')
>>> import dir1.dir2.mod
dir1 init
dir2 init
in mod.py
>>> import importlib
>>> importlib.reload(dir1)
dir1 init
<module 'dir1' from '/mnt/c/dir0/dir1/__init__.py'>
>>> importlib.reload(dir1.dir2)
dir2 init
<module 'dir1.dir2' from '/mnt/c/dir0/dir1/dir2/__init__.py'>
>>> importlib.reload(dir1.dir2.mod)
in mod.py
<module 'dir1.dir2.mod' from '/mnt/c/dir0/dir1/dir2/mod.py'>
>>>

导入后,import语句内的路径会变成脚本的嵌套对象路径。如,mod是对象,嵌套在对象dir2中,而dir2又嵌套在对象dir1中:

>>> dir1
<module 'dir1' from '/mnt/c/dir0/dir1/__init__.py'>
>>> dir1.dir2
<module 'dir1.dir2' from '/mnt/c/dir0/dir1/dir2/__init__.py'>
>>> dir1.dir2.mod
<module 'dir1.dir2.mod' from '/mnt/c/dir0/dir1/dir2/mod.py'>

实际上,路径中的每个目录名称都变成赋值了模块对象的变量,而模块对象的命名空间则是由该目录内的__init__.py文件中所有赋值语句进行初始化的。

>>> dir1.x    # dir1.x引用了变量x,x是在dir1/__init__.py中赋值的
1
>>> dir1.dir2.y   # dir1.dir2.y引用了变量y,y是在dir1/dir2/__init__.py中赋值的
2
>>> dir1.dir2.mod.z  # mod.z引用的变量z则是在mod.py内赋值的
3

包对应的from语句和import语句

>>> from dir1.dir2 import mod # Code path here only
dir1 init
dir2 init
in mod.py
>>> mod.z # Don't repeat path
3
>>> from dir1.dir2.mod import z
>>> z
3
>>> import dir1.dir2.mod as mod # Use shorter name (see Chapter 25)
>>> mod.z
3
>>> from dir1.dir2.mod import z as modz # Ditto if names clash (see Chapter 25)
>>> modz
3

24.3 为什么使用包导入

在较大的程序中,包程序让导入更具信息性,并可以作为组织工具,简化模块的搜索路径,而且可以解决模糊性。

当有多个同名模块时,将模块保存到不同目录中,并将其创建为包,可以在导入时保证同名模块的唯一性。

24.4 包的相对导入

Python 3.0中的变化

相对导入基础知识

在 Python 3.X 和 Python 2.6 中,from语句可以使用前面的点号.来指定,它们需要位于同一包中的模块(所谓的包相对导入),而不是位于模块导入搜索路径上某处的模块(即所谓 绝对导入)。也就是说:

  • 在 Python 3.X 和 Python 2.6中,我们可以使用from语句前面的点号来表示,导入应该性对于外围的包。这样的导入将只是在包的内部搜索,并且不会搜索位于导入搜索路径sys.path中某处的同名模块。
  • 在Python 3.X中,不带点号的导入默认是绝对的。在缺少任何特殊的点号语法的时候,Python忽略包目录自身(即导入搜索路径的相对部分),并在sys.path搜索路径上进行绝对查找。

例如,在Python 3.X中,导入与当前文件位于同一包下的spam模块:

from . import spam

导入与包含这条导入语句的文件位于同一包下的模块spam中的变量名name

from .spam import name

为什么使用相对导入

相对导入的作用域

  • 相对导入只适用于包内导入:这种功能的模块搜索路径修改只针对位于包内的模块文件中的import语句,即包内导入(intrapackage imports)。
  • 相对导入只适用于from语句:这一功能的新语法只适用于from语句,而不适用于import语句。其特点为,一个以一个或多个点号开头的from语句中的模块名。模块名包含嵌入的点号,但没有以点号开头,这样的导入是包导入,而不是相对导入。

模块查找规则总结

关于包和相对导入,Python 3.X中的模块搜索可以总结为如下:

  • 简单模块名(例如,A)通过搜索sys.path路径列表上的每个目录来查找,从左到右进行。这个列表由系统模块设置和用户配置组成。
  • 包就是目录,这些目录中存在一个特殊的__init__.py文件的Python模块。这使得可以在导入操作中使用像A.B.C这样的目录路径语法。例如,导入A.B.C,目录A位于相对于sys.path的普通模块导入搜索,目录BA中的另一个包(子目录),CB中的一个模块或者其他可导入项。
  • 在一个包的文件中,普通importfrom语句就像其他地方的导入一样,使用相同的sys.path搜索规则。然而,在包中使用from语句和开头的点号的导入操作是相对于包的。也就是说,只有包目录会被检查,并且普通的sys.path查找不会被使用。例如,from . import A,模块搜索被限制在包含该from语句出现的文件的目录之中。

相对导入的应用

在包之外导入

/tmp/code/目录下有一个和标准库模块string.py同名的模块文件,其内容如下:

print('string' * 8)

如果/tmp/code/为当前工作目录,并想在此情况下导入模块string,则当前工作目录下的string.py模块就会被导入,因为模块搜索路径中的第一条是当前工作目录(CWD):

root@localhost:/tmp/code# python
Python 3.7.2 (default, Mar 25 2019, 20:38:07)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import string
stringstringstringstringstringstringstringstring
>>> string
<module 'string' from '/tmp/code/string.py'>
>>>

注意:如果不在作为包的一部分的模块文件中,是不允许使用相对导入语法的。

rootlocalhost:/tmp/code# python
Python 3.7.2 (default, Mar 25 2019, 20:38:07)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from . import string
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'string' from '__main__' (unknown location)

由于目录/tmp/code不包含__init__.py文件,所以此目录不是包,也就不允许使用针对包的相对导入语法。

包内的导入

Pitfalls of Package-Relative Imports: Mixed Use

第五版,暂略

24.5 Python 3.3 Namespace Packages

第五版,暂略


第25章 高级模块话题

25.1 模块设计概念

就像函数一样,模块的设计也需要权衡:需要思考那些函数要放进模块、模块的通信机制等。

  • 总是在Python的模块内编写代码。
  • 模块耦合要降到最低(Minimize module coupling):全局变量。
  • 最大化模块的黏合性(Maximize module cohesion):同一目标。
  • 模块应该尽可能少地修改其他模块的变量。修改另一个模块内的全局变量,通常是有问题的设计。应该试着通过函数参数和返回值这类机制去传递结果,而不是进行跨模块的修改。

下图描绘了模块操作的环境。模块包含全局变量、函数、类以及其他的模块(如果导入了)。函数有自己的本地变量。

Figure 25-1

25.2 在模块中隐藏数据

正如我们所看到的,Python模块会导出其文件顶层所赋值的所有变量名。在Python中,无法声明哪个变量名在模块外可见,哪个变量名在模块外不可见。实际上,如果客户端想的话,是没有办法阻止客户端修改一个模块中的变量名的。

在Python中,模块内的数据隐藏是一种惯例,而不是一种语法约束。

最小化from *的破坏:_X__all__

有种特殊的情况,把下划线放在变量名前面(例如_X),可以防止客户端使用from *语句导入模块时,把其中的这些变量名复制出去。

下划线不是“私有”声明。你还是可以使用其他导入形式看见并修改这类变量名,例如,使用import语句。

例如,有如下模块文件unders.py

# unders.py
a, _b, c, _d = 1, 2, 3, 4

可以看到,通过from *语句无法将变量名_b_d导入:

Python 3.7.2 (default, Mar 25 2019, 20:38:07)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from unders import *         # Load non _X names only
>>> a, c
(1, 3)
>>> _b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '_b' is not defined
>>> _d
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '_d' is not defined
>>>

但使用import语句可以导入所有变量名:

>>> import unders         # But other importers get every name
>>> unders._b
2

此外,也可以在模块顶层把变量名的字符串列表赋值给变量__all__,以达到类似于_X下划线命名惯例的隐藏效果。例如,有如下模块文件alls.py

# alls.py
__all__ = ['a', '_c'] # __all__ has precedence over _X
a, b, _c, _d = 1, 2, 3, 4

使用from *语句,只有__all__列表中的变量名会被导入:

>>> from alls import * # Load __all__ names only
>>> a, _c
(1, 3)
>>> b
NameError: name 'b' is not defined

但使用import语句可以导入所有变量名:

>>> from alls import a, b, _c, _d # But other importers get every name
>>> a, b, _c, _d
(1, 2, 3, 4)
>>> import alls
>>> alls.a, alls.b, alls._c, alls._d
(1, 2, 3, 4)

就像_X惯例一样,__all__列表只对from *语句这种形式有效,它并不是私有声明。

25.3 启用未来的语言特性:__future__

有些对语言的改变可能会破坏现有代码,这些对语言的改变通常是逐渐被引入Python的。这些改变,最初通常是以可选扩展的方式出现的,默认是未启用的。要启用这类扩展功能,使用特殊的import语句:

from __future__ import FeatureName

要列出你可以导入并启用的这些特性,你可以导入__future__模块,然后使用dir函数来查看。

25.4 混合使用方式:__name____main__

每个模块都有个名为__name__的内置属性,Python会自动设置该属性:

  • 如果文件是以顶层程序文件执行,在启动时,__name__会被设置为字符串__main__
  • 如果文件被导入,__name__就会被设置为客户端已知的模块名。

由此,模块可以检测自己的__name__,来确定它是在执行还是被导入。

实际上,在文件末端的__name__测试中的自我测试程序代码,可能是Python中最常见并且是最简单的单元测试协议。

25.5 实例:Dual Mode Code

第五版,暂略

25.6 修改模块搜索路径

sys.path在程序启动时就会进行初始化,但在那之后你就可以对其进行怎删改。

>>> import sys
>>> sys.path
['', 'c:\\temp', 'C:\\Windows\\system32\\python33.zip', ...more deleted...]
>>> sys.path.append('C:\\sourcedir') # Extend module search path
>>> import string # All imports search the new dir last

注意,sys.path的设置方法只在修改的Python会话或程序(即进程)中生效。

25.7 import语句和from语句的as扩展

importfrom语句都可以扩展,让模块可以在脚本中给予不同的变量名。下面使用的import语句:

import modulename as name # And use name, not modulename

相当于:

import modulename
name = modulename
del modulename # Don't keep original name

from语句也可以这么用:

from modulename import attrname as name # And use name, not attrname

当使用第24章中介绍的包导入功能时,它也可以十分方便地用来为整个目录路径提供简短的变量名来避免变量名冲突:

import dir1.dir2.mod as mod # Only list full path once
mod.func()
from dir1.dir2.mod import func as modfunc # Rename to make unique if needed
modfunc()

25.8 实例:模块即对象

因为模块暴露了它们作为内置属性的大部分有趣特性,所以很容易地编写管理其他程序的程序。这类管理程序叫作 元程序(metaprogram),也叫作 内省(introspection)。

在Python中,有多种方法可以取得模块属性。 1. 通过.点号运算符和属性名来取得M模块的name属性:

M.name # Qualify object by attribute
  1. 通过对模块的属性字典__dict__进行索引取得M模块的name属性:
M.__dict__['name'] # Index namespace dictionary manually
  1. 字典sys.modules中保存了已加载的模块。通过它可以取得特定模块的特定属性。如下,取得M模块的name属性:
sys.modules['M'].name # Index loaded-modules table manually
  1. 内置函数getattr可以让我们以属性名的字符串来取得属性。如下,取得M模块的name属性:
getattr(M, 'name') # Call built-in fetch function

通过这些方法,我们可以编写处理模块对象的程序,来管理其他模块。

25.9 用名称字符串导入模块

我们可以在运行时以一个字符串的形式获取要导入的模块的名称(例如,如果一个用户从一个GUI中选择一个模块名称)。

运行代码字符串

最通用的方法是,把一条导入语句构建为Python代码的一个字符串,并且将其传递给内置函数exec以运行。

>>> modname = 'string'
>>> exec('import ' + modname) # Run a string of code
>>> string # Imported in this namespace
<module 'string' from 'C:\\Python33\\lib\\string.py'>

exec函数(及其近亲eval)编译一个代码字符串,并且将其传递给Python解释器以执行。

直接调用:两个选项

exec唯一的缺点是,每次运行时它必须编译import语句,编译速度可能会很慢。

使用内置函数__import__来从一个名称字符串载入会快很多。

>>> modname = 'string'
>>> string = __import__(modname)
>>> string
<module 'string' from 'C:\\Python33\\lib\\string.py'>

在Python的新版本中,importlib.import_module也可以做同样的事。

>>> import importlib
>>> modname = 'string'
>>> string = importlib.import_module(modname)
>>> string
<module 'string' from 'C:\\Python33\\lib\\string.py'>

25.10 实例:过渡性模块重载(Transitive Module Reloads)

如果一个模块A导入了模块BC,当我们重载模块A时,仅会重载模块A,而在模块A中导入的模块BC是不会自动重新载入的;而只会获取已经载入的模块BC的模块对象(假设它们之前已经被导入)。

模块A.py的内容:

# A.py
print('now in module A')
import B # Not reloaded when A is!
import C # Just an import of an already loaded module: no-ops

模块B.py的内容:

# B.py
print('module B has been imported')

模块C.py的内容:

# C.py
print('module C has been imported')
root@localhost:/tmp# python
Python 3.7.2 (default, Mar 25 2019, 20:38:07)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import A
now in module A
module B has been imported
module C has been imported
>>> import importlib
>>> importlib.reload(A)
now in module A               # 在模块A中被导入的模块B和C没有被重载
<module 'A' from '/tmp/A.py'>
>>>

递归重载器(Recursive Reloader)

要解决这个问题,一种好的办法是编写一个通用工具来自动进行过渡性重载(transitive reload),通过扫描模块的__dict__属性并检查每一项的type来找到需要重新载入的嵌套模块。

模块reloadall.py的内容:

#!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 # from required in 3.X

def status(module):
    print('reloading ' + module.__name__)

def tryreload(module):
    try:
        reload(module) # 3.3 (only?) fails on some
    except:
        print('FAILED: %s' % module)

def transitive_reload(module, visited):
    if not module in visited: # Trap cycles, duplicates
        status(module) # Reload this module
        tryreload(module) # And visit children
        visited[module] = True
        for attrobj in module.__dict__.values(): # For all attrs
            if type(attrobj) == types.ModuleType: # Recur if module
                transitive_reload(attrobj, visited)

def reload_all(*args):
    visited = {} # Main entry point
    for arg in args: # For all passed in
        if type(arg) == types.ModuleType:
            transitive_reload(arg, visited)

def tester(reloader, modname): # Self-test code
    import importlib, sys # Import on tests only
    if len(sys.argv) > 1: 
        modname = sys.argv[1] # command line (or passed)
    module = importlib.import_module(modname) # Import by name string
    reloader(module) # Test passed-in reloader

if __name__ == '__main__':
    tester(reload_all, 'reloadall') # Test: reload myself?

替代编码方案

第五版,暂略

25.11 模块陷阱

Module Name Clashes: Package and Package-Relative Imports

第五版,暂略

顶层代码的语句次序很重要

当模块首次导入(或重载)时,Python会从头到尾执行语句。这里有些和前向引用(forward reference)相关的概念,值得在此强调: - 在导入时,一旦Python运行到模块文件顶层的程序代码(不在函数内),就会立即执行。因此,该语句无法引用文件后面位置赋值的变量名。 - 位于函数主体内的代码直到函数被调用后才会运行。因为函数内的变量名在函数实际执行前都不会解析,通常可以引用文件任意地方的变量。

一般来说,前向引用只对立即执行的顶层代码有影响,函数可以任意引用变量名。以下是示范前向引用的例子:

func1() # Error: "func1" not yet assigned

def func1():
    print(func2()) # OK: "func2" looked up later

func1() # Error: "func2" not yet assigned

def func2():
    return "Hello"

func1() # OK: "func1" and "func2" assigned

当这个文件导入时(或者作为独立程序运行时),Python会从头到尾运行它的语句。

from赋值变量名,而不是连接

from语句其实是在导入者的作用域内对变量名的赋值语句,也就是变量名拷贝运算,而不是变量名的别名机制。它的实现和Python中所有赋值运算相同,但其微妙之处在于,共享对象的代码存在于不同文件中。

例如,假设我们定义了模块nested1.py

# nested1.py
X = 99
def printer(): print(X)

如果我们在另一个模块nested2.py内使用from导入两个变量名,就会得到两个变量名的拷贝,而不是对两个变量名的连接。导入者内修改变量名,只会重设该变量名在本地作用域版本的绑定值,而不是nested1.py中的变量名。

# nested2.py
from nested1 import X, printer # Copy names out
X = 88 # Changes my "X" only!
printer() # nested1's X is still 99

运行模块nested2.py

% python nested2.py
99

然而,如果我们使用import获得了整个模块,然后赋值某个点号运算的变量名,就会修改nested1.py中的变量名。点号运算符把Python定向到了模块对象内的变量名,而不是导入者nested3.py的变量名。

# nested3.py
import nested1 # Get module as a whole
nested1.X = 88 # OK: change nested1's X
nested1.printer()

运行模块nested3.py

% python nested3.py
88

from *语句会让变量语义模糊

使用from module import *形式语句时,因为你不会列出想要的变量,可能会意外覆盖了作用域内已使用的变量名。更糟的是,这将很难确认变量来自何处。如果有一个以上的被导入文件使用了from *形式,就更是如此了。

reload不会影响from导入

重载被导入的模块,对于使用from导入的模块的变量名没有影响。也就是说,客户端的变量名依然引用了通过from获得的原始对象,即使之后原始模块中的变量名进行了重新设置:

from module import X # X may not reflect any module reloads!
. . .
from imp import reload
reload(module) # Changes module, but not my names

为了保证重载更有效,可以使用import以及点号运算,来取代from。因为点号运算总是会回到模块,这样就会找到模块重载后变量名的新的绑定值。

import module # Get module, not names
. . .
from imp import reload
reload(module) # Changes module in place
module.X # Get current X: reflects module reloads

reloadfrom以及交互模式测试

不要将reloadfrom结合起来使用!

递归形式的from导入无法工作

不要在递归导入中使用from。如果这么做,Python不会卡在死循环中,但是,程序又会依赖于模块中语句的顺序。

有两种方式可以避开这个陷阱:

  • 小心设计:最大化模块的聚合性,同时最小化模块间的耦合性。
  • 如果无法完全断开循环,就要使用import和点号运算(而不是from),将模块变量名的读取放在后边,要么就是在函数中,或者在文件末尾附近去执行from(而不是在模块顶层),以延迟其执行。