来源:1.3 Defining New Functionshtml
译者:飞龙python
协议:CC BY-NC-SA 4.0git
咱们已经在 Python 中认识了一些在任何强大的编程语言中都会出现的元素:程序员
数值是内建数据,算数运算是函数。github
嵌套函数提供了组合操做的手段。express
名称到值的绑定提供了有限的抽象手段。编程
如今咱们将要了解函数定义,一个更增强大的抽象技巧,名称经过它能够绑定到复合操做上,并能够做为一个单元来引用。框架
咱们经过如何表达“平方”这个概念来开始。咱们可能会说,“对一个数求平方就是将这个数乘上它本身”。在 Python 中就是:编程语言
>>> def square(x): return mul(x, x)
这定义了一个新的函数,并赋予了名称square
。这个用户定义的函数并不内建于解释器。它表示将一个数乘上本身的复合操做。定义中的x
叫作形式参数,它为被乘的东西提供一个名称。这个定义建立了用户定义的函数,而且将它关联到名称square
上。函数
函数定义包含def
语句,它标明了<name>
(名称)和一列带有名字的<formal parameters>
(形式参数)。以后,return
(返回)语句叫作函数体,指定了函数的<return expression>
(返回表达式),它是函数不管何时调用都须要求值的表达式。
def <name>(<formal parameters>): return <return expression>
第二行必须缩进!按照惯例咱们应该缩进四个空格,而不是一个Tab,返回表达式并非当即求值,它储存为新定义函数的一部分,而且只在函数最终调用时会被求出。(很快咱们就会看到缩进区域能够跨越多行。)
定义了square
以后,咱们使用调用表达式来调用它:
>>> square(21) 441 >>> square(add(2, 5)) 49 >>> square(square(3)) 81
咱们也能够在构建其它函数时,将square
用做构建块。列入,咱们能够轻易定义sum_squares
函数,它接受两个数值做为参数,并返回它们的平方和:
>>> def sum_squares(x, y): return add(square(x), square(y)) >>> sum_squares(3, 4) 25
用户定义的函数和内建函数以同种方法使用。确实,咱们不可能在sum_squares
的定义中分辨出square
是否构建于解释器中,从模块导入仍是由用户定义。
咱们的 Python 子集已经足够复杂了,但程序的含义还不是很是明显。若是形式参数和内建函数具备相同名称会如何呢?两个函数是否能共享名称而不会产生混乱呢?为了解决这些疑问,咱们必须详细描述环境。
表达式求值所在的环境由帧的序列组成,它们能够表述为一些盒子。每一帧都包含了一些绑定,它们将名称和对应的值关联起来。全局帧只有一个,它包含全部内建函数的名称绑定(只展现了abs
和max
)。咱们使用地球符号来表示全局。
赋值和导入语句会向当前环境的第一个帧添加条目。到目前为止,咱们的环境只包含全局帧。
>>> from math import pi >>> tau = 2 * pi
def
语句也将绑定绑定到由定义建立的函数上。定义square
以后的环境如图所示:
这些环境图示展现了当前环境中的绑定,以及它们所绑定的值(并非任何帧的一部分)。要注意函数名称是重复的,一个在帧中,另外一个是函数的一部分。这一重复是有意的,许多不一样的名字可能会引用相同函数,可是函数自己只有一个内在名称。可是,在环境中由名称检索值只检查名称绑定。函数的内在名称不在名称检索中起做用。在咱们以前看到的例子中:
>>> f = max >>> f <built-in function max>
名称max
是函数的内在名称,以及打印f
时咱们看到的名称。此外,名称max
和f
在全局环境中都绑定到了相同函数上。
在咱们介绍 Python 的附加特性时,咱们须要扩展这些图示。每次咱们这样作的时候,咱们都会列出图示能够表达的新特性。
新的环境特性:赋值和用户定义的函数定义。
为了求出运算符为用户定义函数的调用表达式,Python 解释器遵循与求出运算符为内建函数的表达式类似的过程。也就是说,解释器求出操做数表达式,而且对产生的实参调用具名函数。
调用用户定义的函数的行为引入了第二个局部帧,它只能由函数来访问。为了对一些实参调用用户定义的函数:
在新的局部帧中,将实参绑定到函数的形式参数上。
在当前帧的开头以及全局帧的末尾求出函数体。
函数体求值所在的环境由两个帧组成:第一个是局部帧,包含参数绑定,以后是全局帧,包含其它全部东西。每一个函数示例都有本身的独立局部帧。
这张图包含两个不一样的 Python 解释器层面:当前的环境,以及表达式树的一部分,它和要求值的代码的当前一行相关。咱们描述了调用表达式的求值,用户定义的函数(蓝色)表示为两部分的圆角矩形。点线箭头表示哪一个环境用于在每一个部分求解表达式。
上半部分展现了调用表达式的求值。这个调用表达式并不在任何函数里面,因此他在全局环境中求值。因此,任何里面的名称(例如square
)都会在全局帧中检索。
下半部分展现了square
函数的函数体。它的返回表达式在上面的步骤1引入的新环境中求值,它将square
的形式参数x
的名称绑定到实参的值-2
上。
环境中帧的顺序会影响由表达式中的名称检索返回的值。咱们以前说名称求解为当前环境中与这个名称关联的值。咱们如今能够更精确一些:
名称求解为当前环境中,最早发现该名称的帧中,绑定到这个名称的值。
咱们关于环境、名称和函数的概念框架创建了求值模型,虽然一些机制的细节仍旧没有指明(例如绑定如何实现),咱们的模型在描述解释器如何求解调用表示上,变得更准确和正确。在第三章咱们会看到这一模型如何用做一个蓝图来实现编程语言的可工做的解释器。
新的环境特性:函数调用。
让咱们再一次考虑两个简单的定义:
>>> from operator import add, mul >>> def square(x): return mul(x, x) >>> def sum_squares(x, y): return add(square(x), square(y))
以及求解下列调用表达式的过程:
>>> sum_squares(5, 12) 169
Python 首先会求出名称sum_squares
,它在全局帧绑定了用户定义的函数。基本的数字表达式 5 和 12 求值为它们所表达的数值。
以后,Python 调用了sum_squares
,它引入了局部帧,将x
绑定为 5,将y
绑定为 12。
这张图中,局部帧指向它的后继,全局帧。全部局部帧必须指向某个先导,这些连接定义了当前环境中的帧序列。
sum_square
的函数体包含下列调用表达式:
add ( square(x) , square(y) ) ________ _________ _________ "operator" "operand 0" "operand 1"
所有三个子表达式在当前环境中求值,它开始于标记为sum_squares
的帧。运算符字表达式add
是全局帧中发现的名称,绑定到了内建的加法函数上。两个操做数子表达式必须在加法函数调用以前依次求值。两个操做数都在当前环境中求值,开始于标记为sum_squares
的帧。在下面的环境图示中,咱们把这一帧叫作A
,而且将指向这一帧的箭头同时替换为标签A
。
在使用这个局部帧的状况下,函数体表达式mul(x, x)
求值为 25。
咱们的求值过程如今轮到了操做数 1,y
的值为 12。Python 再次求出square
的函数体。此次引入了另外一个局部环境帧,将x
绑定为 12。因此,操做数 1 求值为 144。
最后,对实参 25 和 144 调用加法会产生sum_squares
函数体的最终值:169。
这张图虽然复杂,可是用于展现咱们目前为止发展出的许多基础概念。名称绑定到值上面,它延伸到许多局部帧中,局部帧在惟一的全局帧之上,全局帧包含共享名称。表达式为树形结构,以及每次子表达式包含用户定义函数的调用时,环境必须被扩展。
全部这些机制的存在确保了名称在表达式中正确的地方解析为正确的值。这个例子展现了为何咱们的模型须要所引入的复杂性。全部三个局部帧都包含名称x
的绑定。可是这个名称在不一样的帧中绑定到了不一样的值上。局部帧分离了这些名称。
函数实现的细节之一是实现者对形式参数名称的选择不该影响函数行为。因此,下面的函数应具备相同的行为:
>>> def square(x): return mul(x, x) >>> def square(y): return mul(y, y)
这个原则 -- 也就是函数应不依赖于编写者选择的参数名称 -- 对编程语言来讲具备重要的结果。最简单的结果就是函数参数名称应保留在函数体的局部范围中。
若是参数不位于相应函数的局部范围中,square
的参数x
可能和sum_squares
中的参数x
产生混乱。严格来讲,这并非问题所在:不一样局部帧中的x
的绑定是不相关的。咱们的计算模型具备严谨的设计来确保这种独立性。
咱们说局部名称的做用域被限制在定义它的用户定义函数的函数体中。当一个名称不能再被访问时,它就离开了做用域。做用域的行为并非咱们模型的新事实,它是环境的工做方式的结果。
可修改的名称并不表明形式参数的名称彻底不重要。反之,选择良好的函数和参数名称对于函数定义的人类可解释性是必要的。
下面的准则派生于 Python 的代码风格指南,可被全部(非反叛)Python 程序员做为指南。一些共享的约定会使社区成员之间的沟通变得容易。遵循这些约定有一些反作用,我会发现你的代码在内部变得一致。
函数名称应该小写,如下划线分隔。提倡描述性的名称。
函数名称一般反映解释器向参数应用的操做(例如print
、add
、square
),或者结果(例如max
、abs
、sum
)。
参数名称应小写,如下划线分隔。提倡单个词的名称。
参数名称应该反映参数在函数中的做用,并不只仅是知足的值的类型。
看成用很是明确时,单个字母的参数名称能够接受,可是永远不要使用l
(小写的L
)和O
(大写的o
),或者I
(大写的i
)来避免和数字混淆。
周期性对你编写的程序复查这些准则,不用多久你的名称会变得十分 Python 化。
虽然sum_squares
十分简单,可是它演示了用户定义函数的最强大的特性。sum_squares
函数使用square
函数定义,可是仅仅依赖于square
定义在输入参数和输出值之间的关系。
咱们能够编写sum_squares
,而不用考虑如何计算一个数值的平方。平方计算的细节被隐藏了,并能够在以后考虑。确实,在sum_squares
看来,square
并非一个特定的函数体,而是某个函数的抽象,也就是所谓的函数式抽象。在这个层级的抽象中,任何能计算平方的函数都是等价的。
因此,仅仅考虑返回值的状况下,下面两个计算平方的函数是难以区分的。每一个都接受数值参数而且产生那个数的平方做为返回值。
>>> def square(x): return mul(x, x) >>> def square(x): return mul(x, x-1) + x
换句话说,函数定义应该可以隐藏细节。函数的用户可能不能本身编写函数,可是能够从其它程序员那里得到它做为“黑盒”。用户不该该须要知道如何实现来调用。Python 库拥有这个特性。许多开发者使用在这里定义的函数,可是不多有人看过它们的实现。实际上,许多 Python 库的实现并不彻底用 Python 编写,而是 C 语言。
算术运算符(例如+
和-
)在咱们的第一个例子中提供了组合手段。可是咱们还须要为包含这些运算符的表达式定义求值过程。
每一个带有中缀运算符的 Python 表达式都有本身的求值过程,可是你一般能够认为他们是调用表达式的快捷方式。当你看到
>>> 2 + 3 5
的时候,能够简单认为它是
>>> add(2, 3) 5
的快捷方式。
中缀记号能够嵌套,就像调用表达式那样。Python 运算符优先级中采用了常规的数学规则,它指导了如何解释带有多种运算符的复合表达式。
>>> 2 + 3 * 4 + 5 19
和下面的表达式的求值结果相同
>>> add(add(2, mul(3, 4)) , 5) 19
调用表达式的嵌套比运算符版本更加明显。Python 也容许括号括起来的子表达式,来覆盖一般的优先级规则,或者使表达式的嵌套结构更加明显:
>>> (2 + 3) * (4 + 5) 45
和下面的表达式的求值结果相同
>>> mul(add(2, 3), add(4, 5)) 45
你应该在你的程序中自由使用这些运算符和括号。对于简单的算术运算,Python 在惯例上倾向于运算符而不是调用表达式。