<audio id="audio" title="09 | 不可或缺的自定义函数" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fa/8f/faa0b56c240a8f7b87424aab7a8f618f.mp3"></audio>

你好，我是景霄。

实际工作生活中，我曾见到不少初学者编写的Python程序，他们长达几百行的代码中，却没有一个函数，通通按顺序堆到一块儿，不仅让人读起来费时费力，往往也是错误连连。

一个规范的值得借鉴的Python程序，除非代码量很少（比如10行、20行以下），基本都应该由多个函数组成，这样的代码才更加模块化、规范化。

函数是Python程序中不可或缺的一部分。事实上，在前面的学习中，我们已经用到了很多Python的内置函数，比如sorted()表示对一个集合序列排序，len()表示返回一个集合序列的长度大小等等。这节课，我们主要来学习Python的自定义函数。

## 函数基础

那么，到底什么是函数，如何在Python程序中定义函数呢？

说白了，函数就是为了实现某一功能的代码段，只要写好以后，就可以重复利用。我们先来看下面一个简单的例子:

```
def my_func(message):
    print('Got a message: {}'.format(message))

# 调用函数 my_func()
my_func('Hello World')
# 输出
Got a message: Hello World

```

其中：

<li>
def是函数的声明；
</li>
<li>
my_func是函数的名称；
</li>
<li>
括号里面的message则是函数的参数；
</li>
<li>
而print那行则是函数的主体部分，可以执行相应的语句；
</li>
<li>
在函数最后，你可以返回调用结果（return或yield），也可以不返回。
</li>

总结一下，大概是下面的这种形式：

```
def name(param1, param2, ..., paramN):
    statements
    return/yield value # optional

```

和其他需要编译的语言（比如C语言）不一样的是，def是可执行语句，这意味着函数直到被调用前，都是不存在的。当程序调用函数时，def语句才会创建一个新的函数对象，并赋予其名字。

我们一起来看几个例子，加深你对函数的印象：

```
def my_sum(a, b):
    return a + b

result = my_sum(3, 5)
print(result)

# 输出
8

```

这里，我们定义了my_sum()这个函数，它有两个参数a和b，作用是相加；随后，调用my_sum()函数，分别把3和5赋于a和b；最后，返回其相加的值，赋于变量result，并输出得到8。

再来看一个例子：

```
def find_largest_element(l):
    if not isinstance(l, list):
        print('input is not type of list')
        return
    if len(l) == 0:
        print('empty input')
        return
    largest_element = l[0]
    for item in l:
        if item &gt; largest_element:
            largest_element = item
    print('largest element is: {}'.format(largest_element)) 
      
find_largest_element([8, 1,-3, 2, 0])

# 输出
largest element is: 8

```

这个例子中，我们定义了函数find_largest_element，作用是遍历输入的列表，找出最大的值并打印。因此，当我们调用它，并传递列表  [8, 1, -3, 2, 0]  作为参数时，程序就会输出 `largest element is: 8`。

需要注意，主程序调用函数时，必须保证这个函数此前已经定义过，不然就会报错，比如：

```
my_func('hello world')
def my_func(message):
    print('Got a message: {}'.format(message))
    
# 输出
NameError: name 'my_func' is not defined

```

但是，如果我们在函数内部调用其他函数，函数间哪个声明在前、哪个在后就无所谓，因为def是可执行语句，函数在调用之前都不存在，我们只需保证调用时，所需的函数都已经声明定义：

```
def my_func(message):
    my_sub_func(message) # 调用my_sub_func()在其声明之前不影响程序执行
    
def my_sub_func(message):
    print('Got a message: {}'.format(message))

my_func('hello world')

# 输出
Got a message: hello world

```

另外，Python函数的参数可以设定默认值，比如下面这样的写法：

```
def func(param = 0):
    ...

```

这样，在调用函数func()时，如果参数param没有传入，则参数默认为0；而如果传入了参数param，其就会覆盖默认值。

前面说过，Python和其他语言相比的一大特点是，Python是dynamically typed的，可以接受任何数据类型（整型，浮点，字符串等等）。对函数参数来说，这一点同样适用。比如还是刚刚的my_sum函数，我们也可以把列表作为参数来传递，表示将两个列表相连接：

```
print(my_sum([1, 2], [3, 4]))

# 输出
[1, 2, 3, 4]

```

同样，也可以把字符串作为参数传递，表示字符串的合并拼接：

```
print(my_sum('hello ', 'world'))

# 输出
hello world

```

当然，如果两个参数的数据类型不同，比如一个是列表、一个是字符串，两者无法相加，那就会报错：

