函数与表格

[In ]:
from datascience import *
import matplotlib
path_data = '../../assets/data/'
matplotlib.use('Agg')
%matplotlib inline
import matplotlib.pyplot as plots
plots.style.use('fivethirtyeight')
import numpy as np

我们正在积累使用 Python 中已有的函数来识别数据集中模式和主题的有用技术库。现在,我们将探索 Python 编程语言的一个核心特性:函数定义。

我们已经在本书中大量使用了函数,但从未定义过自己的函数。定义函数的目的是为可能多次应用的计算过程赋予一个名称。在计算中有许多需要重复计算的情况。例如,我们经常想对表格列中的每个值执行相同的操作。

定义一个函数

下面 double 函数的定义简单的将一个数字加倍。

[In ]:
# Our first function definition

def double(x):
    """ Double x """
    return 2*x

我们通过写 def 来开始任何函数定义。以下是这个小函数其他部分(“语法”)的分解说明:

函数的第一行是 "def double(x)"。"double(x)" 被标注为签名。"double" 是函数名,"x" 是第一个(也是唯一的)参数。下一行是缩进的,显示三个双引号包围着 "Double x"。这被标注为文档字符串或 docstring,描述了函数的功能。接下来的一行是 "return 2*x",被标注为函数体。函数体是每次调用函数时运行的所有代码。Return 告诉 Python 每次调用的值是什么:它是 return 后面表达式的值。最后,docstring 和函数体的缩进被突出显示。这种缩进告诉 Python 这些行是函数体的一部分。

当我们运行上面的单元格时,并没有某个特定的数字被加倍,double 函数体内部的代码还没有被执行。在这方面,我们的函数类似于一个“食谱”。每次我们按照食谱中的说明操作,都需要从原料开始。每次我们想用函数来加倍一个数字,都需要指定一个数字。

我们可以用与其他函数完全相同的方式调用 double。每次我们这样做时,函数体中的代码就会执行,参数的值被赋予名称 x

[In ]:
double(17)
34
[In ]:
double(-0.6/4)
-0.3

上面的两个表达式都是“调用表达式”。在第二个表达式中,首先计算出表达式 -0.6/4 的值,然后作为名为 x 的参数传递给 double 函数。每个调用表达式都会导致 double 的函数体被执行,但使用不同的 x 值。

double 的函数体只有一行:

return 2*x

执行这个“return 语句”会完成 double 函数体的执行并计算出调用表达式的值。

double 的参数可以是任何表达式,只要其值为数字。例如,它可以是一个名称。double 函数不知道也不关心其参数是如何计算或存储的;它唯一的工作是使用传递给它的参数值来执行自己的函数体。

[In ]:
any_name = 42
double(any_name)
84

参数也可以是任何可以被加倍的数值。例如,整个数字数组可以作为参数传递给 double,结果将是另一个数组。

[In ]:
double(make_array(3, 4, 5))
array([ 6,  8, 10])

然而,在函数内部定义的名称,包括像 doublex 这样的参数,只具有短暂的存续期。它们只在函数被调用时被定义,并且只能在函数体内部访问。我们不能在 double 的函数体外部引用 x。技术术语是 x 具有“局部作用域”。

因此,即使在上面的单元格中调用了 double,名称 x 在函数体外部也无法被识别。

[In ]:
x
NameError: name 'x' is not defined
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-7-6fcf9dfbd479> in <module>
----> 1 x

NameError: name 'x' is not defined

文档字符串。 虽然 double 相对容易理解,但许多函数执行复杂的任务,如果没有解释就很难使用。(你可能已经自己发现了这一点!)因此,一个编写良好的函数有一个能唤起其行为的名称,以及文档。在 Python 中,这被称为“文档字符串”——关于其行为及其参数预期的描述。文档字符串还可以展示函数的示例调用,其中调用前面有 >>>

文档字符串可以是任何字符串,只要它是函数体中的第一个内容。文档字符串通常使用开头和结尾的三引号定义,这使得字符串可以跨越多行。第一行按照惯例是函数的一个完整但简短的描述,而后续行则为函数的未来用户提供进一步的指导。

下面是名为 percent 的函数的定义,它接受两个参数。该定义包括一个文档字符串。

[In ]:
# A function with more than one argument

def percent(x, total):
    """Convert x to a percentage of total.
    
    More precisely, this function divides x by total,
    multiplies the result by 100, and rounds the result
    to two decimal places.
    
    >>> percent(4, 16)
    25.0
    >>> percent(1, 6)
    16.67
    """
    return round((x/total)*100, 2)
[In ]:
percent(33, 200)
16.5

将上面定义的函数 percent 与下面定义的函数 percents 进行对比。后者以一个数组作为参数,并将数组中的所有数字转换为占数组总值百分比的形式。所有百分比都四舍五入到两位小数,这次用 np.round 代替了 round,因为参数是一个数组而不是一个数字。

