2014年4月

Win8 下安装 Ubuntu 虚拟机

之前想在自带 Win8 系统的笔记本上安装 Win7,折腾许久,还没有成功。

想必装 Ubuntu 也一样。因此打算装个虚拟机,VMware 是老牌虚拟机软件,但过于厚重。所以我选择了 VirtualBox,它是一款开源虚拟机软件,更加轻量,最新安装包只有100M。目前属于 Oracle 旗下软件。

可以前往官网下载最新安装包

VirtualBox 的主界面非常简洁,易于使用。事先下载需要安装系统的iso文件,在软件上“新建”一个虚拟机即开始安装。

准备工作

  1. VirtualBox (笔者使用的是 VirtualBox 4.3.10 for Windows hosts )
  2. 待安装的操作系统 Ubuntu 的光盘镜像文件 (笔者使用的是 ubuntu-12.04.4-desktop-i386.iso )

笔者所使用的计算机环境:ThinkPad X230i,自带 Win8 操作系统(并且已升级到 8.1),系统显示是 64 位操作系统,但 VirtualBox 提示只能安装 32 位的系统。(不知道什么原因,知道的朋友,不妨告诉我。多谢!)

开始安装

安装非常简单,首先新建一个虚拟机,可以按默认设置一路下去。

然后启动刚刚建好的虚拟机,会提示指定光盘,选择下载好的 Ubuntu 镜像文件,即进入操作系统的安装界面。类似于从光驱安装,根据提示,自己选择就行。不过安装时间,可能会比较长。因为安装过程中会通过网络下载相关的文件,如语言包等。到安装成功,大约需要半小时。英文不好的同学,建议选择汉语,便于理解。

遇到的问题

1.屏幕分辨率无法调整,最大只有 1024*768,两边还空出很多屏幕。另外就是,鼠标滚轮无效。

解决办法:在运行的虚拟机界面,点击左上角菜单项“设备” > “安装增强功能”。它会将 VirtualBox 目录下的增强功能包载入到光驱,然后自动执行安装。安装完毕后,这两个问题解决。

2.启动时提示:

SMBus base address uninitialized - upgrade BIOS or use force_addr=0xaddr

但仍然可以启动,没有太大影响。

Ubuntu 社区给的答案是:

This error is caused by VM having no smbus but Ubuntu always trying to load the module. It doesn't affect anything but is a bit annoying.

(1) Check module is being loaded / 检查模块是否已经加载(会看到已经加载)

lsmod | grep i2c_piix4

(2) If so, blacklist it in the file /etc/modprobe.d/blacklist.conf, by adding the following to the end of the file: / 如果已经加载,将它加入 blacklist 列表。方法是修改这个文件 /etc/modprobe.d/blacklist.conf ,将下面的配置写到该文件末尾。

blacklist i2c_piix4

(3) Update the initramfs

sudo update-initramfs -u -k all

You might want to optionally remove unneeded kernal images before updating the initramfs to cut down on how long that part takes.

笔者按此操作,可以解决。

其它

1.Ubuntu虚拟机在笔者的电脑上运行有点慢,主要体现在图形界面显示迟缓。在 VirtualBox 的虚拟机设置中加大内存,会有好转。想把 Ubuntu 作为日常使用的系统,独立安装应该会更好。

2.更新软件后,可能又会出现屏幕分辨率无法适配。再次运行“安装增强功能”即可。

3.在安装时,并没有要求设置 root 密码。如果想使用 root 帐号,可以重设其密码:

sudo passwd root

Ubuntu 常用命令 apt-get

问题

对于新手,通过 apt-get install 安装软件,很可能遇到这样的问题:

E: Unable to locate package xxx

这是因为无法找到相应的包。

分析:很可能是软件源的问题,要么源有问题,要么更换了源,没有更新(apt-get update)。

如过源有问题,可以更换源。国内推荐选择163的源,如:

deb http://mirrors.163.com/ubuntu/ trusty main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ trusty-security main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ trusty-updates main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ trusty-proposed main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ trusty-backports main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ trusty main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ trusty-security main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ trusty-updates main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ trusty-proposed main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ trusty-backports main restricted universe multiverse

具体请参考Ubuntu镜像使用帮助

命令解读

与 apt-get 相关的目录和文件:

/etc/apt/sources.list
该文件存放软件源站点,当执行apt-get install xxx时,Ubuntu 就去这些站点下载软件包到本地并执行安装。需要更换源站点可以参考源列表。注意:更换源站点,必须执行更新命令apt-get update才能生效。

/var/lib/dpkg/available
该文件的内容是软件包的描述信息, 其中包括当前系统中已安装的和未安装的软件包。

