1.3 定义新函数

我们在Python中确定了一些在任何强大的编程语言中都必须出现的元素:

  1. 数字和算术运算是基本的内置数据值和函数。

  2. 嵌套函数应用程序提供了一种组合操作的方法。

  3. 将名称绑定到值提供了一种有限的抽象方法。

现在我们将学习函数定义,这是一种更强大的抽象技术,通过它可以将名称绑定到复合操作,然后将其作为一个单元来引用。 我们首先研究如何表示平方的概念。 我们可能会说,“要求平方,就把它自己乘起来。” 这在Python中表示为

>>> def square(x):
        return mul(x, x)

它定义了一个名为square的新函数。 这个用户定义的函数不是内置在解释器中。 它代表了一个数与它自身相乘的复合运算。 这个定义中的x被称为一个形式参数,它为要乘的东西提供了一个名称。 该定义创建了这个用户定义函数,并将其与名称square关联起来。

如何定义一个函数。 函数定义由一个def语句组成,该语句指示一个<name> 和一个以逗号分隔的命名的形式参数列表,然后是一个返回语句,称为函数体,它指定了返回表达式。 函数的表达式,这是一个在应用函数时要求值的表达式:

def <name>(<formal parameters>):
    return <return expression>

第二行必须缩进——大多数程序员使用四个空格缩进。返回表达式没有立即求值;它作为新定义函数的一部分存储,只有在最终应用函数时才计算。

定义了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是内置在解释器中、从模块中导入的,还是由用户定义的。

def语句和赋值语句都将名称绑定到值,任何现有的绑定都会丢失。例如,下面的g首先指向一个没有参数的函数,然后是一个数字,然后是一个有两个参数的不同函数。

>>> def g():
        return 1
>>> g()
1
>>> g = 2
>>> g
2
>>> def g(h, i):
        return h + i
>>> g(1, 2)
3

1.3.1 环境

现在,我们的Python子集已经足够复杂,程序的含义已经不那么明显了。 如果形式形参与内置函数名称相同怎么办? 两个函数可以共享名称而不产生混淆吗? 要解决这些问题,我们必须更详细地描述环境。

计算表达式的环境由一系列框架组成,框架被描述为盒子。 每个框架都包含绑定,每个绑定都将名称与其对应的值关联起来。 有一个单一的全局框架。 赋值和导入语句将条目添加到当前环境的第一个框架中。 到目前为止,我们的环境只包括全局框架。

此环境图显示了当前环境的绑定,以及名称绑定到的值。 本文中的环境图是交互式的:您可以通过左边的小程序来查看右边环境演变的状态。 您也可以点击“编辑在线Python导师中的代码”链接,将示例加载到在线Python导师中,这是Philip Guo创建的用于生成这些环境图的工具。 我们鼓励您自己创建示例,并学习生成的环境图。

函数也出现在环境图中。 import语句将一个名称绑定到一个内置函数。 def语句将名称绑定到由定义创建的用户定义函数。 导入mul和定义方形后的环境如下图所示。

每个函数都是一行,以func开头,然后是函数名和形式参数。 像mul这样的内置函数没有正式的参数名,所以… 总是用is代替。

函数的名称会重复两次,一次是在框架中,另一次是作为函数本身的一部分。 出现在函数中的名称为内在名称。 框架中的名称是一个绑定名称。 两者之间有一个区别:不同的名称可以指向同一个函数,但该函数本身只有一个内在名称。 框架中绑定到函数的名称是求值过程中使用的名称。

函数的内在名称在求值时不起作用。 使用Forward按钮逐步执行下面的示例,可以看到一旦名称max绑定到值3,它就不能再作为函数使用了。

错误消息TypeError: 'int' object is not callable是报告名称max(目前绑定到数字3)是一个整数而不是一个函数。 因此,它不能用作调用表达式中的操作符。

函数签名。 函数允许接受的实参数量不同。 为了跟踪这些需求,我们以一种显示函数名称及其形式参数的方式绘制每个函数。 用户定义的函数平方仅接受x; 提供更多或更少的参数将导致错误。 对函数的形式参数的描述称为函数的签名。

