史上最全Python学习笔记(基于《Python学习手册(第4版)》)——Part5 模块部分

文章目录

第21章 模块:宏伟蓝图

sys.path列表

可以通过以下代码查看模块搜索路径在当前机器上的实际配置:

import sys
sys.path

通过修改这个列表,可以修改将来的导入的搜索路径。

高级的模块选择概念

导入钩子(import hook)可以让导入做各种有用的事情,例如,从归档中加载文件、执行解密等。

Python也支持最佳化字节码文件.pyo,这种文件在创建和执行时要加上-O这个Python标志位,因为这些文件执行时会比普通的.pyc文件快一点(5%左右),然而它们并没有被频繁地使用。

第22章 模块代码编写基础

模块的创建

如果打算将其导入,则模块文件名就应该以.py结尾。

当一个模块被导入时,Python会把内部模块名映射到外部文件名,也就是通过把模块搜索路径中的目录路径加在前边,而.py或其它后缀名添加在后边。

C或C++这类外部语言编写的代码也可以来创建Python模块,这类模块成为扩展模块,一般都是在Python脚本中作为包含外部扩展库来使用的。

模块的使用

客户端可以执行import和from语句来导入模块。

import

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

from

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

通俗来讲就是使用from语句,导入的变量名会复制到作用于内,这样在脚本中使用该变量名就可少数如一些:可直接使用变量名,而无需再嵌套模块名称之后。

from *

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

模块的一些特性

导入只发生一次

因为导入的开销较大,因此魔魁啊会在第一次使用import或from时载入并执行,并且只在第一次如此。默认情况下,Python只对每个文件的每个进程做一次操作。之后的导入操作都只会取出已加载的模块对象。

当然有时候需要通过某种导入后再执行一遍模块中的代码,这可以使用在之后介绍的reload内置函数来实现。

import和from是赋值语句

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

# 模块变量的修改(重点理解)

# 假设有一个文件small.py,现对其进行的导入
# x=1
# y=[1,2]
from small import x,y
x=42 # 声明并定义了当前作用域下的x,导入的small中的x并未改变
y[0]=42 # 改变的是small中的y[0],因为变量y在当前作用域下并未被声明和定义,因此会向上扩大其搜索范围。

# 如果想要修改small中的x,则必须使用一下方式
import small
small.x=42 # 如此,small中的x被修改为了42


# 这样的修改常常会使开发人员感到困惑,通才这也是不良设计的选择,之后会再谈到这个技巧。

import和from的对等性

from module import name1,name2

和以下代码是等效的,从中也可一窥from语句的本质

import module
name1=module.name1
name2=module.name2

使用from语句潜在的陷阱

from语句会让变量位置更隐秘和模糊,就像在上面“重点理解”的部分所谈到的那样,倘若恰巧当前命名空间有和导入模块中同名的对象,赋值时可能会悄悄地更改模块中的同名对象。

当让,如果在使用的时候了解并预料到可能发生这种事,在实际情况下这就不是一个大问题了,尤其当你明确列出导入的变量名时。

另一方面,当和reload调用同时使用时,from语句有比较严重的问题,因为导入的变量名可能引用之前版本的对象。再者,from module import *这样的形式的确可能破坏命名空间,让变量名难以理解,尤其是在导入一个以上的文件时。

事实上,from *形式会把一个命名空间融入到另一个,所以会使得模块的命名空间的分割特性时效。这在之后的“模块陷阱”部分会深入讨论。

使用import的时机

当必须使用两个不同模块内定义的相同变量名的变量时,才针对必须使用import,这种情况下不能使用from。

模块命名空间

文件生成命名空间

在模块文件的顶层(也就是不在函数或类的主体内)每一个赋值了的变量名都会变成该模块的属性。

更正式地说明模块加载和作用于的概念以了解其原因。

  • 模块语句会在首次导入时执行。
  • 顶层的赋值语句会创建模块的属性。
  • 模块的命名空间能通过属性__dict__或者dir(M)来获取。
  • 模块是一个独立的作用于(本地变量就是全局变量)。

在python3.0以上的版本中,__dict__属性获取到模块命名空间字典时返回的是一个视图,需要利用list调用:

list(modulename.__dict__.keys())

属性名的点号运算

