Python Learning Chapter 2
Functions
Definitions
一个函数的定义通常如下:
1 | def <name>(<formal parameters>): |
开头为一个 def 语句, 然后是函数的名, 后面跟着 (), () 里是若干个用 , 隔开的形参. 下面是函数要进行的操作, 最后是一个 return 语句(可有可无)用于返回值.
函数的定义很简单, 接下来我们看一点抽象的东西.
我们知道, 给一个变量赋值用的是 =, 定义一个函数用的是 def, 无论是 = 还是 def, 其本质都是为一个名赋予含义, 换句话说, def 语句本质上也是一个赋值语句.
所以很多时候, 我们把函数的定义当作是对名的赋值就会好理解很多.
Functions as Arguments
观察以下几个函数:
1 | def sum_naturals(n): |
我们可以发现以上几个函数都可以写成如下格式:
1 | def <name>(n): |
只有函数名 <name> 和参数的计算方式 <term> 不同.
我们在上一章提到过, python 中不分函数名或是变量名, 而是统一称为名(name), 既然这样, 我们在函数中传参的过程中, 也可以通过名的形式来传入一个函数, 即上面的 <term>.
也就是说, 我们可以把函数作为一个参数传入函数.
故函数 sum_cubes 的定义可以改写为如下形式:
1 | def summation(n, term): |
只考虑计算立方和这一项功能的话, 把一个函数拆成这样三个函数来写可能有些不合适, 但是当我们把所有的可以用该格式概括的(前 n 项求和)函数都写成这样的形式, 那我们的代码复用性就大大的提高了, 这种模块化的思想是编程的哲学, 也是编程的艺术, 既提高了代码的复用性, 也提高了代码的美观程度.
Nested Definitions
函数的嵌套定义(Nested Definnitions), 其实就是在一个函数内定义函数, 在讲嵌套定义之前, 我们先回忆一下之前说过的, 把 def 语句理解为一个赋值语句. 理解了这个, 我们再理解函数的嵌套定义就很简单了.
1 | def f(x): |
在上面的代码块中, 我们在函数 f 里定义了一个函数 g, 如果我们按照 C/C++ 里的方式来理解的话, 这可能会有些奇怪, 但是当你用 python 中名的视角来看, 定义一个函数跟定义一个变量没什么两样, 在这里定义一个函数无异于定义了一个函数类型的名.
所以, 函数的嵌套定义, 我们可以把它理解为在函数的作用域内定义了一个名, 只不过这个名被赋予了函数的意义.
Functions as Returned Values
前面说完了函数作为参数传入, 接下来是函数作为返回值传出.
这里的函数作为返回值真的是返回了一个函数, 与函数的递归调用不同, 这里的返回一个函数, 是真的把函数作为返回值.
举个例子:
1 | def compose1(f, g): |
函数 compose1 把两个只有一个参数的函数 f(x) 和 g(x) 合并成一个函数 h(x).
我们可以通过如下方式调用:
1 | h = compose1(f, g) |
是不是很简单, 其实就是返回值的类型为函数类型罢了.
不知道看到这里的你有没有个疑惑:
为什么不直接在 Functions as Arguments 后面讲 Functions as Returned Values
其实我刚才也有这个疑惑, 甚至我一开始还是把这两部分连着的, 把嵌套定义放在后面(当然CS61A中并没有这样), 在写嵌套定义那一部分的时候我突然发现, 函数作为返回值的时候, 如果不用嵌套定义, 那么我们的函数就会长成下面这样子:
1 | def f(x): |
我们就只能返回一个函数本身或者一个其他函数, 但是这样的功能我们本可以直接用一个 = 来实现, 写的这么麻烦实在很不符合一个优秀程序员的作风.
为了让函数类型的返回值更有意义, 我就把这一节放在了函数的嵌套定义之后, 想必 CS61A 课程安排成这样大概也是如此吧.
Currying
加里化(Currying), 这真是个有趣的东西, 因为我一开始并不知道这是什么玩意, 于是我就翻译了一下, 翻译结果是”咖喱ing”, 我的大脑突然一片空白. 不过这个名字并不重要, 重要的是加里化这个操作.
We can use higher-order functions to convert a function that takes multiple arguments into a chain of functions that each take a single argument.
上面是对加里化的一个简单的解释, 把一个形似 f(a, b, c...) 的函数转化成形似 g(a)(b)(c)... 的形式, 这个操作就是加里化.
举个例子:
1 | def pow(x, y): |
这是一个用于计算 $x^y$ 的函数, 我们对其加里化:
1 | def curried_pow(x): |
curried_pow(x)(y) 的结果与 pow(x, y) 完全相同, 除此之外, curried_pow(x) 也可以作为单独的一个函数使用, 把 x 作为一个常量, 例如 curried_pow(2)(x) 就可以用于计算 $2^x$ .
只是说到这里, 还不足以体现出加里化的作用和优点, 加里化的一个很重要的作用就是可以把一个多参数的函数当作一个单参数的函数来使用, 举个例子:
1 | def map_to_range(start, end, f): |
这个函数用于输出 $f(n), n \in [start, end)$, 显然函数 f 只有一个参数, 所以我们就可以通过对函数 pow 加里化来获取单参数函数, 并且可以以参数的形式任意修改其他参数, 也就是说, 我们只需要对 pow(x, y) 进行加里化, 就可以得到所有的用于计算 $a^x$ 的函数, 此处 $a$ 为常数. 我们就不需要用一个函数就定义一个, 只需要在调用或是传入 curried_pow(x) 时修改 x 的值就可以了.
Lambda Expressions
目前来看, 我们每次需要用一个函数时, 都需要一个名, 也就是我们都是通过一个名来调用函数.
lambda 表达式的出现改变了这一情况. 通过 lambda 表达式, 我们可以把上面的 compose1 函数写成如下形式:
1 | def compose1(f, g): |
这与上面的定义是等价的. lambda 表达式的格式如下:
1 | lambda x : f(g(x)) |
lambda 表达式的值是一个函数, 称为 lambda 函数. 有了 lambda 表达式, 我们就可以换一种方式来对函数进行定义了:
1 | f = lambda x : x * x |
这样就定义了一个函数 f(x) 用于计算 $x^2$. 通过 lambda 表达式的方式定义函数会很简短, 在某些情况下非常好用. 但需要注意的是, lambda 表达式只能用于简单的函数定义(仅包含一个表达式), 而不能含有赋值语句或控制语句. 如果过分使用 lambda 表达式也会造成代码可读性下降等问题.
所以凡事都讲究一个中庸之道, 过犹不及. 对 lambda 表达式抑或是其他特性都是如此.
Function Decorators
函数装饰器(Function Decorators), 正如其名, 是一个用于”装饰”函数的东西, 符号 @ 称为装饰器(Decorator), 我们可以通过它来调用装饰函数来对目标函数进行装饰.
1 | def trace(fn): |
这是一个用于追踪函数的函数, 可以输出目标函数的信息及其传入的参数, 并且没有修改其返回值. 我们通过以下方式来定义一个修饰过后的函数:
1 |
|
当然我们也可以先定义该函数, 再对其进行修饰:
1 | def triple(x): |
这两种方式是等价的, 只不过后者就不需要用到装饰器了.
这里只是简要的介绍了函数装饰器, 就像 CS61A 中讲的那样, 并没有对其进行深入的探讨和研究, 也没有介绍其应用范围.