函数max可以接受任意数量的参数。 它被渲染为max(…)。 不管接受多少个参数,所有内置函数都将呈现为<name>(…),因为这些基本函数从未显式定义过。

1.3.2 调用自定义函数

为了计算运算符命名为自定义函数的调用表达式,Python解释器遵循一个计算过程。 与任何调用表达式一样,解释器计算运算符和操作数表达式,然后将命名函数应用到结果参数。

应用自定义函数会引入第二个本地框架,这个框架只能由该函数访问。 要将用户定义函数应用于某些参数:

  1. 将参数绑定到新的局部框架中函数的形式参数的名称。

  2. 在这个框架开始的环境中执行函数体。

评估主体的环境由两个框架组成:首先是包含形式参数绑定的局部框架,然后是包含其他所有内容的全局框架。 函数应用程序的每个实例都有自己独立的局部框架。 为了详细说明一个示例,描述了同一个示例的环境图的几个步骤。 执行第一个import语句后,只有名称mul被绑定在全局框架中。

首先,执行函数square的定义语句。注意,整个def语句在单个步骤中处理。函数体在调用函数之前不会执行(定义函数时不会执行)。

接下来,使用参数-2调用square函数,因此创建一个新的框架,将形式参数x绑定到值-2。

然后,在当前环境中查找名称x,该环境包含所示的两个框架。在这两种情况下,x的值都是-2,所以平方函数返回4。

square()框架中的“返回值”不是名称绑定; 相反,它指示创建框架的函数调用返回的值。

即使在这个简单的示例中,也使用了两个不同的环境。 顶级表达式square(-2)在全局环境中求值,而返回表达式mul(x, x)在调用square创建的环境中求值。 x和mul都在这个环境中绑定,但在不同的框架中。

环境中框架的顺序通过在表达式中查找名称来影响返回的值。 我们前面已经说过,在当前环境中,一个名称被计算为与该名称相关联的值。 现在我们可以更精确地描述:

名称计算。 名称的计算结果是在当前环境中找到该名称的最早的框架中绑定到该名称的值。

我们对环境、名称和功能的概念框架构成了一个评估模型;虽然一些机械细节仍未说明(例如,绑定是如何实现的),但我们的模型准确而正确地描述了解释器如何计算调用表达式。 在第三章中,我们将看到这个模型如何作为实现编程语言的工作解释器的蓝图。

1.3.3 例子:调用一个自定义函数

让我们再次考虑我们的两个简单函数定义,并说明对用户定义函数的调用表达式求值的过程。下图

Python首先计算名称sum_squares,它被绑定到全局框架中的用户定义函数。原始数字表达式5和12的计算结果是它们所表示的数字。

接下来,Python应用sum_squares,它引入了一个本地框架,将x绑定到5,将y绑定到12。

sum_squares函数体包含以下调用表达式:

  add     (  square(x)  ,  square(y)  )
________     _________     _________
operator     operand 0     operand 1

所有三个子表达式都在当前环境中计算,该环境以标记为sum_squares()的框架开始。 运算符子表达式add是在全局框架中找到的名称,绑定到内置的添加函数。 在应用加法之前,必须依次计算这两个操作数子表达式。 两个操作数都在当前环境中以标记为sum_squares的框架开始计算。

在操作数0中,square在全局框架中定义了一个用户定义的函数,而x在局部框架中定义了数字5。 Python通过引入另一个将x绑定到5的局部框架来将square应用到5。

在这种环境下,表达式mul(x, x)的值为25。

我们的求值过程现在转到操作数1,y将其命名为数字12。Python再次计算square的主体,这次引入了另一个将x绑定到12的局部框架。因此,操作数1的值为144。

最后,对参数25和144应用加法,会得到sum_squares的最终返回值:169。

这个例子说明了我们到目前为止所开发的许多基本思想。 名称绑定到值,这些值分布在许多独立的本地框架中,以及一个包含共享名称的全局框架中。 每次调用一个函数时都会引入一个新的本地框架,即使同一个函数被调用两次也是如此。