/var/cache/apt/archives/
该目录是使用 apt-get install 安装软件时,软件包的临时存放路径。

/var/lib/apt/lists/
使用 apt-get update 命令会从 /etc/apt/sources.list 中下载软件列表,并保存到该目录。

Ubuntu英文版无法正常显示中文,如何安装中文字体?

装了英文版 Ubuntu,结果中文显示异常,如下图示
中文界面字体异常

如何解决呢

查看 Ubuntu 字体文件夹(/usr/share/fonts/truetype/),其中没有中文字体。而一般网站默认字体是宋体,那么我们需要下载宋体,并把它复制到这个字体文件夹即可。

本文提供的字体,下载后解压缩,然后复制到字体文件夹,刷新页面即可正常显示中文。如果需要更多中文字体,可以去下载并复制到这个目录。也可以直接从 Windows 系统的字体文件夹(C:\Windows\Fonts\)复制过来使用。

Python 模块的工作原理[译]

作者: Tomasz Lisowski 翻译: Eric Ling

译注

这是一篇老文,作者至少写于 5 年前,但它的内容仍然是“新鲜”的。可以通过阅读本文,深入理解 Python 模块的工作原理。

1. 前言

当我们写更大的 Python 程序时,变量名、函数名、类名的数量增长如此巨大,因此很有必要将他们组织成类别或小组,这通常被称为命名空间。下面的语言结构提供了这种便利:

  • classes (类)
  • modules (模块)
  • packages (包)

本文中我们不打算讨论"类",因为它值得单独讨论。然而,我们将试着阐述 Python 模块和包的功能,并特别关注模块的导入机制。我希望这能帮助 Python 编程人员更好地理解那些有用的包,比如 wxPython 或者 NumPy。

这篇文章受启发于船舶设计软件 Tribon M3 用户的提问。这个软件是 AVEVA Solutions Ltd 公司开发的,它内嵌了一个包含超过 600 个专门函数的 Python 2.3 解释器,作为自定义工具。当然,这里讨论的功能,通常是 Python 语言的特性,不但对 Python 2.3 有效,而且对新的发行版 2.4 和 2.5 也有效。

2. 模块

2.1 模块是什么

简单来讲,模块就是变量、函数以及类的定义的集合。他们也可包含直接执行的代码,我们将在后面讨论这个特性。通常,我们可以区分出 3 种类型的模块:

  • Built-in modules (内建模块)

他们总是可访问的,因为他们已经创建到 Python 解释器里了。例如:sys, math, time。

内建模块的名字被保存在一个名叫 sys.builtin_module_names 的列表中。下面的代码将打印出当前运行的 Python 解释器中所有的内建模块的名字。

import sys
for name in sys.builtin_module_names:
    print name
  • 用 Python 编写的标准模块

这里我们需要模块的源代码(它们是用 Python 写的),或者至少一个特殊的二进制版本文件(以 .pyc 结尾),它包含了模块预编译的字节码。为了在程序中可用,这个文件必须位于定义在 sys.path 中的某个文件夹。例如:os, re, csv, types。

  • 用其它语言(例如 C)编写的模块,而且已经编译成一个 DLL

真正编译语言的使用可以提高只有 Python 的解决方案的性能,而且可以访问更低层次的 API。为了让 DLL 成为 Python 的一个可用模块,代码必须遵循一定的规范,手册 Extending and Embedding the Python Interpreter 详细地列出了这些规范。这个手册是 Python 语言的作者 Guido van Rossum 写的。

习惯上,编译的 DLL 文件以 .pyd 结尾,而不是 .dll。跟标准模块一样,它也必须位于定义在 sys.path 中的某个文件夹。例如:_tkinter, _sre。

2.2 怎么使用模块

想使用模块中定义的类和方法,必须先导入模块。这可以通过 import 语句来完成,使用以下任意一种语法:

  • import moduleName
  • import moduleName as alternateName
  • from moduleName import *
  • from moduleName import id1, id2, …
  • from moduleName import id1 as altName1, id2 as altName2, …

通常,模块导入的流程如下:

  1. 首先,Python 解释器在字典 sys.modules 中寻找这个模块名,这个字典中包含了解释器当前已经加载和初始化的所有模块。如果找到了这个模块,系统将跳过第 2 步,直接前往第 3 步。
  2. 如果模块还没有加载,系统首先搜寻内建模块列表,如果也没有找到,则继续到 sys.path 所包含的目录中寻找。如果找到,则加载它并初始化。最后,如果加载和初始化都没有出错,那么这个模块将被注册到 sys.modules 字典中。如果没有找到,将抛出 ImportError 异常。
    注意:如果一个模块已经存在于 sys.modules 字典中,随后的所有导入这个模块的 import 语句,不会再初始化这个模块。
  3. 到这一步,Python 解释器将怎样运行取决于刚刚使用的 import 语句的形式。