点号运算其实就是表达式,传回和对象相匹配的属性名的值。其实,属性的点号运算和作用域法则是不相关的概念。当使用点号运算来读取变量名时,就把明确的对象提供给Python,来从其中取出赋值的变量名。LEGB规则只适用于无点号运算的纯变量名。以下是其规则。

  • 简单变量 X是指在当前作用域内搜索变量名X(遵循LEGB规则)
  • 点号运算 X.Y是指在当前范围内搜索X,然后搜索对象X之中的属性Y(而非在作用域内)
  • 多层点号运算 X.Y.Z指的是寻找对象X之中的变量名Y,然后再找对象X.Y之中的Z。
  • 通用性 点号运算可用于任何具有属性的对象:模块、类、C扩展类型等。
    在后边的章节,会看到点号运算对类(这也是继承发生的地方)的意义还要多一点,但是,一般而言,此处所列举的规则适用于Python中的所有变量名。

导入和作用域

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

  • 函数绝对无法看见其它函数内的变量名,除非它们在物理上处于这个函数内。
  • 模块程序代码绝对无法看见其它模块内的变量名,除非明确地进行了导入。
    在Python中,一段程序的作用域完全由程序所处的文件中实际位置决定。作用域绝不会被函数调用或模块导入影响。

命名空间的嵌套

就某种意义而言,虽然导入不会使命名空间发生向上的嵌套,但确实会发生向下的嵌套。利用属性的点号运算路径,有可能深入到任意嵌套的模块中并读取其属性。

重载模块

如果要强制使模块代码重新载入并重新运行,得可以要求Python这么做,也就是的调用reload内置函数。以下将要探索如何使用reload让系统变得更加动态,简而言之:

  • 导入(无论是通过import或from语句)只会模块在流程中第一次导入时,加载和执行该模块的代码。
  • 之后的导入只会使用已加载的模块对象,而不会重载或重新执行文件的代码。
  • reload函数会强制已加载的模块的代码重新载入并重新执行。次文件中新的代码的赋值语句会在适当的地方修改现有的模块对象。

利用reload的原因

reload函数可以修改程序的一部分呢而无须停止整个程序。因此,利用reload,可以立即看到对组件的修改的效果。重载无法用于每种情况,但是能用时,可缩短开发的流程。

需要注意的是,reload当前只能用在Python编写的模块,用C这类语言编写的编译后的扩展名模块也可在执行中动态加载,但是无法重载。

除了可以在交互式提示符号下重载(以及重新执行)模块外,重载在较大的系统中也有用处,在重新启动整个应用程序的代价太大时尤其如此。例如,必须在启动时通过网络链接服务器的系统,就是动态重载的一个非常重要的应用场景。

重载在GUI工作中也很有用(组件的回调行为可以在GUI保持活动的状态下进行修改)。此外,当Python作为C或者C++程序的嵌入式语言时,也有用处(C/C++程序可以请求重载其执行的Python代码而无需停止)。

通常情况下,重载使程序能够提供高度动态的接口。例如,Python通常作为较大系统的定制语言:用户可以在系统运作时通过编写Python程序定制产品,而不用重新编译整个产品(或者甚至获取整个源代码)。这样,Python程序代码本身就增加了一种动态本质了。

reload基础

与import和from不同的是:

  • reload是Python中的内置函数,而不是语句;
  • 传给reload的是已经存在的模块对象,而不是变量名;
  • reload在Pthon3.0中位于模块之中,并且必须导入自己。
from imp import reload
reload(module)              # Get reload itself(in 3.0)
...use module.attributes... # Get updated exports

一般的用法是:导入一个模块,在文本编辑器内修改其源代码,然后将其重载。当调用reload时,Python会重读模块文件的源代码,重新执行其顶层语句。有关reload所需要知道的最重要的事情就是,reload会在适当的地方修改模块对象,但并不会删除并重建模块对象。因此,程序中任何引用该模块对象的地方,自动会受到reload的影响。下面是一些细节。

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

第23章 模块包

除了模块名外,导入也可以指定目录路径。Python代码的目录就称为包,因此,这类导入称为包导入。事实上,包导入是把计算机上的目录变成另一个Python命名空间,而属性则对应于目录中所包含的子目录和模块文件。

当多个相同名称的程序安装在某一机器上时,包导入也可以偶尔用啦i解决导入到不确定性。

包导入基础

依然是使用import或from语句来进行包导入,只不过后边的模块命改成文件路径,各层路径的名称之间用点号相隔,如下:

import dir1.dir2.dir3.mod
from dir1.dir2.dir3.mod import *

包和搜索路径设置

以上的模块路径是相对路径,是相对于当前工作环境下的sys.path模块搜索路径列表中的一个目录。在使用时,不能以任何平台特定的路径语法来表示,如下是错误的:

import C:\mycode\dir1\dir2\dir3\mod.py # wrong!

而应该是增加C:\mycode在PYTHONPATH系统变量中或是.path文件中(假设它不是这个程序的主目录,否则就不需要这个步骤了),然后这样描述:

import dir1.dir2.dir3.mod

__init__.py包文件

