11.3 创建函数

11.3.1 def语句

函数是用def语句来创建的,语法如下:

11.3 创建函数 - 图1

标题行由def关键字,函数的名字,以及参数的集合(如果有的话)组成。def子句的剩余部分包括了一个虽然可选但是强烈推荐的文档字串和必需的函数体。在本书中我们已经看到很多函数的声明,这又是一个:

11.3 创建函数 - 图2

11.3.2 声明与定义比较

在某些编程语言里,函数声明和函数定义区分开的。一个函数声明包括提供对函数名,参数的名字(传统上还有参数的类型),但不必给出函数的任何代码,具体的代码通常属于函数定义的范畴。

在声明和定义有区别的语言中,往往是因为函数的定义可能和其声明放在不同的文件中。Python将这两者视为一体,函数的子句由声明的标题行以及随后的定义体组成的。

11.3.3 前向引用

和其他高级语言类似,Python也不允许在函数未声明之前,对其进行引用或者调用。我们下面给出几个例子来看一下:

11.3 创建函数 - 图3

如果我们调用函数foo(),肯定会失败,因为函数bar()还没有声明:

11.3 创建函数 - 图4

我们现在定义函数bar(),在函数foo()前给出bar()的声明:

11.3 创建函数 - 图5

现在我们可以安全的调用foo(),而不会出现任何问题:

11.3 创建函数 - 图6

事实上,我们甚至可以在函数bar()前定义函数foo():

11.3 创建函数 - 图7

太神奇了,这段代码可以非常好的运行,不会有前向引用的问题:

11.3 创建函数 - 图8

这段代码是正确的,因为即使(在foo()中)对bar()进行的调用出现在bar()的定义之前,但foo()本身不是在bar()声明之前被调用的。换句话说,我们声明foo(),然后再声明bar(),接着调用foo(),但是到那时,bar()已经存在了,所以调用成功。

注意foo()在没有错误的情况下成功输出了foo()。名字错误是当访问没有初始化的标识符时才产生的异常。

11.3.4 函数属性

在这一章中,我们稍后将对命名空间进行简短的讨论,尤其是它们与变量作用域的关系。在下一章中会有对命名空间的更深入的探讨,然而,这里我们只是想要指出Python名称空间的基本特征。

你可以获得每个Pyhon模块、类和函数中任意的名称空间。你可以在模块foo和bar里都有名为χ的一个变量,但是在将这两个模块导入你的程序后,仍然可以使用这两个变量。所以,即使在两个模块中使用了相同的变量名字,这也是安全的,因为句点属性标识对于两个模块意味了不同的命名空间,比如说,在这段代码中没有名字冲突:

11.3 创建函数 - 图9

函数属性是Python另外一个使用了句点属性标识并拥有名称空间的领域(更多关于名称空间将在本章的稍后部分以及第十二章关于Python的模块中进行讨论)。

11.3 创建函数 - 图10

上面的foo()中,我们以常规的方式创建了我们的文档字串,比如,在函数声明后第一个没有赋值的字串。当声明bar()时,我们什么都没做,仅用了句点属性标识来增加文档字串以及其他属性。我们可以接着任意地访问属性。下面是一个使用了交互解释器的例子(你可能已经发现,用内建函数help()显示会比用doc属性更漂亮,但是你可以选择你喜欢的方式)。

11.3 创建函数 - 图11

注意我们是如何在函数声明外定义一个文档字串。然而我们仍然可以就像平常一样,在运行时刻访问它。然而你不能在函数的声明中访问属性。换句话说,在函数声明中没有“self”这样的东西让你可以进行诸如dict[‘version’] = 0.1的赋值。这是因为函数体还没有被创建,但之后你有了函数对象,就可以按我们在上面描述的那样方法来访问它的字典。另外一个自由的名称空间!

函数属性是在2.1中添加到Python中的,你可以在PEP232中阅读到更多相关信息。

11.3.5 内部/内嵌函数

在函数体内创建另外一个函数(对象)是完全合法的。这种函数叫做内部/内嵌函数。因为现在Python支持静态地嵌套域(在2.1中引入但是到2.2时才是标准),内部函数实际上很有用的。内嵌函数对于较老的python版本没有什么意义,那些版本中只支持全局和一个局部域。那么如何去创造一个内嵌函数呢?

最明显的创造内部函数的方法是在外部函数的定义体内定义函数(用def关键字),如:

11.3 创建函数 - 图12

我们将以上代码置入一个模块中,如inner.py,然后运行,会得到如下输出:

11.3 创建函数 - 图13

内部函数一个有趣的方面在于整个函数体都在外部函数的作用域(即是你可以访问一个对象的区域;稍后会有更多关于作用域的介绍)之内。如果没有任何对bar()的外部引用,那么除了在函数体内,任何地方都不能对其进行调用,这就是在上述代码执行到最后你看到异常的原因。

另外一个函数体内创建函数对象的方式是使用lambda语句。我们会在稍后的11.7.1小节进行讲述。如果内部函数的定义包含了在外部函数里定义的对象的引用(这个对象甚至可以是在外部函数之外),内部函数会变成被称为闭包(closure)的特别之物。在接下来的11.8.4小节,我们将对闭包进行更多的学习。稍后我们将介绍装饰器,但是例子程序也包含了闭包的预览。