```
print(my_sum([1, 2], 'hello'))
TypeError: can only concatenate list (not &quot;str&quot;) to list

```

我们可以看到，Python不用考虑输入的数据类型，而是将其交给具体的代码去判断执行，同样的一个函数（比如这边的相加函数my_sum()），可以同时应用在整型、列表、字符串等等的操作中。

在编程语言中，我们把这种行为称为**多态**。这也是Python和其他语言，比如Java、C等很大的一个不同点。当然，Python这种方便的特性，在实际使用中也会带来诸多问题。因此，必要时请你在开头加上数据的类型检查。

Python函数的另一大特性，是Python支持函数的嵌套。所谓的函数嵌套，就是指函数里面又有函数，比如：

```
def f1():
    print('hello')
    def f2():
        print('world')
    f2()
f1()

# 输出
hello
world

```

这里函数f1()的内部，又定义了函数f2()。在调用函数f1()时，会先打印字符串`'hello'`，然后f1()内部再调用f2()，打印字符串`'world'`。你也许会问，为什么需要函数嵌套？这样做有什么好处呢？

其实，函数的嵌套，主要有下面两个方面的作用。

第一，函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问，不会暴露在全局作用域，因此，如果你的函数内部有一些隐私数据（比如数据库的用户、密码等），不想暴露在外，那你就可以使用函数的的嵌套，将其封装在内部函数中，只通过外部函数来访问。比如：

```
def connect_DB():
    def get_DB_configuration():
        ...
        return host, username, password
    conn = connector.connect(get_DB_configuration())
    return conn

```

这里的函数get_DB_configuration，便是内部函数，它无法在connect_DB()函数以外被单独调用。也就是说，下面这样的外部直接调用是错误的：

```
get_DB_configuration()

# 输出
NameError: name 'get_DB_configuration' is not defined

```

我们只能通过调用外部函数connect_DB()来访问它，这样一来，程序的安全性便有了很大的提高。

第二，合理的使用函数嵌套，能够提高程序的运行效率。我们来看下面这个例子：

```
def factorial(input):
    # validation check
    if not isinstance(input, int):
        raise Exception('input must be an integer.')
    if input &lt; 0:
        raise Exception('input must be greater or equal to 0' )
    ...

    def inner_factorial(input):
        if input &lt;= 1:
            return 1
        return input * inner_factorial(input-1)
    return inner_factorial(input)


print(factorial(5))

```

这里，我们使用递归的方式计算一个数的阶乘。因为在计算之前，需要检查输入是否合法，所以我写成了函数嵌套的形式，这样一来，输入是否合法就只用检查一次。而如果我们不使用函数嵌套，那么每调用一次递归便会检查一次，这是没有必要的，也会降低程序的运行效率。

实际工作中，如果你遇到相似的情况，输入检查不是很快，还会耗费一定的资源，那么运用函数的嵌套就十分必要了。

## 函数变量作用域

Python函数中变量的作用域和其他语言类似。如果变量是在函数内部定义的，就称为局部变量，只在函数内部有效。一旦函数执行完毕，局部变量就会被回收，无法访问，比如下面的例子：

```
def read_text_from_file(file_path):
    with open(file_path) as file:
        ...

```

我们在函数内部定义了file这个变量，这个变量只在read_text_from_file这个函数里有效，在函数外部则无法访问。

相对应的，全局变量则是定义在整个文件层次上的，比如下面这段代码：

```
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    if value &lt; MIN_VALUE or value &gt; MAX_VALUE:
        raise Exception('validation check fails')

```

这里的MIN_VALUE和MAX_VALUE就是全局变量，可以在文件内的任何地方被访问，当然在函数内部也是可以的。不过，我们**不能在函数内部随意改变全局变量的值**。比如，下面的写法就是错误的：

```
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    ...
    MIN_VALUE += 1
    ...
validation_check(5)

```

如果运行这段代码，程序便会报错：

```
UnboundLocalError: local variable 'MIN_VALUE' referenced before assignment

```

这是因为，Python的解释器会默认函数内部的变量为局部变量，但是又发现局部变量MIN_VALUE并没有声明，因此就无法执行相关操作。所以，如果我们一定要在函数内部改变全局变量的值，就必须加上global这个声明：

```
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    global MIN_VALUE
    ...
    MIN_VALUE += 1
    ...
validation_check(5)

```

这里的global关键字，并不表示重新创建了一个全局变量MIN_VALUE，而是告诉Python解释器，函数内部的变量MIN_VALUE，就是之前定义的全局变量，并不是新的全局变量，也不是局部变量。这样，程序就可以在函数内部访问全局变量，并修改它的值了。