如果选择使用包导入,就必须多遵循一条约束:包导入语句的路径中的每个目录内都必须有__init__.py这个文件,否则导入包会失败。但是美欧列在import语句之中的容器目录可以不需要包含这类文件。

对于上面的那个导入语句(假设其存在于dir0这个目录之下,dir0包含着了模块搜索路径或者PYTHONPATH之中)必须遵循以下的规则:

  • dir1和dir2和dir3中必须都含有一个__init__.py文件
  • dir0是容器,不需要__init__.py文件,如果有的话也会被忽略
  • dir0(并非dir0\dir1)必须列在模块搜索路径上(也就是此目录必须是主目录,或者列在PYTHONPATH之中)

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

包的初始化

Python首次导入某个目录时,会自动执行该目录下的__init__.py文件中的所有程序代码。因此,这类文件自然就是防止包内文件所需要初始化的代码的场所。例如,包可以使用其初始化文件来创建所需要的数据文件、连接数据库等。一般而言,如果直接执行,则初始化文件没什么用,当包首次读取时,就会自动运行。

模块命名空间的初始化

在包导入的模型中,脚本内的目录路径,在导入后会变成真实的嵌套对象路径。例如,上一个例子中,导入后,表达式dir1.dir2会运行,并返回一个模块对象,而此对象的命名空间包含了dir2的__init__.py文件所赋值的所有变量名。这类文件为目录(没有实际相配的模块文件)所创建的模块对象提供了命名空间。

from * 语句的行为

作为一个高级功能,可以在__init__.py文件内使用__all__列表来定义目录以from *语句导入时,需要导出什么。在初始化文件中,__all__列表是指定当包(目录)名称使用from *的时候,应该导入到子模块的名称清单。如果没有设定这个列表,from *语句将不会自动加载嵌套于该目录内的子模块。取而代之的是,只加载该目录的__init__.py文件中赋值语句定义的变量名,包括该文件中程序代码明确导入的任何子模块。

当然,如果用不着这些文件的话,也可以让其保持空白。不过,为了让目录导入完全运作,这类文件就得存在。

包导入实例

下列三个文件分别位于目录dir1和dir1的子目录dir2中——这些文件的路径名在注释中给出:

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

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

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

这里的dir1要么是工作所在目录(也就是主目录)的子目录,要么就位于模块搜索路径中的一个目录的子目录。无论哪种,dir1的容器都不需要__init__.py文件。

当Python向下搜索路径时,import语句会在每个目录首次遍历时,执行该目录的初始化文件。print语句加在这里,用来跟踪它们的执行。此外,就像模块文件一样,任何已导入的目录也可以传递给reload,来强制该项目的重新执行。就像这里展示的那样,reload可以接收点号路径名称,来重载嵌套的目录和文件。(注释部分将给出的是执行导入语句后显示的结果)

import dir1.dir2.mod # First imports run init files
# dir1 init
# dir2 init
# in mod.py

import dir1.dir2.mod # Later import again,but do not

reload(dir1)
# dir1 init
# <module 'dir1' from 'dir1\__init__pyc'>

reload(dir1.dir2)
# dir2 init
# <module 'dir1.dir2' from 'dir1\dir2\__init__.pyc'>

导入后,import语句内的路径会变成脚本的嵌套对象路径。

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

包对应的from语句和import语句

当import语句和包一起使用时,有些不方便,因为必须经常在程序中重新输入路径。因此,让包使用from语句,来避免每次读取时都得重新输入路径,通常这样比较方便。也许更重要的是,如果重新改变目录树结构,from语句只需在程序代码中更新依次路径,而import则需要修改很多地方。

为什么使用包导入

包扮演了重要的角色,尤其是在较大的程序中:包让导入更具信息行,并可以作为组织工具,简化模块的搜索路径,而且可以解决模糊性。

首先,因为包导入提供了程序文件的目录信息,因此可以轻松找到文件,从而可以作为组织工具来使用。没有包导入时,通常得通过查看模块搜索路径才能找出文件。再者,如果根据功能把文件组织成子目录,包导入会让模块扮演的角色更为明显,也使代码更具可读性。

包导入也可以大幅简化PYTHONPATH和.pth文件搜索路径设置。实际上,如果所有跨目录的导入,都使用包导入,并且让这些包导入都相对于一个共同的根目录,把所有Python程序代码都存在其中,在搜索路径上就只需一个单独的接入点:通用的根目录。最后,包导入让你想导入的文件更明确,从而解决了模糊性。

包相对导入