[In ]:
def percents(counts):
    """Convert the values in array_x to percents out of the total of array_x."""
    total = counts.sum()
    return np.round((counts/total)*100, 2)

函数 percents 返回一个百分比数组,除了四舍五入的误差外,这些百分比加起来等于100。

[In ]:
some_array = make_array(7, 10, 4)
percents(some_array)
array([33.33, 47.62, 19.05])

理解 Python 执行函数所采取的步骤是有帮助的。为了方便理解,我们在下面的同一个单元格中放入了函数定义和对该函数的调用。

[In ]:
def biggest_difference(array_x):
    """Find the biggest difference in absolute value between two adjacent elements of array_x."""
    diffs = np.diff(array_x)
    absolute_diffs = abs(diffs)
    return max(absolute_diffs)

some_numbers = make_array(2, 4, 5, 6, 4, -1, 1)
big_diff = biggest_difference(some_numbers)
print("The biggest difference is", big_diff)
The biggest difference is 5

以下是运行该单元格时发生的情况:

整体布局包括左侧重复出现的相同代码片段、带有指向特定代码部分的标注线的描述性文本框,以及右侧的“作用域中定义的名称”部分,该部分显示了不同阶段的变量状态。代码片段首先定义了 biggest_difference 函数,该函数接受 array_x 作为输入,并具有以下docstring:‘Find the biggest difference in absolute value between two adjacent elements of array_x.’ 第一行代码注释是‘The function biggest_difference is defined.’ 代码的下一次重复在函数结束后的行上标注‘The array some_numbers is defined.’ 并且在“作用域中定义的名称”下现在是‘biggest_difference = <a function>’。下一个注释针对下一行‘big_diff = biggest_difference(some_numbers)’,标注为‘Our function is called. Before this line finishes, Python executes its body.’ some_numbers 已被添加到“作用域中定义的名称”中。下一个注释回到函数体并说明‘The argument is given name array_x. The function’s first line (docstring) does nothing.’ 我们看到 array_x 现在具有 some_numbers 的值。docstring 之后的下一行代码是‘diffs = np.diff(array_x)’,注释为‘The array diffs is defined.’ 下一行代码是‘absolute_diffs = abs(diffs)’,注释为‘The array absolute_diffs is defined.’ 我们看到“作用域中定义的名称”中的‘diffs’ 及其在数组中填入的计算值。下一行代码是’return max(absolute_diffs)’,注释为‘The value of max(absolute_diffs) is computed and becomes the value of the call biggest_difference(some_numbers)’,并且‘absolute_diffs’ 被添加到“作用域中定义的名称”中。最后,注释跳回到函数调用之后的行,标注为‘The function call is done, so array_x, diffs, and absolute_diffs are no longer available.’ 在“作用域中定义的名称”下,这些已被移除。图的最后一部分显示代码‘big_diff’,这是函数调用后唯一的代码,注释为‘The value of big_diff has been assigned from the return value.’

多个参数

一个表达式或代码块可以有多种泛化方式,因此一个函数可以接受多个参数,每个参数决定结果的不同方面。例如,我们之前定义的 percents 函数每次都将数值四舍五入到两位小数。下面的双参数定义允许不同的调用四舍五入到不同的小数位数。

[In ]:
def percents(counts, decimal_places):
    """Convert the values in array_x to percents out of the total of array_x."""
    total = counts.sum()
    return np.round((counts/total)*100, decimal_places)

parts = make_array(2, 1, 4)
print("Rounded to 1 decimal place: ", percents(parts, 1))
print("Rounded to 2 decimal places:", percents(parts, 2))
print("Rounded to 3 decimal places:", percents(parts, 3))
Rounded to 1 decimal place:  [28.6 14.3 57.1]
Rounded to 2 decimal places: [28.57 14.29 57.14]
Rounded to 3 decimal places: [28.571 14.286 57.143]

这个新定义的灵活性有一个小代价:每次调用函数时都必须指定小数位数。默认参数值允许函数以可变数量的参数被调用;任何在调用表达式中未指定的参数都会获得其默认值,该默认值在 def 语句的第一行中声明。例如,在 percents 的最终定义中,可选参数 decimal_places 的默认值为2。

[In ]:
def percents(counts, decimal_places=2):
    """Convert the values in array_x to percents out of the total of array_x."""
    total = counts.sum()
    return np.round((counts/total)*100, decimal_places)

parts = make_array(2, 1, 4)
print("Rounded to 1 decimal place:", percents(parts, 1))
print("Rounded to the default number of decimal places:", percents(parts))
Rounded to 1 decimal place: [28.6 14.3 57.1]
Rounded to the default number of decimal places: [28.57 14.29 57.14]

注意:方法

函数通过在函数名后的括号中放入参数表达式来调用。任何独立定义的函数都以这种方式调用。你还见过方法的例子,它类似于函数,但使用点号表示法调用,例如 some_table.sort(some_label)。你定义的函数将始终以函数名在前的方式调用,传入所有参数。