7.md 10.2 KB
Newer Older
W
ch7.  
wizardforcel 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
# 


通过使用 Python 中已有的函数,我们正在建立一个使用的技术清单,用于识别数据集中的规律和主题。 现在我们将探索Python编程语言的核心功能:函数定义。

我们在本书中已经广泛使用了函数,但从未定义过我们自己的函数。定义一个函数的目的是,给一个计算过程命名,它可能会使用多次。计算中有许多需要重复计算的情况。 例如,我们常常希望对表的列中的每个值执行相同的操作。

## 定义函数

`double`函数的定义仅仅使一个数值加倍。

```py
# Our first function definition

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

我们通过编写`def`来开始定义任何函数。 下面是这个小函数的其他部分(语法)的细分:



当我们运行上面的单元格时,没有使特定的数字加倍,并且`double`主体中的代码还没有求值。因此,我们的函数类似于一个菜谱。 每次我们遵循菜谱中的指导,我们都需要以食材开始。 每次我们想用我们的函数来使一个数字加倍时,我们需要指定一个数字。

我们可以用和调用其他函数完全相同的方式,来调用`double`。 每次我们这样做的时候,主体中的代码都会执行,参数的值赋给了名称`x`

```py
double(17)
34
double(-0.6/4)
-0.3
```

以上两个表达式都是调用表达式。 在第二个里面,计算了表达式`-0.6 / 4`的值,然后将其作为参数`x`传递给`double`函数。 每个调用表达式最终都会执行`double`的主体,但使用不同的`x`值。

`double`的主体只有一行:

```py
return 2*x
```

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

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

```py
any_name = 42
double(any_name)
84
```

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

```py
double(make_array(3, 4, 5))
array([ 6,  8, 10])
```

但是,函数内部定义的名称(包括像double的x这样的参数)只存在一小会儿。 它们只在函数被调用的时候被定义,并且只能在函数体内被访问。 我们不能在`double`之外引用`x`。 技术术语是`x`具有局部作用域。

因此,即使我们在上面的单元格中调用了`double`,名称`x`也不能在函数体外识别。

```py
x
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-18-401b30e3b8b5> in <module>()
----> 1 x

NameError: name 'x' is not defined
```

文档字符串。 虽然`double`比较容易理解,但是很多函数执行复杂的任务,并且没有解释就很难使用。 (你自己也可能已经发现了!)因此,一个组成良好的函数有一个唤起它的行为的名字,以及文档。 在 Python 中,这被称为文档字符串 - 描述了它的行为和对其参数的预期。 文档字符串也可以展示函数的示例调用,其中调用前面是`>>>`

文档字符串可以是任何字符串,只要它是函数体中的第一个东西。 文档字符串通常在开始和结束处使用三个引号来定义,这允许字符串跨越多行。 第一行通常是函数的完整但简短的描述,而下面的行则为将来的用户提供了进一步的指导。

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

```py
# 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)
percent(33, 200)
16.5
```

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

```py
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。

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

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

```py
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
```

这就是当我们运行单元格时,所发生的事情。

## 多个参数

可以有多种方式来推广一个表达式或代码块,因此一个函数可以有多个参数,每个参数决定结果的不同方面。 例如,我们以前定义的百分比`percents`,每次都四舍五入到两位。 以下两个参的数定义允许不同调用四舍五入到不同的位数。

```py
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`

```py
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)`。 你定义的函数将始终首先使用函数名称,并传入所有参数来调用。

## 在列上应用函数

我们已经看到很多例子,通过将函数应用于现有列或其他数组,来创建新的表格的列。 所有这些函数都以数组作为参数。 但是我们经常打算,通过一个函数转换列中的条目,它不将数组作为它的函数。 例如,它可能只需要一个数字作为它的参数,就像下面定义的函数`cut_off_at_100`

```py
def cut_off_at_100(x):
    """The smaller of x and 100"""
    return min(x, 100)
cut_off_at_100(17)
17
cut_off_at_100(117)
100
cut_off_at_100(100)
100
```

如果参数小于或等于 100,函数`cut_off_at_100`只返回它的参数。但是如果参数大于 100,则返回 100。

在我们之前使用人口普查数据的例子中,我们看到变量`AGE`的值为 100,表示“100 岁以上”。 以这种方式将年龄限制在 100 岁,正是`cut_off_at_100`所做的。

为了一次性对很多年龄使用这个函数,我们必须能够引用函数本身,而不用实际调用它。 类似地,我们可能会向厨师展示一个蛋糕的菜谱,并要求她用它来烤 6 个蛋糕。 在这种情况下,我们不会使用这个配方自己烘烤蛋糕, 我们的角色只是把菜谱给厨师。 同样,我们可以要求一个表格,在列中的 6 个不同的数字上调用`cut_off_at_100`

首先,我们创建了一个表,一列是人,一列是它们的年龄。 例如,`C`是 52 岁。

```py
ages = Table().with_columns(
    'Person', make_array('A', 'B', 'C', 'D', 'E', 'F'),
    'Age', make_array(17, 117, 52, 100, 6, 101)
)
ages
```

| Person | Age |
| --- | --- |
| A | 17 |
| B | 117 |
| C | 52 |
| D | 100 |
| E | 6 |
| F | 101 |

### 应用

要在 100 岁截断年龄,我们将使用一个新的`Table`方法。 `apply`方法在列的每个元素上调用一个函数,形成一个返回值的新数组。 为了指出要调用的函数,只需将其命名(不带引号或括号)。 输入值的列的名称必须是字符串,仍然出现在引号内。

```py
ages.apply(cut_off_at_100, 'Age')
array([ 17, 100,  52, 100,   6, 100])
```

我们在这里所做的是,将`cut_off_at_100`函数应用于`age`表的`Age`列中的每个值。 输出是函数的相应返回值的数组。 例如,17 还是 17,117 变成了 100,52 还是 52,等等。

此数组的长度与`age`表中原始`Age`列的长度相同,可用作名为`Cut Off Age`的新列中的值,并与现有的`Person``Age`列共存。


```py
ages.with_column(
    'Cut Off Age', ages.apply(cut_off_at_100, 'Age')
)
```

| Person | Age | Cut Off Age |
| --- | --- | --- |
| A | 17 | 17 |
| B | 117 | 100 |
| C | 52 | 52 |
| D | 100 | 100 |
| E | 6 | 6 |
| F | 101 | 100 |

### 作为值的函数