A. import moduleName

模块的名字被注入到本地命名空间(the local namespace, 可理解为当前的 Python 运行环境)。模块中所有定义的标识符,需要通过模块名字修饰的前缀形式来访问。(参见2.7)

例如:

import myModule
res = myModule.doIt(arg)
B. import moduleName as alternateName

模块的别名被注入到本地命名空间。模块中定义的所有标识符,需要通过模块别名修饰的前缀形式来访问。(参见2.7)

例如:

import sys
# Define the module 'utils' by importing either
# winUtils or linuxUtils depending on the platform
if sys.platform == 'win32':
    import winUtils as utils
else:
    import linuxUtils as utils
...
# platform-independent module access
user = utils.getUser()
C. from moduleName import *

这种情况,模块的名字没有被注入到本地命名空间,但是系统会搜索这个已经加载的模块的名字空间,并把模块中所有的公共标识符的名字注入到本地命名空间。

Python 是如何确定哪些标识符是公共的,哪些不是呢?你将在2.3小节找到答案。

D. from moduleName import id1, id2,… or from moduleName import id1 as altName1, id2 as altName1, …

这种情况下,模块的名字也没有被注入到本地命名空间,但是系统会搜索这个已经加载的模块的名字空间,把列出来的标识符的名字(或者别名)注入到本地命名空间。

当然,如果列出来的名字中有不存在的,将抛出一个 ImportError 异常。

注意:在使用 C 或者 D 类型的语法时,存在对已有标识符重新绑定值的风险(译注:模块中的标识符覆盖了本地命名空间中相同的标识符),导致无法访问原标识符的值。

例如:

doIt = True
...
from myModule import *
...
print "doIt =", doIt

上面的代码简单地认为,变量 doIt 仍然是第 1 行定义的布尔值。不过,如果模块 myModule 中定义了一个相同的公共变量,我们就有麻烦了。此时,我们的布尔变量被模块中导入的同名标识符重定义(或者叫覆盖)了。当我们再试着打印出原来的值,会发现变量 doIt 已经不是布尔值,而是一个函数或者字符串,因为我们已经失去了对原值的访问能力(它很可能已经被垃圾回收机制回收了)。

当然,如果我们使用另外一种导入语法,就不会发生这种情况。

doIt = True
...
import myModule
...
print "doIt =", doIt

这里的变量 doIt 仍然是第 1 行定义的布尔值。doItmyModule.doIt 是两个截然不同的对象。

当然,在模块中采用好的命名规范,同样能降低这种名字冲突带来的风险。

2.3 公共标识符

在前面的章节,我们提到过如下语句:

from myModule import *

它将从模块中导入所有的公共标识符。那么 Python 怎么辨别出哪些是公共的标识符,哪些不是呢?

首先,Python 会检查导入的模块中是否已经定义了全局变量 __all__ 。如果定义了,那么这个变量是一个以字符串为基本元素的序列,这些字符串所代表的变量,被认为是公共的标识符。这个特性可以阻止某些变量的导入,尤其是那些私有性质的标识符。

设想有一个大型的模块,包含了许多函数,但是只有一个主函数被其它的模块调用。而其它的函数仅作为辅助函数,只在这个大型模块内部被调用。这种情况下非常适合使用 __all__ 变量。例如:

模块:myModule.py

def fun1():   # Private function
    ... # Code
...
def main():   # Main (public) function
    ... # Code
__all__ = ['main']

现在我们尝试执行下面的代码:

from myModule import *
res = main()

没有任何问题。我们再来执行这个代码:

res = fun1()

呃,抛出了 NameError 异常。这是怎么回事呢?

尽管函数 fun1main 都存在于被导入的模块,但只有其中一个可以被访问。抛出这个异常是因为 'fun1' 没有出现在变量 __all__ 所定义的列表中。这就导致 import 语句忽略了这个函数。

如果被导入的模块中不存在 __all__ 变量,所有的变量都被认为是公共的,除了以下划线(_)开头的标识符。

译注:如果使用 from import 方法指定某个标识符,即使它不在__all__变量中,甚至以下划线开头的标识符,也会被导入到本地命名空间。

2.4 模块初始化

Python 初始化一个模块,要么执行一个专门的初始化函数(对于非 Python 语言编写的模块),要么运行模块的主体(对于 Python 语言编写的模块)。对于后者,会解析模块中定义的内容,并立即执行可执行的代码。

