422 lines
14 KiB
Markdown
422 lines
14 KiB
Markdown
|
# 09 | 不可或缺的自定义函数
|
|||
|
|
|||
|
你好,我是景霄。
|
|||
|
|
|||
|
实际工作生活中,我曾见到不少初学者编写的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
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
其中:
|
|||
|
|
|||
|
* def是函数的声明;
|
|||
|
|
|||
|
* my\_func是函数的名称;
|
|||
|
|
|||
|
* 括号里面的message则是函数的参数;
|
|||
|
|
|||
|
* 而print那行则是函数的主体部分,可以执行相应的语句;
|
|||
|
|
|||
|
* 在函数最后,你可以返回调用结果(return或yield),也可以不返回。
|
|||
|
|
|||
|
|
|||
|
总结一下,大概是下面的这种形式:
|
|||
|
|
|||
|
```
|
|||
|
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 > 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 "str") 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 < 0:
|
|||
|
raise Exception('input must be greater or equal to 0' )
|
|||
|
...
|
|||
|
|
|||
|
def inner_factorial(input):
|
|||
|
if input <= 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 < MIN_VALUE or value > 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 = "local"
|
|||
|
def inner():
|
|||
|
nonlocal x # nonlocal关键字表示这里的x就是外部函数outer定义的变量x
|
|||
|
x = 'nonlocal'
|
|||
|
print("inner:", x)
|
|||
|
inner()
|
|||
|
print("outer:", x)
|
|||
|
outer()
|
|||
|
# 输出
|
|||
|
inner: nonlocal
|
|||
|
outer: nonlocal
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
如果不加上nonlocal这个关键字,而内部函数的变量又和外部函数变量同名,那么同样的,内部函数变量会覆盖外部函数的变量。
|
|||
|
|
|||
|
```
|
|||
|
def outer():
|
|||
|
x = "local"
|
|||
|
def inner():
|
|||
|
x = 'nonlocal' # 这里的x是inner这个函数的局部变量
|
|||
|
print("inner:", x)
|
|||
|
inner()
|
|||
|
print("outer:", 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
|
|||
|
# 输出
|
|||
|
<function __main__.nth_power.<locals>.exponent(base)>
|
|||
|
|
|||
|
cube
|
|||
|
# 输出
|
|||
|
<function __main__.nth_power.<locals>.exponent(base)>
|
|||
|
|
|||
|
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函数的概念及其应用,有这么几点你需要注意:
|
|||
|
|
|||
|
1. Python中函数的参数可以接受任意的数据类型,使用起来需要注意,必要时请在函数开头加入数据类型的检查;
|
|||
|
|
|||
|
2. 和其他语言不同,Python中函数的参数可以设定默认值;
|
|||
|
|
|||
|
3. 嵌套函数的使用,能保证数据的隐私性,提高程序运行效率;
|
|||
|
|
|||
|
4. 合理地使用闭包,则可以简化程序的复杂度,提高可读性。
|
|||
|
|
|||
|
|
|||
|
## 思考题
|
|||
|
|
|||
|
最后给你留一道思考题。在实际的学习工作中,你遇到过哪些使用嵌套函数或者是闭包的例子呢?欢迎在下方留言,与我讨论,也欢迎你把这篇文章分享给你的同事、朋友。
|
|||
|
|