在包自身的内部,包文件的导入可以使用和外部导入相同的路径语法,但是,它们也可能使用特殊的包内搜索规则来简化导入语句。也就是说,包内的导入可能相对于包,而不是列出包导入路径。
如今,这种工作与版本有关。py2.6首先在导入上隐式地搜索包目录,而py3.0需要显式地相对导入语法。这种变化,通过使得相同的包的导入更为明显,从而增强代码的可读性。

Python3.0中的变化

包中的导入操作的工作方式在Python3.0中略有比那花。这种变化只适用于我们本章中已经学习国的包目录中的文件中的导入:其他文件中的导入像以前一样工作。对于包中的导入,Python3.0引入了两个变化:

  • 修改了模块导入搜索路径语义,以默认地跳过包自己的目录。导入只是检查搜索路径的其他组件。这叫做“绝对”导入。
  • 扩展了from语句的语法,以允许显式地要求导入只搜索包的目录。这叫做“相对”导入语法。

在Python3.0中,通常必须使用特殊的from语法来导入与导入者位于同一包中的模块,除非从一个包根目录拼出一个完整的路径。没有这一语法,包将不会自动搜索到。

相对导入基础知识

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

  • 在Python3.0中,可以使用from语句前面的点号来表示,导入应该相对于外围的包——这样的导入将知识在包的内部搜索,并且不会搜索位于导入搜索路径(sys.path)上某处的同名模块。其直接效果式包模块覆盖了外部的模块。

在Python3.0中,如下形式的语句:

from . import spam

告诉Python把位于与语句中给出的文件相同包路径中的名为spam的一个模块导入。类似的,以下语句:

from .spam import name

意味着“从名为spam的模块导入变量name,而这个spam模块与包含这条语句的文件位于同一个包下。”

在Python3.0中,不带点号的一个import总是会引发Python略过模块导入搜索路径的相对部分,并且sys.path所包含的绝对目录中查找。例如,在Python3.0的方式中,如下形式的一条语句,总是在sys.path上的某处查找一个string模块,而不是查找该保重具有相同名称的模块。

import string

注意,前面的点号可以用来仅对from语句强制相对导入,而不对import语句这样。前面没有点号的from语句与import语句的行为相同,在Python3.0中是绝对的(略过包目录)。

其他的基于点的相对引用模式也是可能的。在位于名为mypakg的一个包目录中的一个模块文件中,如下替代import形式也想所述的那样工作:

from .string import name1,name2 # Imports names from mypkg.string
from . import string # Imports mypkg.string
from .. import string # Imports string sibling of mypkg

为什么使用相对导入

这个功能设计的初衷是,当脚本在同名文件出现在模块搜索路径上许多地方时,可以解决模糊性。

一来用户自定义的模块可能和标准库中的模块重名,在早期版本中使用import导入时无法跳过包路径,会产生模糊性;二来用户自定义的模块当前虽然不和标准库中的模块重名,但是未来标准库中可能会引入和当前用户自定义模块同名的模块,对未来的用户程序造成影响。

Py3.0中的相对导入解决方案

为解决上述的问题,在Python3.0中,在包中运行的导入已经改成绝对的。在这种方式下,一条如下形式的import语句将总是在包之外找到一个string,通过sys.path的绝对导入搜索。

import string # Imports string outside package

没有前面的点号的一条from语句,也看做是绝对的。

from string import name # Imports name from string outside package

深刻体会以下的导入语句,对位于莫格模块A.B.C中的代码做以下任何一种导入:

from . import D # Imports A.B.D(.means A.B)
from .. import E # Imports A.E(..meas A)
from .D import X # Imports A.B.D.X(.meas A.B)
from ..E import X # Imports A.E.X(..meas A)
相对导入VS绝对导入

此外,一个文件有时候也可以在一条绝对导入语句中显式地指定其包。例如,在下main的语句中,将在sys.path的一个绝对路径中找到mypkg。

from mypkg import string # Imports mypkg.string(absolute)

然而,这依赖于配置以及模块搜索路径这只的顺序,尽管相对导入的语法不会依赖于此。实际上,这种形式需要直接包含将要包含在模块搜索路径中的mypgk。通常,像这样显式地指定包的时候,绝对导入语句必须列出包的根目录下的所有目录。

from system.section.mypkg import string  # system container on sys.path only

在较大或较深的包中,这可能比点语法要做更多的工作。

from . import string # Relateve import syntax

使用后一种形式,包含的包自动搜索,而不管搜索路径设置是什么。

相对导入的作用域

相对导入乍看可能有些令人困惑,但是,如果记住一些关键点,将会很有帮助。

  • 相对导入适用于只在包内导入。
  • 相对导入只是用于from语句。
  • 术语含糊不清。

换句话说,Python3.0中的“包相对导入”针对只是归结为删除了Python2.6中针对包的特定搜索路径行为,并且添加了特殊的from语法来显式地要求相对行为。