正如2.2节中所讲的,模块只会被初始化一次,而且是模块不存在于 sys.modules 字典中时。这种行为,对于包含直接可执行代码的模块产生了重大的影响。这些代码只会在模块初始化的时候执行,而且只是一次,无论模块被当前的运行环境导入(import)了多少次。

因此,把直接可执行的代码放在模块中(并且模块在程序中被导入多次),这不是一个好的设计习惯。我们不能简单地认为在每次导入模块时,这些代码都会运行一次。相反,我们应该把这些代码放到一个函数中(如 run()),并且使用下面的方式:

import myModule
myModule.run()

明确地调用函数 run()

然而在某些情况下,我们需要写这样一个模块,有时候被导入,有时候作为主程序直接执行。这对于模块开发时尤其有用,我们可运行模块来做单元测试。

遵循前面的建议,我们应该将测试代码(也就是所讲的可直接执行的代码)隐藏到一个函数中(如 runTest())。在模块直接运行时,这个函数被执行;被导入时则不执行。为了达到这个目的,我们需要一种方法来有条件地执行这个函数,能检测出模块是直接运行的还是被导入的。我们可以通过在模块底部增加下面的代码来达到这个目的:

if __name__ == '__main__':
    runTest()

如果模块被导入,则特殊的字符串变量 __name__ 被赋值为模块的名字;如果模块直接运行,则为 __main__ 。因此,上面的 if 语句只在模块直接运行时,才执行 runTest() 函数,被导入时不会执行。

为了越过模块只被初始化一次的限制,Python 语言提供了 reload() 函数。

reload(module_name)

这会使模块再次初始化,并且返回这个模块对象作为函数结果。

2.5 .py, .pyc, and .pyo 后缀文件

如果一个模块是用 Python 写的,它的源代码被存储在以 .py 结尾的文件中(例如 myModule.py 就是模块 myModule 的源代码文件)。

模块无论何时被成功编译,都会尝试把编译的内容写到以 .pyc 结尾的同名文件中(例如 myModule.pyc)(译注:通常它被称为字节码文件,和其它的编译语言有所不同)。如果编译失败,也不会是一个错误;比如由于某种原因,文件是不完整的,则结果文件 myModule.pyc 被认为是无效的,并且会忽略掉。

字节码文件(.pyc)的内容是平台独立的,一个 Python 模块的目录可以被不同架构的机器共享。

如果 Python 解释器调用时使用了 -O 参数(或者 -OO),Python 将会优化编译后的字节码文件。不再生成 .pyc 文件,而是产生一个以 .pyo 结尾的字节码文件。它和 .pyc 文件使用上是一样的,不同点在于 .pyc 文件是未经优化的。而 .pyo 文件是 Python 解释器在优化模式下工作的。

编译后的字节码文件(.pyc 或者 .pyo 文件)中存储了对应的模块文件(.py 文件)的修改时间。在以下情况,Python 会加载编译后的字节码文件(不会重新编译模块):

  1. 如果模块文件(.py)不存在。
  2. 如果模块文件存在,并且它的修改时间和字节码文件中保存的时间一致。

否则,Python 会忽略字节码文件,重新编译源代码文件。这是自动进行的,你不用考虑去更新你的字节码文件,Python 会替你完成这些。

有时在开发 Python 程序时,你可能已经注意到,Python 好像没看到你对模块的修改,除非你退出并重启开发环境。这可以通过下面几步来解释:

  1. 第一次运行程序时(例如对某部分进行测试),Python 会根据 import 指向的模块解析这个模块。因为它之前没有被导入(在当前 Python 解释器会话中),它没有被列入 sys.modules 字典中。因此,Python 加载并解析它,创建或者更新字节码文件(.pyc),并且把编译后的模块对象存储到 sys.modules 字典中。
  2. 然后你对模块源代码进行了修改,保存文件,然后再次运行测试代码。噢,有点不对劲!测试代码运行得跟之前一样,就好像你没有对模块进行修改一样。
  3. 这是因为之前,Python 根据 import 指向的模块,对模块源代码进行了解析。然而与第1步相反的是,现在我们的模块对象已经存在 sys.modules 字典中。因此 Python 不会再尝试加载和解析模块——它只会从 sys.modules 字典中读取模块对象。所以它不是模块的最新版本,它不包含第2步所做的修改。

幸运的是,大多数开发环境提供了一些工具,来重新加载已经加载的模块。(例如一些 '重新加载模块' 的按钮。)如果没有,你总是可以使用临时的语句 reload(moduleName) ,在 import 模块之后,重新加载模块。

2.6 sys.path 变量