另外，如果遇到函数内部局部变量和全局变量同名的情况，那么在函数内部，局部变量会覆盖全局变量，比如下面这种：

```
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    MIN_VALUE = 3
    ...

```

在函数validation_check()内部，我们定义了和全局变量同名的局部变量MIN_VALUE，那么，MIN_VALUE在函数内部的值，就应该是3而不是1了。

类似的，对于嵌套函数来说，内部函数可以访问外部函数定义的变量，但是无法修改，若要修改，必须加上nonlocal这个关键字：

```
def outer():
    x = &quot;local&quot;
    def inner():
        nonlocal x # nonlocal关键字表示这里的x就是外部函数outer定义的变量x
        x = 'nonlocal'
        print(&quot;inner:&quot;, x)
    inner()
    print(&quot;outer:&quot;, x)
outer()
# 输出
inner: nonlocal
outer: nonlocal

```

如果不加上nonlocal这个关键字，而内部函数的变量又和外部函数变量同名，那么同样的，内部函数变量会覆盖外部函数的变量。

```
def outer():
    x = &quot;local&quot;
    def inner():
        x = 'nonlocal' # 这里的x是inner这个函数的局部变量
        print(&quot;inner:&quot;, x)
    inner()
    print(&quot;outer:&quot;, x)
outer()
# 输出
inner: nonlocal
outer: local

```

## 闭包

这节课的第三个重点，我想再来介绍一下闭包（closure）。闭包其实和刚刚讲的嵌套函数类似，不同的是，这里外部函数返回的是一个函数，而不是一个具体的值。返回的函数通常赋于一个变量，这个变量可以在后面被继续执行调用。

举个例子你就更容易理解了。比如，我们想计算一个数的n次幂，用闭包可以写成下面的代码：

```
def nth_power(exponent):
    def exponent_of(base):
        return base ** exponent
    return exponent_of # 返回值是exponent_of函数

square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方 
square
# 输出
&lt;function __main__.nth_power.&lt;locals&gt;.exponent(base)&gt;

cube
# 输出
&lt;function __main__.nth_power.&lt;locals&gt;.exponent(base)&gt;

print(square(2))  # 计算2的平方
print(cube(2)) # 计算2的立方
# 输出
4 # 2^2
8 # 2^3

```

这里外部函数nth_power()返回值，是函数exponent_of()，而不是一个具体的数值。需要注意的是，在执行完`square = nth_power(2)`和`cube = nth_power(3)`后，外部函数nth_power()的参数exponent，仍然会被内部函数exponent_of()记住。这样，之后我们调用square(2)或者cube(2)时，程序就能顺利地输出结果，而不会报错说参数exponent没有定义了。

看到这里，你也许会思考，为什么要闭包呢？上面的程序，我也可以写成下面的形式啊！

```
def nth_power_rewrite(base, exponent):
    return base ** exponent


```

确实可以，不过，要知道，使用闭包的一个原因，是让程序变得更简洁易读。设想一下，比如你需要计算很多个数的平方，那么你觉得写成下面哪一种形式更好呢？

```
# 不适用闭包
res1 = nth_power_rewrite(base1, 2)
res2 = nth_power_rewrite(base2, 2)
res3 = nth_power_rewrite(base3, 2)
...

# 使用闭包
square = nth_power(2)
res1 = square(base1)
res2 = square(base2)
res3 = square(base3)
...

```

显然是第二种，是不是？首先直观来看，第二种形式，让你每次调用函数都可以少输入一个参数，表达更为简洁。

其次，和上面讲到的嵌套函数优点类似，函数开头需要做一些额外工作，而你又需要多次调用这个函数时，将那些额外工作的代码放在外部函数，就可以减少多次调用导致的不必要的开销，提高程序的运行效率。

另外还有一点，我们后面会讲到，闭包常常和装饰器（decorator）一起使用。

## 总结

这节课，我们一起学习了Python函数的概念及其应用，有这么几点你需要注意:

<li>
Python中函数的参数可以接受任意的数据类型，使用起来需要注意，必要时请在函数开头加入数据类型的检查；
</li>
<li>
和其他语言不同，Python中函数的参数可以设定默认值；
</li>
<li>
嵌套函数的使用，能保证数据的隐私性，提高程序运行效率；
</li>
<li>
合理地使用闭包，则可以简化程序的复杂度，提高可读性。
</li>

## 思考题

最后给你留一道思考题。在实际的学习工作中，你遇到过哪些使用嵌套函数或者是闭包的例子呢？欢迎在下方留言，与我讨论，也欢迎你把这篇文章分享给你的同事、朋友。