模块查找规则总结

使用包导入和相对导入,Python3.0中的模块查找可以完整地概括为如下几条:

  • 简单模块名通过搜索sys.path路径列表上的每个目录来查找,从左到右进行。这个列表由系统默认设置和用户配置设置组成。
  • 包是带有一个特殊__init__.py文件的Python模块的直接目录,这使得一个导入中可以使用A.B.C目录路径语法。在A.B.C的一条导入中,名为A的目录位于相对于sys.path的常规模块导入搜索,B是A中的另一个包子目录,C是一个模块或B中的其他可导入项。
  • 在一个包文件中,常规的import语句使用和其他地方的导入一样的sys.path搜索规则。包中的导入使用from语句以及前面的点号,然而,它是相对于包的:也就是说,只检查包目录,并且不适用常规的sys.path查找。

第24章 高级模块话题

本章将涉及到的话题包括:数据隐藏、__future__模块、__name__变量、sys.path修改、列表工具、通过名称字符串来运行模块、过渡式重载等。

在模块中隐藏数据

在Python中,模块内的数据隐藏是一种惯例而不是一种语法约束。的确可以通过破坏模块名称使这个模块不能工作,但值得庆幸的是,目前还没遇过这种程序员。有些人对Python资料隐藏采取的这种开放态度不以为然,并宣称这表明Python无法实现封装。然而,Python的封装更像是打包而不是约束。

最小化from * 的破坏:_X和__all__

把下划线放在变量名前面,可以防止客户端使from * 语句导入模块命时,把其中的那些变量名复制出去。这其实是为了对命名空间的破坏最小化而已。当然,变量前的下划线不是“私有”声明:依然可以使用其他导入形式看见并修改这些变量名。例如,使用import语句。

此外,也可以在模块顶层把变量名的字符串列表赋值给变量__all__,以达到类似于_X命名惯例的隐藏效果。如此,from *语句只会把列在__all__列表中的这些变量名复制出来。

可以这么理解,_X是之处不被复制的变量名,而__all__则是指出了要复制的变量名。

启用以后的语言特性

可能破坏现有代码方面的变动会不断引进,一开始是以选用扩展功能的方式出席那,默认是关闭的。要开启这类扩展功能,可以使用像以下形式的特定的import语句:

from __future__ import featurename

这个语句一般出现在模块文件的顶端(也许在docstring之后),因为这是以每个模块为基础,开启特殊的代码编译。此外,在交互模式提示符下提交这个语句也是可以的,从而能够实验今后语言的变化。于是,在接下来的交互会话过程中就可以使用这些功能了。

混合用法模式:__name__和__main__

这是一个特殊的与模块相关的技巧,可把文件作为模块导入,并以独立式程序的形式运行。每个模块都有个名为__name__的内置函数,Python会自动设置该属性:

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

结果就是模块可以检测自己的__name__,来确定它是在执行还是在导入。实际上,一个模块的__name__变量充当一个使用模式标志,允许它编写成一个可导入的库和一个顶层脚本。尽管简单,我们将会看到这一钩子几乎在可能遇到的每个Python程序文件中应用。

使用__name__测试最常见的就是自我测试代码。简而言之,可以在文件末尾加个__name__测试,把测试模块导出的程序代码放在模块中。如此以来,可以继续导入,在客户端使用该文件,而且可以通过检测其逻辑在系统shell中运行它。事实上这儿是最简单的单元测试协议(在第35章讨论其他用于测试Python程序代码的常用选项,哟啊知道,unittest和doctest标准库模块,提供更为高级的测试工具。)

在编写既可以作为命令行工具也可以作为工具库使用的文件时,__name__技巧也很好用。例如,夹是用Python编写了一个文件寻找脚本。如果将其打包成一些函数,而且文件中加入__name__测试,当此文件独立执行时,就自动调用这些函数,这样就能提高代码的利用效率。如此一来,脚本的代码就可以在其他程序中再利用了。

以__name__进行单元测试

使用带有__name__的命令行参数

这里展示了通常使用__name__技巧的另一种方式。如下的模块formats.py,为导入者定义了字符串格式化工具,还检查其名称看它是否作为一个顶层脚本再运行;如果是这样的话,它测试并使用系统命令行上列出的参数来运行一个定制的或传入的测试。再Python中,sys.argv列表包含了命令行参数,它是反映在命令行上录入的单词的一个字符串列表,其中,第一项总是将要运行的脚本的名称:

"""
Various specialized string display formatting utilities.
Test me with canned self-test or command-line garguments.
"""