11.3.6 *函数(与方法)装饰器

装饰器背后的主要动机源自Python面向对象编程。装饰器是在函数调用之上的修饰。这些修饰仅是当声明一个函数或者方法的时候,才会应用的额外调用。

装饰器的语法以@开头,接着是装饰器函数的名字和可选的参数。紧跟着装饰器声明的是被修饰的函数和装饰函数的可选参数。装饰器看起来会是这样:

11.3 创建函数 - 图14

那么装饰器语法如何(以及为什么)产生的呢?装饰器背后的灵感是什么?唔,当静态方法和类方法在2.2时被加入到Python中的时候,实现方法很笨拙:

11.3 创建函数 - 图15

(要澄清的是对于那个发行版本,这不是最终的语法)在这个类的声明中,我们定义了叫staticFoo()的方法。现在因为打算让它成为静态方法,我们省去它的self参数,而你会在12章中看到,self参数在标准的类方法中是必需的。接着用staticmethod()内建函数来将这个函数“转化”为静态方法,但是在defstaticFoo()后跟着staticFoo=staticmethod(sta-ticFoo)显得有多么的臃肿。使用装饰器,你现在可以用如下代码替换掉上面的:

11.3 创建函数 - 图16

此外,装饰器可以如函数调用一样“堆叠”起来,这里有一个更加普遍的例子,使用了多个装饰器:

11.3 创建函数 - 图17

11.3 创建函数 - 图18

这和创建一个组合函数是等价的。

11.3 创建函数 - 图19

函数组合用数学来定义就像这样: (g.f)(x)=g(f(x))。对于在Python中的一致性:

11.3 创建函数 - 图20

1.有参数和无参数的装饰器

是的,装饰器语法一开始有点让你犯迷糊,但是一旦你适应了,唯一会困扰你的就是什么时候使用带参数的装饰器。没有参数的情况,一个装饰器如:

11.3 创建函数 - 图21

…非常地直接

11.3 创建函数 - 图22

跟着是无参函数(如上面所见)组成。然而,带参数的装饰器decomaker():

11.3 创建函数 - 图23

需要自己返回以函数作为参数的装饰器。换句话说,decomaker()用deco_args做了些事并返回函数对象,而该函数对象正是以foo作为其参数的装饰器。简单地说:

11.3 创建函数 - 图24

这里有一个含有多个装饰器的例子,其中的一个装饰器带有一个参数:

11.3 创建函数 - 图25

这等价于:

11.3 创建函数 - 图26

我们希望如果你明白这里的这些例子,那么事情就变得更加清楚了。下面我们会给出简单实用的脚本,该脚本中装饰器不带任何参数。例子11.8就是含有无参装饰器的中间脚本。

2.什么是装饰器

现在我们知道装饰器实际就是函数。我们也知道他们接受函数对象。但它们是怎样处理那些函数的呢?一般说来,当你包装一个函数的时候,你最终会调用它。最棒的是我们能在包装的环境下在合适的时机调用它。我们在执行函数之前,可以运行些预备代码,如post-morrem分析,也可以在执行代码之后做些清理工作。所以当你看见一个装饰器函数的时候,很可能在里面找到这样一些代码,它定义了某个函数并在定义内的某处嵌入了对目标函数的调用或者至少一些引用。从本质上看,这些特征引入了Java开发者称呼之为AOP (Aspect Oriented Programming,面向方面编程)的概念。你可以考虑在装饰器中置入通用功能的代码来降低程序复杂度。例如,可以用装饰器来:

  • 引入日志;

  • 增加计时逻辑来检测性能;

  • 给函数加入事务的能力。

对于用Python创建企业级应用,支持装饰器的特性是非常重要的。你将会看到上面的条例与我们下面的例子有非常紧密地联系,这在例11.2中也得到了很好地体现。

3.修饰符举例

下面我们有个极其简单的例子,但是它应该能让你开始真正地了解装饰器是如何工作的。这个例子通过显示函数执行的时间“装饰”了一个(没有用的)函数。这是一个“时戳装饰”,与我们在16章讨论的时戳服务器非常相似。

这个装饰器(以及闭包)示范表明装饰器仅仅是用来“装饰”(或者修饰)函数的包装,返回一个修改后的函数对象,将其重新赋值原来的标识符,并永久失去对原始函数对象的访问。

例11.2 使用函数装饰器的例子(deco.py)

11.3 创建函数 - 图27

运行脚本,得到如下输出:

11.3 创建函数 - 图28

4.逐行解释

5 ~ 10行

在启动和模块导入代码之后,tsfunc()函数是一个显示何时调用函数的时戳的装饰器。它定义了一个内部的函数wrappedFunc(),该函数增加了时戳以及调用了目标函数。装饰器的返回值是一个“包装了”的函数。

12 ~ 21行

我们用空函数体(什么都不做)来定义了foo()函数并用tsfunc()来装饰。为证明我们的设想,立刻调用它,然后等待4秒,然后再调用两次,并在每次调用前暂停1秒。

结果,函数立刻被调用,第1次调用后,调用函数的第2个时间点应该为5(4+1),第3次的时间应该大约为之后的1秒。这与上面看见的函数输出十分吻合。

你可以在《Python langugae reference》、Python2.4中“What’s New in Python 2.4”的文档和PEP318中来阅读更多关于装饰器的内容。