14.3 可执行的对象声明和内建函数

Python提供了大量的BIF来支持可调用/可执行对象,其中包括exec语句。这些函数帮助程序员执行代码对象,也可以用内建函数complie()来生成代码对象。

14.3 可执行的对象声明和内建函数 - 图1

14.3.1 callable()

callable()是一个布尔函数,确定一个对象是否可以通过函数操作符(())来调用。如果函数可调用便返回 True,否则便是False(对与2.2和较早的版本而言,分别是1和0)。这里有些对象及其对应的callable返回值:

14.3 可执行的对象声明和内建函数 - 图2

14.3.2 compile()

compile()函数允许程序员在运行时刻迅速生成代码对象,然后就可以用exec语句或者内建函数eval()来执行这些对象或者对它们进行求值。一个很重要的观点是:exec和eval()都可以执行字符串格式的Python代码。当执行字符串形式的代码时,每次都必须对这些代码进行字节编译处理。compile()函数提供了一次性字节代码预编译,以后每次调用的时候,都不用编译了。

compile的三个参数都是必需的,第一参数代表了要编译的Python代码。第二个字符串,虽然是必需的,但通常被置为空串。该参数代表了存放代码对象的文件的名字(字符串类型)。compile的通常用法是动态生成字符串形式的Python代码,然后生成一个代码对象——代码显然没有存放在任何文件。

最后的参数是个字符串,它用来表明代码对象的类型。有三个可能值:

14.3 可执行的对象声明和内建函数 - 图3

1. 可求值表达式

14.3 可执行的对象声明和内建函数 - 图4

2. 单一可执行语句

14.3 可执行的对象声明和内建函数 - 图5

3. 可执行语句组

14.3 可执行的对象声明和内建函数 - 图6

在最后的例子中,我们第一次看到input()。一直以来,我们都是从raw_input()中读取输入的。内建函数input()是我们将在本章稍后讨论的一个快捷函数。

14.3.3 eval()

eval()对表达式求值,后者可以为字符串或内建函数complie()创建的预编译代码对象。这是eval()第一个也是最重要的参数……这便是你想要执行的对象。第二个和第三个参数,都为可选的,分别代表了全局和局部名称空间中的对象。如果给出这两个参数,globals必须是个字典,locals可以是任意的映射对象,比如,一个实现了getitem()方法的对象。(在2.4之前,local必须是一个字典)如果都没给出这两个参数,分别默认为globals()和locals()返回的对象,如果只传入了一个全局字典,那么该字典也作为locals传入。好了,我们一起来看看eval():

14.3 可执行的对象声明和内建函数 - 图7

在这种情况下,eval()和int()都返回相同的结果:整型932。然而,它们采用的方式却不尽相同。内建函数eval()接收引号内的字符串并把它作为Python表达式进行求值。内建函数int()接收代表整型的字符串并把它转换为整型。这只有在该字符串只由字符串932组成的时候才会成功,而该字符串作为表达式返回值932,932也是字符串”932”所代表的整型。当我们用纯字符串表达式的时候,两者便不再相同了:

14.3 可执行的对象声明和内建函数 - 图8

在这种情况下,eval()接收一个字符串并把“100+200”作为表达式求值,当进行整型加法后,给出返回值300。而对int()的调用失败了,因为字符串参数不是能代表整型的字符串,因为在字符串中有非法的文字,即,空格以及“+”字符。可以这样理解eval()函数的工作方式:对表达式两端的引号视而不见,接着假设“如果我是Python解释器,我会怎样去观察表达式呢?”,换句话说,如果以交互方式输入相同的表达式,解释器会做出怎么样的反应呢?按下回车后的结果应该和eval()返回的结果相同。

14.3.4 exec

和eval()相似,exec语句执行代码对象或字符串形式的Python代码。类似地,用compile()预编译重复代码有助于改善性能,因为在调用时不必经过字节编译处理。exec语句只接受一个参数,下面便是它的通用语法:

14.3 可执行的对象声明和内建函数 - 图9

被执行的对象(obj)可以只是原始的字符串,比如单一语句或是语句组,它们也可以预编译成一个代码对象(分别用“single”和“exec”参数)。下面的例子中,多个语句作为一个字符串发送给exec:

14.3 可执行的对象声明和内建函数 - 图10

最后,exec还可以接受有效的Python文件对象。如果我们用上面的多行代码创建一个叫xcount.py的文件,那么也可以用下面的方法执行相同的代码:

14.3 可执行的对象声明和内建函数 - 图11

注意一旦执行完毕,继续对exec的调用就会失败。呃,并不是真正的失败……只是不再做任何事,这或许让你感到吃惊。事实上,exec已从文件中读取了全部的数据且停留在文件末尾(end-of-file, EOF)。当用相同文件对象对exec进行调用的时候,便没有可以执行的代码了,所以exec什么都不做,如同上面看见的行为。我们如何知道它在EOF呢?

我们用文件对象的tell()方法来告诉我们处于文件的何处,然后用os.path.getsize()来告诉我们 xcount.py脚本有多大。这样你就会发现,两个数字完全一样:

14.3 可执行的对象声明和内建函数 - 图12

14.3 可执行的对象声明和内建函数 - 图13

如果想在不关闭和重新打开文件的情况下再次运行它,可以用seek()到文件最开头并再次调用exec了。比如,假定我们还没有调用f.close(),那么我们可以这样做:

14.3 可执行的对象声明和内建函数 - 图14

14.3.5 input()

内建函数input()是eval()和raw_input()的组合,等价于eval(raw_input())。类似于raw_input(),input()有一个可选的参数,该参数代表了给用户的字符串提示。如果不给定参数的话,该字符串默认为空串。

从功能上看,input不同于raw_input(),因为raw_input()总是以字符串的形式,逐字地返回用户的输入。input()履行相同的的任务;而且,它还把输入作为Python表达式进行求值。这意味着input()返回的数据是对输入表达式求值的结果:一个Python对象。

下面的例子会让人更加清楚:当用户输入一个列表时,raw_input()返回一个列表的字符串描绘,而 input()返回实际的列表:

14.3 可执行的对象声明和内建函数 - 图15

上面用raw_input()运行。正如你看见的,每样东西都是字符串。现在来看看当用input()的时候会发生什么:

14.3 可执行的对象声明和内建函数 - 图16

虽然用户输入字符串,但是input()把输入作为Python对象来求值并返回表达式的结果。

14.3.6 使用Python在运行时生成和执行Python代码

本小节我们将看到两个Python脚本的例子,这两个例子在运行时刻把Python代码作为字符串并执行。第一个例子更加动态,但第二个突出了函数属性。

1. 在运行时生成和执行Python代码

第一个例子是loopmake.py脚本,一个简单的、迅速生成的和执行循环的计算机辅助软件工程(computer-aided software engineering,CASE)。它提示用户给出各种参数(比如,循环类型(while或for),迭代的数据类型(数字或序列)),生成代码字串,并执行它。

例14.1 动态生成和执行Python代码(loopmake.py)

14.3 可执行的对象声明和内建函数 - 图17

14.3 可执行的对象声明和内建函数 - 图18

以下是一些脚本执行的例子。

14.3 可执行的对象声明和内建函数 - 图19

14.3 可执行的对象声明和内建函数 - 图20

14.3 可执行的对象声明和内建函数 - 图21

2.逐行解释

1 ~ 25行

在脚本的第一部分,我们设置了两个全局变量。第一个是由一行破折号(即是名字)组成的静态字符串,第二个则是由用于生成循环的骨架代码组成的字典。for循环的健值是“f”,用于迭代序列的while循环的则是“s”,而记数while循环的是“n”。

27 ~ 30行

这里我们提示用户输入他想要的循环类型和数据类型。

32 ~ 36行

选定数字;给出开始、停止和增量值。在这个部分的代码中,第一次引入了内建函数input()。我们将在14.3.5小节中看到,input()和raw_input()相似,因为它提示用户给出字符串输入,但是不同于raw_input(), input()会把输入当成Python表达式来求值,即使用户以字符串的形式输入,也会返回一个Python对象。

38 ~ 39行

选定序列;这里以字符串的形式输入一个序列。

41行

给出用户想要使用的迭代循环变量的名字。

43 ~ 44行

生成添加自定义内容的for循环。

46 ~ 50行

生成迭代序列的while循环。

52 ~ 54行

生成计数的while循环。

56 ~ 61行

输出生成的源代码及其执行后的结果。

63 ~ 64行

当直接调用该模块的时候,执行main()。

为了很好地控制脚本的大小,我们从原来的脚本中剔除了所有的注释和错误检测。在本书的Web站点上,都可以找到原来的和修改后的版本。

扩展的版本包括了额外的特性,比如用于字符串输入的不必要的引号,输入数据的默认值,以及检测无效的返回和标识符;也不允许以关键字和内建名字作为变量名字。

3.有条件地执行代码

第二个例子着重描写了在第11章引入的函数属性,它是从Python增强提议232 (PEP 232)中的例子得到的灵感。假设你是一位负责质量控制的软件开发者,你鼓励你的工程师将回归测试或回归指令代码放到主代码中,但又不想让测试代码混合到产品代码中。你可以让工程师创建字符串形式的测试代码。当你的测试框架执行的时候,它会检测函数是否定义了测试体,如果是的话,(求值并)执行它。如果不是,便跳过,像通常一样执行。

例14.2 函数属性(funcAttrs.py)

调用sys.exit()使Python解释器退出。exit()的任何整型参数作为退出状态会返回给调用者,该值默认为0。

14.3 可执行的对象声明和内建函数 - 图22

1 ~ 8行

我们在脚本的开始部分定义了foo()和bar()。两个函数都只是返回True。不同点在于foo()没有属性而bar()有文档字串。

10 ~ 16行

使用函数属性,我们给foo()加入了文档字串以及退化或单元测试字符串。注意检测字符串实际上由 Python代码组成。

18 ~ 29行

好了,真正的工作在这里开始。我们从用内建函数dir()迭代现在(即全局)名称空间开始。它返回的列表包含了所有对象的名字。因为这些都是字符串,我们需要在第19行将它们转化为真正的Python对象。

除了预期的系统变量,比如,builtins,我们还期望显示函数。我们只对函数有兴趣;第20行的代码让我们跳过了所有遇到的非函数对象。一旦我们知道我们有某个函数,就可以检查它是否有文档字串,如果有的话,把它显示出来。23~27行表演了魔法。如果函数有检测属性,那么就执行它,否则告诉用户没有可用的单元测试。最后的几行显示出遇到的非函数对象的名字。执行代码后,我们得到如下的输出:

14.3 可执行的对象声明和内建函数 - 图23