所有这些机制的存在都是为了确保名称在程序执行过程中在正确的时间解析为正确的值。 这个例子说明了为什么我们的模型需要我们引入的复杂性。 所有三个本地框架都包含名称x的绑定,但是这个名称在不同的框架中绑定到不同的值。 局部框架将这些名称分开。

1.3.4 本地名称

函数实现中不应该影响函数行为的一个细节是实现者对函数形参的名称的选择。因此,以下函数应该提供相同的行为:

>>> def square(x):
        return mul(x, x)
>>> def square(y):
        return mul(y, y)

这个原则——函数的意义应该独立于它的作者选择的参数名——对编程语言有重要的影响。 最简单的结果是,函数的形参名称必须保持在函数体的局部。

如果参数不是它们各自函数体的局部参数,那么square中的参数x可能与sum_squares中的参数x混淆。 关键的是,情况并非如此:x在不同的局部框架中的绑定是不相关的。 计算模型经过精心设计,以确保这种独立性。

我们说局部名称的作用域仅限于定义它的用户定义函数体。 当一个名称不再可访问时,它超出了作用域。 这种限定范围行为并不是我们模型中的新现象; 这是环境工作方式的结果。

1.3.5 选择名称

名称的可互换性并不意味着形式化参数名根本不重要。 相反,精心选择的函数和参数名对于函数定义的可解释性至关重要!

下面的指南改编自Python代码风格指南,它是所有(非叛逆的)Python程序员的指南。 一组共享的约定使开发人员社区成员之间的通信更加顺畅。 作为遵循这些约定的副作用,您将发现您的代码在内部变得更加一致。

  1. 函数名小写,单词之间用下划线分隔。 鼓励使用描述性名称。

  2. 函数名通常会唤起解释器应用于参数的操作(例如,print, add, square)或结果数量的名称(例如,max, abs, sum)。

  3. 参数名称小写,单词之间用下划线分隔。 最好使用单字名称。

  4. 形参名称应该与形参在函数中的作用相同,而不仅仅是允许的形参类型。

  5. 当参数的作用明显时,可以使用单字母参数名,但要避免使用“l”(小写字母)、“O”(大写字母oh)或“I”(大写字母I),以避免与数字混淆。

即使在Python标准库中,这些准则也有许多例外。 就像英语的词汇表一样,Python从各种贡献者那里继承了词汇,结果并不总是一致的。

1.3.6 作为抽象函数

虽然它非常简单,但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库具有此属性。 许多开发人员使用那里定义的函数,但很少有人检查它们的实现。

功能抽象的方面。 要掌握函数抽象的使用,考虑它的三个核心属性通常是有用的。 函数的定义域是它可以接受的一组参数。 函数的范围是它可以返回的值的集合。 函数的目的是计算输入和输出之间的关系(以及它可能产生的任何副作用)。 通过它们的领域、范围和意图来理解函数抽象对于在复杂程序中正确使用它们是至关重要的。

例如,我们用来实现sum_squares的任何square函数都应该具有这些属性:

  • 定义域是任意一个实数。

  • 值域是任意非负实数。

  • 目的是输出是输入的平方。

这些属性并没有指定意图是如何实现的;那个细节被抽象掉了。

1.3.7 操作符

数学运算符(例如+和-)提供了组合方法的第一个示例,但是我们还没有为包含这些运算符的表达式定义求值过程。

带有中缀操作符的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提供了两个中缀操作符:/和//。 前者是普通除法,因此即使除数将被除数平均除,也会得到一个浮点数或小数值:

>>> 5 / 4
1.25
>>> 8 / 4
2.0

另一方面,//运算符将结果舍入为整数:

>>> 5 // 4
1
>>> -5 // 4
-2

这两个操作符是truediv和floordiv函数的简写。

>>> from operator import truediv, floordiv
>>> truediv(5, 4)
1.25
>>> floordiv(5, 4)
1

您可以随意在程序中使用中缀操作符和圆括号。 对于简单的数学操作,惯用的Python更喜欢操作符而不是调用表达式。

Last updated

Was this helpful?