变量 sys.path 定义了一个文件列表,将在这些文件夹中搜寻要导入的模块。它是在解释器初始化时创建的,也可以在运行时对它进行更改。重要的是,我们需要知道这个列表是如何创建的,以及怎样将新的路径添加到这个列表,使得我们的脚本能够找到它们的模块。

首先,这个变量通过视窗环境变量(the Windows environment variable) PYTHONPATH 来初始化。Python 解释器和其它使用 Python 解释器的软件系统(例如 Tribon M3),可以定义和更新这个变量,设置它为一个用分号分隔的路径列表。

然后一个特殊的 site 模块被导入。注意,这里不需要显式地导入 import site —— Python 会为你完成这一步。这是一个标准的 Python 模块,用户可以通过它来自定义环境,例如添加新文件夹到 sys.path 列表。默认地,(如果你安装了一个独立的 Python 解释器),它会添加一些标准的文件夹,比如'/Python23/lib/site-packages'。

此外,site 模块会从 sys.path 列表中的文件夹中搜寻 *.pth 文件(路径配置文件,path configuration files)。如果找到了,就读取这些文件,并且将定义在这些文件中的路径自动地添加到 sys.path 列表。这些文件被用于某些 Python 包,例如 wxPython,它定义了 wxPython 包中的模块的位置。

更多的系统自定义可以放到一个可选的 sitecustomize 模块,这是 site 模块尝试导入的模块。这样我们就可以保持 site 模块不变,将所有的自定义内容写入 sitecustomise 模块。

最后,模块搜寻路径(sys.path)也可以在运行时自定义。例如:

path = 'E:\\PRIVATE\\MODULES'
import sys
if path not in sys.path:
    sys.path.append(path)
import my_test_module

这里,模块文件 my_test_module.py 位于文件夹 E:PRIVATEMODULES 中。

在上面的例子中,用户定义的文件夹放在 sys.path 列表的末尾。如果你想把它放在列表的开关,可以通过替换以下语句来完成:

sys.path.append(path)

替换为

sys.path.insert(0, path)

为什么这个很重要?我们假定 my_test_module 模块除了在文件夹 E:PRIVATEMODULES 中,还在 sys.path 列表中的其它文件中也存在。它可能是同样的模块,或者老版本的模块,甚至一个根本不相关的模块,只是碰巧它俩拥有同样的名字。无论什么原因导致了同名模块,处理的规则很简单:

按照 sys.path 列表定义的文件夹顺序,以先找到的文件为准。

因此,你可能导入了一个不同的模块,它不是你希望导入的,同时没有产生任何警告。

当然,对模块使用良好的命名规范,可以减少这种产生歧义的风险。

2.7 访问被导入的标识符

正如 2.2 章节中所讨论的,标识符的访问方式,取决于模块的导入方式。也取决于被导入的标识符所在的命名空间。

我们来分析下面两种导入语句:

  1. import math -> math.sin(…)
  2. from math import sin -> sin(…)

第一种情况,模块名字本身被注册到当前的命名空间,但是函数名 sin 是在模块的命名空间。因此,这里我们需要用限定的名字来访问 math.sin。

第二种情况,标识符 sin 本身被注册到当前的命名空间,因此可以直接使用这个名字。这里不能使用限定的名字 math.sin 来访问这个函数,因为模块的名字(math)在当前的命名空间中不存在。

总的来说,from 版本的 import 语句让我们可以使用简短的标识符,可能也提高了程序性能,但是导入的许多新的标识符会使当前的命名空间很乱。使用下面的语句,发生这种负面影响更严重:

from moduleName import *

这真的会使许多标识符被导入到当前的命名空间。

2.8 命名空间和名字解析(the name resolution)