def commas(N):
    """
    format positive integer-like N for display with commas between
    digit groupings:xxx,yyy,zzz
    """
    digits=str(N)
    assert(digits.isdigit())
    result=''
    while digits:
        digits,last3=digits[:-3],digits[-3:]
        result=(last3+','+result) if result else last3
    return result

def money(N,width=0):
    """
    format number N for display with commas,2 decimal digits,
    leading $ and sign,and optional padding:$ -xxx,yyy.zz
    """
    sign='-' if N <0 else ''
    N=abs(N)
    whole=commas(int(N))
    fract=('%.2f'% N)[-2:]
    format='%s%s.%s'%(sign,whole,fract)
    return '$%*s'%(width,format)

if __name__=='__main__':
    def selftest():
        tests=0,1  # fails:-1,1.23
        tests+=12,123,1234,12345,123456,1234567
        tests+=2**32,2**100
        for test in tests:
            print(commas(test))
        print('')
        tests=0,1,-1,1.23,1.,1.2,3.14159
        tests+=12.34,12.344,12.345,12.346
        tests+=2**32,(2**32+.2345)
        tests+=-1.2345,-1.2,-0.2345
        tests+=-(2**32),-(2**32+.2345)
        tests+=(2**100),-(2**100)
        for test in tests:
            print('%s[%s]'%(money(test,17),test))
        
        import sys
        if len(sys.argv)==1:
            selftest()
        else:
            print(money(float(sys.argv[1]),int(sys.argv(2))))

当直接运行时,它像前面那样测试自己,但是它使用命令行上的选项阿里控制测试行为。请直接运行这个文件而不带命令行参数,看看它的自测试代码打印出什么。要测试特定的字符串,用一个最小的字段宽度将它们传入到命令行上。

想了解更高级的命令行处理,请参阅Python的标准库和手册中的getopt和optparse模块。

修改模块搜索路径

模块搜索路径是一个目录列表,可以通过环境比那辆PYTHONPATH以及可能的.pth路径文件进行定制。而还没有介绍的是,实际上Python程序本身是如何修改搜索路径的,也就是修改名为sys.path的内置列表。sys.path在程序启动时就会进行初始化,但在那之后,可以随意对其元素进行删除、附加和重设。

import sys
sys.path
# ['','C:\\users','C:\\Windows\\system32\\python30.zip'... more deleted...]
sys.path.append('C:\\sourcedir') # Extend module search path
import string     # Al imports search the new dir

一旦做了这类的修改,就会对Python程序中将要导入的地方产生影响,因为所有导入和文件都共享在了同一个sys.path列表上。事实上,这个列表可以任意修改。

sys.path=[r'd:\temp']  # change the hole module search path

因此,可以使用这个技巧,在Python程序中动态配置搜索路径。不过,要小心:如果从路径中删除重要目录,就无法获取一些关键的工具了。例如,如果从路径中删除Python的源代码库目录,就再也无法获取string模块了。

此外,还要记住sys.path的设置方法旨在修改的Python会话或程序中才会存续。在Python结束之后,不会被保留下来。PYTHONPATH和.pth文件路径是保存在操作系统中,而不是执行中的Python程序。

import语句和from语句的as扩展

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

import modulename as name

相当于

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

在这类import之后,可以(事实上也必须)使用列在as之后的变量名来引用该模块。from语句也可以这么用,把从某个文件导入的变量名,赋值给脚本中的不同变量名:

from modulename import attrname as name

这个扩展功能很常用,替变量名较长的比哪里那个提供简短一些的同义词,而且当已在脚本中使用一个变量名使得执行普通import语句会被覆盖时,使用as,就可避免变量名冲突。

import reallylongmodulenmame as name
name.func()

from module1 import utility as util1
from module2 import utility as util2

此外,使用包导入功能呢时,也可为整个目录路径提供简短、简单的名称,十分方便。

import dir1.dir2.mod as mod
mod.func()

模块是对象:元程序

因为模块通过内置属性显示了它们的大多数有趣的特性,因此,可以很容易地编写程序来管理其他程序。通常称这类管理程序为元程序(metaprogram),因为它们是在其他系统之上工作。这也称为内省(introspection),因为程序能看见和处理对象的内部。内省是高级功能,但是它也可以用作创建程序的工具。

例如,要取得M模块内名为name的属性,可以有以下的方式:

M.name
M.__dict__['name']
sys.modules['M'].name
getattr(M,'name')

通过像这样揭示了模块内部的机制,Python可以帮助你建立关于程序的程序。例如,以下是名为mydir.py的模块,运用这个概念,可以实现定制版本的内置函数dir。它定义并导出了一个名为listing的函数,这个函数以模块对象为参数,打印该模块命名空间的格式化列表。

"""
mydir.py:a module that lists the namespaces of other modules
"""

seplen=60
sepchar='-'

def listing(module,verbose=True):
    sepline=sepchar *seplen
    if verbose:
        print(sepline)
        print('name:',module.__name__,'\nfile:',moduleule.__file__)
        print(sepline)
    
    count=0
    for attr in module.__dic__:
        print(sepline)
        print('%02d) %s'%(count,attr),end=' ')
        if attr.startswith('__'):
            print('<built-in name>')
        else:
            print(getattr(module,arrt))
        count+=1
    
    if verbose:
        print(sepline)
        print(module.__name__,'has %d names'% count)
        print(seplien)
        
if __name__ == '__main__':
    import mydir
    listing(mydir)  # Self-test code:list myself

用名称字符串导入模块

一条import或from语句中的模块名是直接编写的变量名称。然而,有时候,我们的程序可以在运行时以一个字符串的形式获取要到导入的模块的名称(例如:如果一个用户从一个GUI中选择一个模块名称)。遗憾的是,无法使用import语句来直接载入以字符串形式给出其名称的一个模块,Python期待一个变量名,而不是字符串。例如:

import "string"
# File "<stdin>",line 1
#     import "string"
# SyntaxError:invalid syntax

直接把该字符串赋给一个变量名也是无效的:

x="string"
import x   # will raise SyntaxError like the code above

在上面的语句,Python将会尝试导入一个文件x.py而不是string模块——一条import语句中的名称既变成了赋给载入的模块的一个变量,也从字面上表示了该外部文件。

为了解决这个问题,就需要使用特殊的工具,从运行时生成的一个字符串来动态地载入一个模块。最通用的方法是,把一条导入语句构建成为Python代码的一个字符串,并且将其传递给exec内置函数以运行(exec是Python2.6的一条语句,但是它完全可以像这里展示的那样使用——直接省略掉圆括号):

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

exec函数(以及其进去eval)编译一个代码字符串,并且将其传递给Python解释器以执行。在Python中,字节代码编译器在运行时可以使用,因此,我们像这样编写构建和运行其他程序的程序。默认情况下,exec运行当前作用域中的代码,但是,可以通过传入可选的命名空间字典来更加具体地应用。

exec唯一的、真正的缺点是,每次运行时它必须编译impor语句,因此如果它多次运行,如果使用内置的__import__函数来从一个名称字符串载入的话,代码可能会运行得更快。效果时类似的,但是,__import__运行模块对象,因此,这里将其赋值给一个名称以保存它:

modname="string"
string=__import__(modname)
string
# <module 'string' from 'c:\Python3\lib\string.py'>

过渡性模块重载

在之前学习过了模块重载,这是选择代码中的修改而不需要停止或重新启动一个程序的一种方式。当重载入一个模块时,Python只是重新载入特殊模块的文件,它不会自动重载那些为了导入要重载文件的模块。

例如,如果要重载某个模块A,并且A导入模块B和C,重载只适用于A,而不适用于B和C。A中导入B和C的语句在重载的时候重新运行,但是,它们只是获取已经载入的B和C模块对象(假设它们之前已经载入过了)。在实际的代码中,文件A.py如下:

import B # Not reloaded when A is 
import C # just an import of an already loaded module

% python
>>>...
>>> from imp import reload
>>> reload(A)

默认情况下,这意味着不能依赖于重载来过渡性地选择程序中的所有模块的修改;相反,必须使用多次reload调用来独立地更新子部分。对于交互测试的大系统而言,工作量很大。可以通过在A这样的父模块中添加reload调用,从而设计自己的系统能够自动重载它们的子部分,但是,这样会使模块的代码变得复杂。

一种更好的方法是,编写一个通用的工具来自动进行过渡性重载,通过扫描模块的__dict__属性并检查每一项的type以找到要重新载入的嵌套模块。这样的一个工具函数应该递归地调用自己,来导航任意形式的导入依赖性链条。

例如,下面给出的模块reloadall.py有一个reload_all函数来自动地重载一个模块,以及该模块导入的每个模块等,所有通往每个导入链条最低端的通路都被考虑到。它使用字典来记录已经重载的模块,递归地遍历导入链条,以及标准库的types模块,这个模块直接为内置类型预定义type结果。访问字典的技术在这里用来在导入是递归或冗余时避免循环,因为模块对象可以是字典键:

"""
reloadall.py:transitively reload nested modules
"""

import types
from imp import  reload

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

def transitive_reload(module,visited):
    if not module in visited:
        status(module)
        reload(module)
        visited[module]=None
        for attrobj in module.__dict__.values():
            if type(attrobj)==type.ModuleType:
                transitive_reload(attrobj,visited)