为了理解 Python 是如何解析(限定的或者没有限定的)标识符名字到变量、函数等的内存地址,我们需要知道它是如何搜索可用的命名空间。通常有三个命名空间,会按照下面的顺序依次搜索:

  1. 局部命名空间(Local namespace)
  2. 全局(模块的)命名空间(Global (module's) namespace)
  3. 内建命名空间(Built-in namespace)

局部命名空间所包含的名字只作用于当前的范围(例如一个函数中的局部变量)。全局或者模块的命名空间所包含的名字作用于当前的模块(这里,主程序也被认为是一个模块)。在这里,你可找到所有全局的变量、函数、类等等。最后,内建命名空间包含的名字被定义在 __builtin__ 模块中,它们总是可访问的。它包含了这些名字,例如:

  • 所有异常的名字(如 ArithmeticError, KeyError, ValueError 等等)
  • 内建函数的名字(如 ord, abs, int, str, range 等等)

理解名字解析搜索命名空间的顺序,有助于我们写出更高效的程序。例如:

def codes(name):
    codeList = []
    for c in name:
        codeList.append(ord(c))
    return codeList

上面的函数通过字符串 name 中的字符生成一个 ASCII 码列表。如果这是一个很长的字符串,优化函数中的循环或许是值得的。让我们来考虑循环中需要解析的名字:

  • codeList.append
  • ord

codeList.append 是一个限定的名字,需要两次搜索。首先,Python 需要查找名字 codeList ,恰好它定义在局部命名空间中。然后它被找到了,它是一个列表,继续去列表对象的定义寻找标识符 append。这两次搜索每次都发生在 for 循环中。我们可给限定的名字 codeList.append 定义一个局部的别名函数,把它用到循环中,来降低程序开销。

下一个需要优化的是 ord 函数。它是一个内建函数,因此 Python 不得不无效地搜索局部命名空间和全局命名空间。(译注:即不得不在局部命名空间和全局命名空间没有找到后,才能在内建命名空间中找到它。)通过定义一个局部的别名,我们可以让 Python 在第一次搜索便找到它,即在局部命名空间中找到它。

归纳起来,优化后的代码如下:

def codes(name):

codeList = []
locAppend = codeList.append  # 注意:没有圆括号
locOrd = ord  # 注意:还是没有圆括号
for c in name:
    locAppend(locOrd(c))
return codeList

当然,这还不是最高效的版本。还有优化的空间,但它已经超出了我们探讨的主题“命名空间和名字解析”。所以,我把寻找更好的方法作为练习留给读者。

如果我们理解了名字解析的基本原理,那就明白 Python 是如何处理限定的名字了。例如,标识符 csv.DictWriter.writerow 指的是 DictWriter 类的 writerow(…) 方法,并且这个类定义在 csv 模块中。让我们来分析一下下面的代码:

def fun():
    import csv
    method = csv.DictWriter.writerow

函数 fun() 是这样执行的,成功导入模块 csv 后,Python 将 csv 作为函数 fun() 局部命名空间中的标识符。

执行下一个语句,Python 进行了如下活动:

  1. 它在局部命名空间中寻找 csv 标识符。找到后,Python 不会再进一步到外层的命名空间中寻找(全局命名空间和内建的命名空间)。
  2. Python 发现 csv 标识符是一个模块。因此 Python 在字典 sys.modules 中找到它的定义。(译者注:Python 加载成功的模块,都会把它们加入到这个字典。)
  3. 然后 Python 到这个模块的名字空间中寻找 DictWriter 标识符,并且找到了。
  4. Python 发现 DictWriter 标识符是一个类,然后把这个定义加载到内存中。
  5. 然后到 DictWriter 类的名字空间中去寻找 writerow 标识符,并且找到了。
  6. Python 发现 writerow 标识符是 DictWriter 类的一个方法,并且定位到了这个代码块在内存中的地址。
  7. 总结起来,Python 把限定的名字 csv.DictWriter.writerow 解析为 writerow 方法在内存中的地址。然后把它绑定到了局部名字 method ,以便更快地访问它。

最后,我们必须理解 Python 是如何处理赋值的,是如何限定新旧标识符的值。默认地,Python 将值绑定到局部命名空间的变量,并且很可能隐藏在其它命名空间中的同名对象。例如:

abs = 5

执行上面的赋值语句后,我们失去了对内建函数 abs() 的访问能力。只要新创建的变量 abs 存在于当前的区域,内建函数 abs() 就是隐藏的。我们可以通过删除这个变量来恢复原状。这样就可以使 abs() 恢复访问。

del abs
x = abs(-3)

另外一种方法是在隐藏内建函数之前给它定义一个别名:

locAbs = abs
abs = 5
x = locAbs(-3)

当然,最好的办法是避免这种含糊的语句。

也有可能,赋值应该指向全局命名空间中的变量,而不是局部变量。例如:

lastX = None
def fun(x):
    global lastX
    lastX = x
    return 2*x

在上面的代码中,赋值语句 lastX = x 并没有创建一个局部变量 lastX,而是更新了全局变量——多亏了 global 声明语句。

2.9 __import__ 函数

到目前为止,我们所讨论的 import 语句提供了静态的导入功能。导入的模块名硬编码在你的程序中。Python 语言还提供了一个可以动态导入模块的函数,被导入的模块名事先是不知道的。例如:

def listGlobals(modName):
    module = __import__(modName)
    return dir(module)

上面的函数返回被导入模块的全局的标识符,模块的名字就是传入函数的参数。__import__() 函数 被 import 语句内部调用。它导入给定的模块,并返回模块对象,然后可以像使用模块名一样使用这个返回的对象。例如,我们可以这样写:

res = module.run()

使用 __import__() 函数返回的变量。

__import__() 函数还有额外的可选的参数:

module = __import__(modName, globalDict, localDict, fromList)

字典 globalDict 包含全局的标识符(在这里你可以使用 globals() 函数给它赋值),字典 localDict 代表局部的标识符(可以通过 locals() 函数赋值)。参数 fromList 用于限定导入的标识符,像这样的语法 from modName import id1, id2, …

有时候我们可以通过继承来扩展内建的 __import__() 函数,以支持某些特殊的导入情形。如需编写自己的导入函数,imp 模块是非常有用的。

2.10 内嵌函数

这个话题跟模块没有明显的关系,但它跟之前讨论的命名空间和名字解析关系很大。Python 允许在函数内部定义函数。例如:

def fun(x):
    status = 0
    def step1(a):
        if status == 0:
            ...
    def step2(a):
        if status == 0:
            ...

    step1(x)
    step2(x)

这里参数 x 正常地传递到内嵌函数。但是请注意,内嵌函数也访问了其外层空间的变量(status),这个相对的外层空间指的是函数 fun() 的作用域。这允许一些变量通过参数传递给内部函数。

但这也不是 100% 正确的。你可稳妥地读取这些变量,但如果你试图在内嵌函数中修改这些变量,你将创建一个同名的局部变量,仅在这个内嵌函数内有效,从而屏蔽了外层名字空间的变量。

总的来说,内嵌函数可以帮助你隐藏私有函数。它非常有用,但仅适用于简短的函数,并且不能修改外层空间中的局部变量。

这会促使你使用这种方法来减少一个模块的外部可见函数,但这显然会让查找错误更加困难,并且降低代码的可读性。因此,我建议你保守地使用这个特性。

2.11 我的模块可用吗?

有时候,我们写的代码,假定给定的模块在目标系统中是可用的。也有可能,这个模块丢失了,我们需要做一个替换。这就需要检测的能力,确定模块导入是否成功:

try:
    import myModule
    myModuleOK = True
except ImportError:
    myModuleOK = False
    import alternativeModule as myModule

布尔变量 myModuleOK 会告诉你,模块 myModule 是否被导入了。此外,你可能需要导入一个替代的模块,提供类似的功能。

多亏有了 as 语句,剩下的代码可以假定 myModule 是存在的——并不需要知道,它提供了一个替代原版本的模块。

注意,我们必须捕获 ImportError 异常,其它的异常无法表明这是一个导入错误。

3. 包

通常,包是模块的分级结构。因此,我们所讲的关于模块的大部分内容,也适用于包。在这个章节,我们聚焦在针对包的特性。

使用包的时候,模块的名字由点号拼接成了新的名字。例如 win32com.client (来自 pywin32 模块,针对 Windows 的扩展包)。

3.1 包的目录结构

我们通过 pywin32 包的例子来分析包的目录结构。安装后,在这个目录 /Python23/Lib/site-packages ,你会发现一个 pywin32.pth 文件,它会给 sys.path 列表增加下列目录:

  • /Python23/Lib/site-packages/win32
  • /Python23/Lib/site-packages/win32/Lib
  • /Python23/Lib/site-packages/pythonwin

这是标准的 Windows 目录,它包含了包提供的模块。但这不是所有的。在 site-packages 目录中,可以发现更多来自 pywin32 包的目录,但并没有列在 pywin32.pth 文件中。其中一个目录是 /Python23/Lib/site-packages/win32com 。

这个目录有什么特别呢?你会发现它包含一个 __init__.py 文件,作为 win32com 包的初始化。这个目录还包含一些其它的 Python 源文件(例如 util.py),也包含子目录。其中一个是 'client',有趣的是,它也包含一个 __init__.py 文件。

它表明,import 语句把子目录包含的 __init__.py 文件当作模块。因此下面的语句可以很好地执行:

  • import win32com – 它会加载文件 win32com/__init__.py, 提供名字 'util' 和 'client' 作为内部名字 (通过 'win32com.util', 和 'win32com.client' 访问)
  • from win32com import util – 它会加载文件 win32com/__init__.py, 提供名字 'util' 作为局部命名空间中的标识符

这个规则是递归的,如果导入的元素也是一个包含 __init__.py 文件的子目录。

  • import win32com.client - 加载文件 win32com/client/__init__.py,提供名字例如 'Dispatch' 作为内部名字(win32com.client.Dispatch 是一个用于生成 COM 对象实例的常用函数。)
  • from win32com.client import Dispatch - 加载文件 win32com/client/__init__.py ,提供名字 'Dispatch' 作为局部命名空间中的标识符

文件 __init__.py 必须存在于子目录中,子目录被称为子包,这个文件可以为空。

仔细一点,你会发现函数 Dispatch 定义在子目录 'client' 中的 __init__.py 文件中。 但是除了这个文件,子目录 'client' 还包含了其它 Python 文件。它们都是 win32com.client 子包的模块。总结起来,要使用 win32com.client.Dispatch() 函数,我们需要首先执行下面的 import 语句:

  • import win32com.client -> obj = win32com.client.Dispatch(…)
  • from win32com.client import Dispatch -> obj = Dispatch(…)
  • from win32com import client -> obj = client.Dispatch(…)

正如你从上的例子中看到的,语句 from package import item 可以导入子包、子模块,以及定义在包中的其它标识符。

3.2 from package import *

Python 解释器无法通过自身找到包或者子包的子模块。这里我们需要提供一个 __all__ 变量(也可参考2.3小节),是由公开的可用的子模块名字组成的序列。在特殊的包中,需要在 __init__.py 文件中定义这个变量。

例如:

假如我们把下面的语句加在 'client' 目录的 __init__.py 文件中。

__all__ = ['build', 'util']

然后,下面的语句:

from win32com.client import *

将只会导入下面的子模块到当前的命名空间:

  • build
  • util

尽管 'client' 目录还包含了其它模块。

如果 __all__ 变量没有定义,import 语句不会导入包的所有子模块。

如果只是确认,包已经被成功加载,只有下面的标识符会被导入到当前的命名空间:

  • 定义在包的 __init__.py 文件中是名字
  • 显式地被包的 __init__.py 文件加载的子模块
  • 被之前的 import 语句加载的子模块

3.3 内部包引用

子模块经常需要引用包中的其它子模块。如果其它子模块被定义在同一个子包中,这样我们可以使用简单的引用,不再需要前缀名字。

如果有一个 'test' 模块定义在 win32com.client 子包中,就可以通过下列简单的方式来引用:

import util

而不需要使用限定的名字:

import win32com.client.util

它可以正常工作,因为此时的模块搜索路径已经变为包含当前子包,并且将当前子包作为第一搜索路径。

如果引用其它子包中的模块,就必须使用完整的限定的名字。

这里仍然考虑假想的子模块 'test',它属于 win32com.client 子包。我们希望从 win32com.server 子包中导入 'policy' ,就必须使用下面的语句:

import win32com.server.policy

3.4 关于 wxPython 的一点说明

关于 wxPython 的一个常见问题是它的双重命名规范。你可以使用其中任何一种,尽管旧风格已经不赞成使用。

旧风格(变种一):

from wxPython import wx
class MyFrame(wx.wxFrame):
    ...

旧风格(变种二):

from wxPython.wx import *
class MyFrame(wxFrame):
    ...

变种一需要写2次 'wx' 前缀,非常烦人。变量二则具有很大可能导致名字冲突,因此 'wx' 前缀强加在所有 wx 模块的所有名字前。这种风格不应该再使用。

新风格像下面展示的这样:

import wx
class MyFrame(wx.Frame):
    ...

这里你导入的不是 wxPython 模块,而是 wx 模块。在新的 wx 模块中,所有的标识符都丢弃了 'wx' 前缀,这大大地提高了代码的可读性。那它是怎样工作的呢?

在 Python23/Lib/site-packages/wx 目录中,我们可以看到 __init__.py 文件,这表明 wx 是一个 Python 包。进一步研究这个文件发现,wx 包是如何将原来的 wxPython 包中的标识重命名的。

它定义了一个 _rename() 函数,通过一定的规则,将名字字典转换成对应的丢弃了前缀的字典。在 wxPython.wx 模块本身的命名空间,也调用了这个函数进行标识符名字转换,并把新的名字存储到 globals() 函数引用的字典中,那么调用新的模块 wx 就自然地展示了新的名字。

这是一个低层次处理模块的很好例子,也证明了 Python 语言是如此的灵活,做了这么一种外部不可见的特殊转换。

使用上面所讨论的新风格,我们要记住首先使用 import wx 语句从 wxPython 包导入 wx 。然后,可以进行其它导入,例如:

from wx import html

如果你没有首先导入 wx ,其它的导入语句将不会进行适当的名字转换,会产生问题。

4 最后的话

我真心希望这篇文章能够帮助 Python 开发者使用 Python 的全部潜能。我也会对任何错误和遗漏负全责。事实上,如果有反馈意见,我将感激不尽,这将能帮我完善这篇文章。

非常感谢 MBM Project Tribon forum 的成员与我的讨论,以及向我提的问题,还有 Tribon Vitesse trainings 的参与者。

英文原文