def reload_all(*args):
    visited={}
    for arg in args:
        if type(arg)==type.ModuleType:
            transitive_reload(arg,visited)
            
if __name__=='__main__':
    import reloadall
    reload_all(reladall)

模块设计理念

就像函数一样,模块也有设计方面的折中考虑:许哟啊考虑哪些函数放进模块、模块通信机制等。当开始编写较大的Python系统时,这些就会变得明朗起来。但是要记住以下一些通用的概念。

  • 总是在Python的模块内编写代码。
  • 模块耦合要降到最低:全局变量。
  • 最大化模块的粘合性:统一目标。
  • 模块应该少去修改其他模块的变量。

模块陷阱

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

当模块首次导入(或重载)时,Python会从头到尾执行语句。这里有些和前向引用相关的含义,值得在此强调:

  • 在导入时,模块文件顶层的程序代码(不在函数内),一旦Python运行到时,就会立刻执行。因此,该语句时无法引用文件后面位置赋值的变量名。
  • 位于函数主体内的代码直到函数被调用后才会运行。因为函数内的变量名在函数实际执行前都不会解析,通常可以引用文件内任意地方的变量。

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

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

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

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

def func2():
    return "Hello"

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

在顶层程序中混用def不仅难读,也造成了对语句顺序的依赖性。作为一条原则,**如果需要把立即执行的代码和def一起混用,就要把def放在文件前面,把顶层代码放在后面。**这样,函数在使用的代码运行的时候,就能保证它们都已定义并赋值过了。

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

from语句其实是在导入者的作用域内对变量名的赋值语句,也就是变量名的拷贝运算,而不是变量名的别名机制。它的实现和Python所有赋值运算都一样,但是其微妙之处在于,共享对象的代码存在于不同的文件中。例如,假设我们定义了下列模块(nested1.py)。

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

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

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

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

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

from *会让变量语义模糊

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

解决的办法则是不要这么做:试着在from语句中明确列出想要的属性,而且限制在每个文件中最多只有一个被导入的模块使用from *这样的形式。如此一来,任何未定义的变量名一定可以减少到某一个from *所代表的模块。如果总是使用import而不是from,就可以完全避开这个问题,但这样的建议过于苛刻。

reload不会影响from导入

这是另一个和from相关的陷阱:正如前边讨论过的那样,因为from在执行时会复制(赋值)变量名,所以不会连接到变量名的那个模块。通过from导入的变量名就简单地变成了对象的引用,当from运行时这个对象恰巧在被导入者内由相同的变量名引用。

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

from module import X  # X may not feflect any module reloads!
...
from imp import reload
reload(module)  # Changes module,but not my names
X  # Still references old object

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

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

reload、from以及交互模式测试

通常情况下,最好不要通过导入或重载来启动程序,因为其中牵涉到了许多复杂的问题。当引入from之后,事情就变得更糟了。Python的初学者常常会遇到这里所提到的陷阱。在文本编辑窗口开启一个模块文件之后,假设启动一个交互模式会话,通过from加载并测试模块:

from module import function
function(1,2,3)

发现了一个BUG,跳回编辑窗口,做了修改,并试着重载模块:

from imp import reload
import module
reload(module)
function(1,2,3)

然而,这样也无法运行:reloda更新了模块对象,但是就像上一节讨论的,像function这样的变量名之前是从模块中复制出来的,依然引用了旧的对象(在这个例子中,就是function的原始版本)。要确实获得新的function,必须在reload之后调用module.fucntion,或者重新执行from:

from imp import reload
import module
reload(module)
from module import function
function(1,2,3)

这样新版本的function才能执行。

正如所见的那样,使用reload和from有些本质上的问题:不但得记住导入后要重载,而且得记住在重载之后重新执行from语句。即使是专家,其复杂度也让人够头疼的了(实际上,这种情形在Python3.0中甚至变得更糟糕,因为必须也记住导入reload本身)。

使用reload和import,或者其他方式启动程序,建议使用如:IDLE中的“Run”/“Run Module”菜单选项、文件图标或者是系统命令行。

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

这是一个最诡异(也是最罕见)的陷阱。因为导入会从头到尾执行一个文件的语句,使用相互导入的模块时,需要十分小心(称为递归导入)。因为一个模块内的语句在其导入另一个模块时不会全部执行,有些变量名可能还不存在。

如果使用import取出整个模块,模块的变量名在稍后是哦那个点号运算,在获取值之前都不会被读取。但是,如果使用from来取出特定的变量名,则必须记住,只能读取在模块中已经赋值的变量名。

解决的办法就是,不要在递归导入中使用from(真的,不要)。如果这么做,Python虽然不会卡在死循环中,但是程序又会依赖于模块中语句的顺序。

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值