Python 中的函数

程序语言中的,编写代码要不断追求简洁和易读。换句话说,我们要尽量避免写重复的代码,少复制粘贴,也就是所谓的 DRY 原则(Don’t Repeat Yourself)。而 函数 的作用就是用来 提高程序的模块性,和代码的重复利用率。接下来我们详细解读 Python 中函数的使用。

引言 >>>>

事实上,前面我们在介绍 Python 基本数据结构时,就已经学习过 Python 中提供的许多内建(内置)函数:

1
# input() print() len() type() id() str() ...

对于 Python 支持的内建函数,可以直接从 Python 的官方网站查看更详细的文档说明 –> Python 内置函数(v3.10.3)


当然,根据我们的任务需要自由、灵活、快速的定制我们自己的函数(自定义函数)也是必要的!!!

Python 支持自定义函数(Function),即将一段有规律的、可重复使用的代码封装成函数,从而达到一次编写、多次调用的目的。

不难理解,函数的本质 >>>>

是一段有特定功能、可以重复使用的代码,这段代码已经被提前编写好了,并且为其起一个“好听”的名字。在后续编写程序过程中,如果需要同样的功能,直接通过起好的名字就可以调用这段代码。


那么,Python 中如何定义一个函数呢 ↓↓↓↓↓

函数定义

当我们需要定制一个特定功能的函数时,Python 中需要用 def 关键字实现,具体的语法格式如下:

1
2
3
4
def 函数名 (参数列表):
''' 函数说明文档:函数功能说明 '''
函数体
[return [返回值]]

此格式中,各部分参数的含义如下:

  • 函数名:一个符合 Python 语法的标识符,但不建议使用 a、b、c 这类简单的标识符作为函数名,函数名最好能够体现出该函数的功能;
  • 形参列表:设置该函数可以接收多少个参数,多个参数之间用逗号( , )分隔;
  • 函数说明文档:函数的第一行语句可以选择性地使用文档字符串用于函数功能说明,非必须(推荐);
  • 函数体:实现特定功能的多行代码;
  • return [返回值]:返回可选,可返回值也可返回表达式,函数一旦执行到 return 时,就执行完毕,并将表达式结果返回。如果没有 return 语句或不带表达式的 return,结果相都当于返回 None

注意,在创建函数时,即使函数不需要参数,也必须保留一对空的 (),否则 Python 解释器将提示 invaild syntax 错误。

给出一个用于计算矩形面积的函数定义实例:

1
2
3
4
def area(width, height):
""" Fun: Calculate the area of ​​a rectangle """
print("width: %d, height: %d" % (width, height))
return width * height

空函数沙发

要实现一个功能,我还没想好怎么做,先占个沙发以待后续补充开发(借助 pass 函数):

1
2
3
def nop():
#TODO balabala
pass

这样可以保证代码结构完整性,让代码能运行起来。如果不加会报错:

1
2
3
4
5
6
>>> def nop():
...
...
File "<stdin>", line 3
^
IndentationError: expected an indented block

函数文档说明支持

函数的说明文档,本质就是放于函数内的一段字符串注释。

只不过作为说明文档,字符串的放置位置是有讲究的,函数的说明文档通常位于函数内部、所有代码的最前面 –> 见函数语法规则中的函数说明文档。

这样的话,你可以通过 Python 的 help() 内置函数或者 funName.__doc__ 进行查看:

1
2
3
4
5
6
7
def area(width, height):
""" Fun: Calculate the area of ​​a rectangle """
print("width: %d, height: %d" % (width, height))
return width * height

help(area)
print(area.__doc__)

输出如下信息:

1
2
3
4
5
6
Help on function area in module __main__:

area(width, height)
Fun: Calculate the area of ​​a rectangle

Fun: Calculate the area of ​​a rectangle

函数调用

不管是 Python 中提供的内建函数,亦或是上面自定义的函数,只是将特定的功能代码封装了起来,一切都是为了等待调用的。

这就像是神奇宝贝里的精灵球安静地待着,只有听见你的召唤时才会出场,为你所用。

Python 中函数的调用规则都是一样,要想调用一个函数,需要知道函数的名称和需要的参数(出来吧,皮卡丘~):

1
[返回值] = 函数名([形参值])

其中,函数名即指的是要调用的函数的名称;形参值指的是当初创建函数时要求传入的各个形参的值。如果该函数有返回值,我们可以通过一个变量来接收该值,当然也可以不接受。

以上面定义好的面积函数来说明 Python 中的函数调用:

1
2
3
4
5
6
7
8
9
10
11
graph_width = 10
graph_height = 5

graph_area = area(graph_width, graph_height)
print("The area of graph is ",graph_area)
# Output: The area of graph is 50

# 既可以传递数值,也可以将变量作为参数进行传递
area1 = area(10,50)
print(area1)
# Output: 500

需要注意的是,创建函数有多少个形参,那么调用时就需要传入多少个值;并且默认情况下,调用函数时传入的参数(实参列表)和参数列表(形参列表)会按函数声明中定义的顺序匹配起来(故,形参也被称为 位置参数)。即便该函数没有参数,函数名后的小括号也不能省略。


深入理解函数名称

以我们熟悉的, Python 内置的求绝对值的函数 abs() 为例:

1
2
>>> abs(-10)
10

你需要使用 abs() 的形式进行函数调用。但你想过没有,如果只写 abs 呢?

1
2
>>> abs
<built-in function abs>

输出信息显示:abs 是一个内置函数。可见,abs() 是函数调用,而 abs 是函数本身。

[1] >>>> 函数别名

由变量可以指向函数调用的返回值,很自然的可以想到,如果把函数本身(函数名)赋值给变量是什么情况?

1
2
3
>>> fun_test = abs
>>> fun_test
<built-in function abs>

哎!把函数名赋值给变量后(变量指向函数本身),好像给这个函数起了一个 “别名”

同理的话,验证一下 >>>> 是否可以通过该变量来调用这个函数

1
2
3
>>> fun_test = abs
>>> fun_test(-10)
10

成功!直接调用 abs() 函数和调用 fun_test() 完全相同。突然又有了个想法:

1
2
3
4
5
>>> id(fun_test)
3038282253312
>>> id(abs)
3038282253312
>>> abs

哦哦~~~经过小心验证,你可以大胆猜测了 >>>> 函数名其实就是指向一个函数对象的引用

[2] >>>> 函数名是指向函数对象的一个引用

所以,上面完全可以把函数名赋给一个变量,让它们指向同一个函数对象引用,这相当于给这个函数起了一个“别名”,但一般禁止这么使用,容易混淆。

又来了个奇怪的想法,既然函数名是一个引用,不就相当于一个变量吗?如果把 abs 指向其他对象,会有什么情况发生?

1
2
3
4
5
6
7
8
>>> abs = "test"
>>> abs(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object is not callable

>>> abs
'test'

果然,和猜想的差不多!abs 已经不再是指向求绝对值函数对象的引用了,而是变为一个普通的变量了!

当然实际代码绝对不能这么写,这里是为了对比函数调用(abs())和函数名(abs)的区别。要恢复 abs 函数,请重启 Python 交互环境。


形参(位置参数) && 实参

函数调用中有提到,实参列表、形参列表,以及位置参数的说法,到底怎么区分:

形式参数(形参) >>>> 定义函数时,函数名后面括号中的参数就是形式参数(形式上要求的)。

实际参数(实参) >>>> 调用函数时,函数名后面括号中的参数就是形式参数(实际使用时传入的)。

调用函数时,传入的实参列表和函数的形参列表,会按函数声明中定义的顺序匹配起来,故形参也被称为位置参数。


参数检查机制

函数调用时,Python 会自动检查传入参数数量是否正确?传入参数类型是否正确?你需要检查传入参数的顺序是否和形成一致?

[1] >>>> 参数个数检查

调用函数时,必须保证实参和形参数量必须一致。如果参数个数不对,Python 解释器会自动检查出来,并抛出TypeError

1
2
3
4
5
6
7
8
9
>>> area(2,3,5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: area() takes 2 positional arguments but 3 were given

>>> area(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: area() missing 1 required positional argument: 'height'

可以看到,提示你多出/缺少必要的位置参数。

[2] >>>> 参数位置检查

调用函数时,必须确保实参位置和形式位置顺序一一对应,否则会产生以下 2 种结果:

1)–> 位置一致,但传入实参类型和形参不匹配,抛出 TypeError

1
2
3
4
5
6
7
8
9
>>> area("Google", 12)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in area
TypeError: %d format: a number is required, not str
>>> abs("A")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'

2)–> 传入实参类型和形参类型一致,位置不一致,导致函数计算结果和预期不符

这是由于实参和形参会函数声明中定义的顺序进行匹配,此时实参类型和形参类型一致,无法检查出错误,但会导致运行结果和预期不符:

例如,设计一个求梯形面积的函数,并利用此函数求上底为 4cm,下底为 3cm,高为 5cm 的梯形的面积。但如果交互高和下低参数的传入位置,计算结果将导致错误:

1
2
3
4
def area(upper_base,lower_bottom,height):
return (upper_base + lower_bottom)*height/2
print("正确结果为:", area(4,3,5))
print("错误结果为:", area(4,5,3))

也就是说,参数个数以及参数类型 Python 解释器自动会帮你检查,但参数位置需要你自己确保!!!

[3] >>>> 自定义函数参数类型检查

自定义函数时,可以设置对传入参数的类型进行强制检查,只允许符合我们要求的参数传入,这可以防止其他人调用我们自定义的函数时引发类型异常,增强代码的健壮性。

假如只允许整数的参数。数据类型检查可以用内置函数 isinstance() 实现:

1
2
3
4
5
6
7
>>> def area(width, height):
... if not isinstance(width, int) or not isinstance(height,int):
... raise TypeError("bad operand type for area(int,int)")
... print("width: %d, height: %d" % (width, height))
... return width * height
...
>>>

添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:

1
2
3
4
5
>>> area(12.0,10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in area
TypeError: bad operand type for area(intint)

关于错误和异常处理将在后续讲到,这里不用深究。


参数传递机制

大多数的编程语言中,说到函数中的参数传递,就会谈到值传递 && 引用传递,关于 Python 中的参数传递机制也是众说纷纭,但主流有两种想法:

[1] >>>> 值传递和引用(地址)传递

  • 值传递:适用于实参类型为不可变类型(字符串、数字、元组);
  • 引用(地址)传递:适用于实参类型为可变类型(列表,字典)。

值传递和引用传递理解和 C/C++、Java 类似,函数参数进行值传递后,若形参的值发生改变,不会影响实参的值;而函数参数继续引用传递后,改变形参的值,实参的值也会一同改变。

[2] >>>> 引用传递(个人理解)

基本数据类型时就提过,Python 中一切皆对象,变量是没有类型的,类型属于对象(变量所指向的内存数据)。例如:

1
2
test = [1,2,3]
test = "HelloPython"

以上代码中,[1,2,3] 是 List 类型,"HelloPython" 是 String 类型,而变量 test 是没有类型,它仅仅是一个对象的引用(一个指针),可以是指向 List 类型对象,也可以是指向 String 类型对象。

所以,变量参数传递时,就是引用的传递,不同的是,由于指向内存单元数据对象类型(可变与不可变对象)的不同导致不同的结果:

  • 可变对象:由于引用指向列表,字典,集合等可变对象,故改变形参的值,实参的值也会一同改变;

  • 不可变对象:由于引用指向字符串、数字、元组等可变对象,故改变形参的值,实参的值不受影响。

实例印证:

传递不可变对象实例 >>>>

1
2
3
4
5
6
7
>>> def ChangeInt(test):
... test = 10
...
>>> test = 2
>>> ChangeInt(test)
>>> print(test)
2

传递可变对象实例 >>>>

1
2
3
4
5
6
7
8
9
10
11
>>> def changeme( mylist ):
... "修改传入的列表"
... mylist.append([1,2,3,4])
... print ("函数内取值: ", mylist)
... return
...
>>> mylist = [10,20,30]
>>> changeme( mylist )
函数内取值: [10, 20, 30, [1, 2, 3, 4]]
>>> print ("函数外取值: ", mylist)
函数外取值: [10, 20, 30, [1, 2, 3, 4]]

return [表达式]

我们知道,当程序执行到 return [表达式] 时标志函数执行完成,用于将表达式结果(任意数据类型)进行返回。

需要注意的是,return 语句在同一函数中可以出现多次,但只要有一个得到执行,就会直接结束函数的执行。

[1] >>>> return None 隐含机制

如果函数中没有 return [表达式] 语句或者单独的 return 或者 return None,函数都返回是一个 None。

也就说没有返回值语句时,Python 解释器会自动帮你返回 None

–> 思考,函数可以返回多个值吗?答案是肯定的。

[2] >>>> return 返回多个值

实现 Python 函数返回多个值,有以下 2 种方式:

  • 在函数中,提前将要返回的多个值存储到一个列表或元组中,然后函数返回该列表或元组;
  • 函数直接返回多个值,之间用逗号( , )分隔,Python 会自动将多个值封装到一个元组中,其返回值仍是一个元组。

程序演示:

1
2
3
4
5
6
7
8
9
10
11
12
def retu_list():
add = ["Python", "C/C++", "Java"]
return add
print("retu_list: ", retu_list())

def retu_tuple():
return "Python", "C/C++", "Java"
print("retu_tuple: ", retu_tuple())

# Output
# retu_list: ['Python', 'C/C++', 'Java']
# retu_tuple: ('Python', 'C/C++', 'Java')

序列解包方式接收多个返回值 >>>>

可以直接使用序列元素个数对应数量的变量,接收函数返回列表或元组中的多个值:

1
2
3
4
5
def retu_tuple():
return "Python", "C/C++", "Java"

p,c,j = retu_tuple()
print("retu_tuple: ( %s, %s, %s" % (p,c,j), ")")

函数参数列表详解

Python 的函数定义非常简单,但由于参数列表的多种定义方式,灵活度却非常大。参数列表支持的参数类型定义:

  • 位置参数
  • 关键字参数
  • 默认参数
  • 不定长参数

这些参数形式的引入,使得不仅能处理比较复杂的参数使用场景,还可以极大的简化调用者的代码。

位置参数

位置参数详细说明可见 【形参(位置参数) && 实参】 和 【参数检查机制】 小节说明。

目前,我们调用函数时所用的参数都是位置参数,即传入函数的实际参数必须与形式参数的数量和位置必须一一对应(需要人为参与)。

不知道你有没有这种感觉,按顺序给参数传入数值总是感觉不太靠谱,一旦疏忽搞错怎么办? >>>> 关键字参数。


关键字参数

关键字参数,是指使用形式列表中参数的名字来 “绑定” 实参列表中输入的参数值。

通过此方式指定函数实参时,不再需要与形参的位置完全一致,只要将参数名写正确即可。

[1] 使用关键字参数传参 >>>>

1
2
3
4
5
6
7
8
>>> def move(start_x, start_y, step):
... end_x = start_x + step
... end_y = start_y + step
... return end_x, end_y
...

>>> move(start_x=2, start_y=3, step=2)
(4, 5)

[2] 使用位置 && 关键字参数混合传参 >>>>

你还可以使用位置参数和关键字参数混合传参的方式:

1
2
3
4
5
6
>>> move(2,3, step=2)
(4, 5)


# 这样是不允许的:(move(2,1, start_y=3))
# 认为你传入了两个 start_y

需要注意,混合传参时 关键字参数必须位于所有的位置参数之后 。否则,会产生错误:

1
SyntaxError: positional argument follows keyword argument

默认参数

如果函数参数列表中的某个参数的值很少发生变化,为了避免每次都传同样的值,可以使用默认参数(函数定义时给出参数默认值),以达到简化函数调用的目的。语法规则如下:

1
2
def 函数名(...,形参名,形参名=默认值):
代码块

注意,使用此格式定义函数时,指定有默认值的形式参数必须在所有没默认值参数的最后,否则会产生语法错误。

以下实例中,step 为默认参数,调用时如果没有传入 step 参数,则使用默默认值:

1
2
3
4
5
6
7
>>> def move(start_x, start_y, step=1):
... end_x = start_x + step
... end_y = start_y + step
... return end_x, end_y
...
>>> move(2,3)
(3, 4)

这样,当我们调用 move(2, 3) 时,相当于调用了 move(2, 3,1)

使用默认参数时,也要符合位置参数 && 关键字参数规则:

1
2
3
4
5
6
7
8
9
10
11
>>> move(2,3)
>
>>> move(2,1,start_y=3) # 这是不允许的
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: move() got multiple values for argument 'start_y'

>>> move(start_x=2,start_y=3,step=1)
(3, 4)
>>> move(2,3,step=1)
>(3, 4)

默认参数设置原则

使用默认参数时,要注意:

  • 有默认值的形式参数必须在所有没默认值参数的最后,否则会产生语法错误;
  • 当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

来看一个运用实例:

写个北京市一年级小学生注册的函数,需要传入名字和性别两个参数:

1
2
3
def enroll(name, gender):
print('name:', name)
print('gender:', gender)

如果要继续传入年龄、城市等信息怎么办?这样会使得调用函数的复杂度大大增加。此时,我们可以把年龄和城市设为默认参数(变化小):

1
2
3
4
5
def enroll(name, gender, age=8, city='Beijing'):
print('name:', name)
print('gender:', gender)
print('age:', age)
print('city:', city)

这样,大多数学生注册时不需要提供年龄和城市,只提供必须的两个参数:

1
2
3
4
5
>>> enroll('Sarah', 'F')
name: Sarah
gender: F
age: 8
city: Beijing

只有与默认参数不符(飞北京,不是 8 岁入学)的学生才需要提供额外的信息:

1
2
3
enroll('Bob', 'M', 7)
enroll('Adam', 'M', city='Tianjin')
enroll('Adam', 'M', 9, city='Tianjin')

可见,默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。


默认参数必须指向不可变对象

默认参数的很有用,但使用不当,也会掉坑里….默认参数有个最大的坑

先定义一个带有默认参数空 list 的函数,调用方法会添加一个 END 再返回:

1
2
3
def add_end(L=[]):
L.append('END')
return L

当你正常调用时,结果似乎不错:

1
2
3
4
>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']

当你使用默认参数调用时,一开始结果也是对的:

1
2
>>> add_end()
['END']

但是,再次调用add_end()时,结果就不对了:

1
2
3
4
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

很多初学者很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了 'END' 后的 list。还记得我们前面在参数传递中说过的可变类型对象的传递么?原因正在这里。

定义默认参数要牢记一点:默认参数必须指向不变对象!要修改上面的例子,我们可以用 None 这个不变对象来实现:

1
2
3
4
5
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L

现在,无论调用多少次,都不会有问题:

1
2
3
4
>>> add_end()
['END']
>>> add_end()
['END']

不定长参数

有些时候,你可能需要一个函数能够处理比当初声明时更多的参数,也就是说,定义时我们无法明确参数的个数。

以一个计算 a1 + a2 + a3 + …… +a_n(n 个数的和)的样例来说明:

要定义出这个函数,按照之前的规则,我们必须确定输入的参数。但由于参数个数不确定(不知道具体要算几个数的和),所以我们首先想到可以把 a1,a2,a3…… 作为一个 list 或 tuple 传进来,这样,函数可以定义如下:

1
2
3
4
5
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum

这种方法,调用的时候,我们需要先组装出一个 list 或 tuple,然后传入 calc 函数,比较麻烦以及浪费资源的。

在 Python 中,支持定义可变参数(不定长参数)的函数,即传入函数中的实际参数可以是任意多个。

Python 定义不定长,有以下 2 种形式(名字自己起的哈):

  • 元组型不定长非关键字实参;
  • 字典型不定长关键字实参。

元组型不定长非关键字实参

此种形式的语法格式如下所示:

1
*args

*args 表示创建一个名为 args 的空元组,该元组可接受任意多个(不定长)外界传入的非关键字实参。

程序演示了如何定义一个参数可变的函数:

1
2
3
4
5
6
def calc(arg1, *numbers):
sum = 0
for n in numbers:
sum = sum + n * n
print(arg1)
return sum

上面程序中,calc() 函数的最后一个参数就是 numbers 元组,这样在调用该函数时,除了前面位置参数接收对应位置的实参外,其它非关键字参数都会由 numbers 元组接收。

当然,可变参数并不一定必须为最后一个函数参数,例如修改 calc() 函数为:

1
2
3
4
5
6
def calc(*numbers, arg1):
sum = 0
for n in numbers:
sum = sum + n * n
print(arg1)
return sum

numbers 可变参数作为 calc() 函数的第一个参数。需要注意的是,在调用该函数时,必须以关键字参数的形式给普通参数传值,否则 Python 解释器会把所有参数都优先传给可变参数,如果普通参数没有默认值,就会报错。

| >>>> =======================================================

重新认识 print() 函数,查看 print() 函数文档:

1
print(*objects, sep = ' ', end = '\n', file = sys.stdout, flush = False)

可以看到第一个参数 objects 带了 * 号,为不定长参数 –> 这也是为什么 print() 函数可以传递任意数量的参数。其余四个为默认参数,我们可以通过修改默认值来改变参数:

1
print('金枪鱼', '三文鱼', '鲷鱼', sep = '+', end = '=?')

字典型不定长关键字实参

此种形式的语法格式如下所示:

1
**kwargs

**kwargs 表示创建一个名为 kwargs 的空字典,该字典可接受任意多个(不定长)外界传入以关键字参数赋值的实参。

这种方法允许你传入 0 个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个 dict。请看示例:

1
2
3
>>> def person(name, age, **vardict):
... print('name:', name, 'age:', age, 'other:', vardict)
...

函数 person 除了位置参数 nameage 外,还接受关键字参数 vardict。在调用该函数时,可以只传入位置参数:

1
2
>>> person("Opera",35)
name: Opera age: 35 other: {}

也可以同时传入任意个数的关键字实参:

1
2
>>> person("Google",23,city="Beijing",job="IT")
name: Google age: 23 other: {'city': 'Beijing', 'job': 'IT'}

参数混合

在 Python 中定义函数,可以使用位置参数、默认参数、、关键字参数、不定长参数,并且这 4 种参数都可以组合使用,例如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
# 定义了支持参数收集的函数
def dis_str(home,*str,**course) :
print(home)
print(str)
print(course)
#调用函数
dis_str("home_test",\
"str_1",\
"str_2",\
shell="course_1",\
go="course_2",\
java="course_1")

程序输出结果为:

1
2
3
home_test
('str_1', 'str_2')
{'shell': 'course_1', 'go': 'course_2', 'java': 'course_1'}

序列支持向函数传参

我们知道,Python 支持定义具有可变参数的函数,即该函数可以接收任意多个参数,其中非关键字参数会集中存储到元组参数(*args)中,而关键字参数则集中存储到字典参数(**kwargs)中,这个过程可称为参数收集。

不仅如此,Python 还支持逆向参数收集,即直接将列表、元组、字典作为函数参数,Python 会将其进行拆分,把其中存储的元素按照次序分给函数中的各个形参。

在以逆向参数收集的方式向函数参数传值时,Pyhon 语法规定,当传入列表或元组时,其名称前要带一个 * 号,当传入字典时,其名称前要带有 2 个 * 号。

[1] >>>> 向定长形参列表传参

举个例子:

1
2
3
4
5
6
7
8
9
10
def dis_str(name,add) :
print("name:",name)
print("add",add)
data = ["Python教程","http://c.biancheng.net/python/"]
# 使用逆向参数收集方式传值
dis_str(*data)

# Output:
# name: Python教程
# add http://c.biancheng.net/python/

[2] >>>> 向不定长形参列表传参

以逆向参数收集的方式,还可以给拥有可变参数的函数传参,例如:

1
2
3
4
5
6
7
8
9
10
11
12
def dis_str(name,*add) :
print("name:",name)
print("add:",add)
data = ["http://c.biancheng.net/python/",\
"http://c.biancheng.net/shell/",\
"http://c.biancheng.net/golang/"]
#使用逆向参数收集方式传值
dis_str("Python教程",*data)

# Output:
# name: Python教程
# add: ('http://c.biancheng.net/python/', 'http://c.biancheng.net/shell/', 'http://c.biancheng.net/golang/')

同样的,你可以执行下面代码,可上述输出一样(比较一下差别):

1
2
3
4
5
6
7
8
9
10
11
12
13
def dis_str(name,*add) :
print("name:",name)
print("add:",add)
data = ["Python教程",\
"http://c.biancheng.net/python/",\
"http://c.biancheng.net/shell/",\
"http://c.biancheng.net/golang/"]
#使用逆向参数收集方式传值
dis_str(*data)

# Output:
# name: Python教程
# add: ('http://c.biancheng.net/python/', 'http://c.biancheng.net/shell/', 'http://c.biancheng.net/golang/')

| >>>> ======================================================

使用逆向参数收集的方式,必须注意 * 号的添加。以逆向收集列表列表为例,如果传参时其列表名前不带 * 号,则 Python 解释器会将整个列表作为参数传递给一个参数。

1
2
3
4
5
6
7
8
9
10
11
12
def dis_str(name,*add) :
print("name:",name)
print("add:",add)
data = ["Python教程",\
"http://c.biancheng.net/python/",\
"http://c.biancheng.net/shell/",\
"http://c.biancheng.net/golang/"]
dis_str(data)

# 输出
# name: ['Python教程', 'http://c.biancheng.net/python/', 'http://c.biancheng.net/shell/', 'http://c.biancheng.net/golang/']
# add: ()

函数间协作

上面,我们使用函数来封装具有独立功能的代码模块,实际编程时,一个程序往往是通过多个函数的配合来实现的。

浅析变量作用域

当多个函数同时运行时,就涉及函数中一个非常重要的概念 —— 变量作用域(Vars Scope)。

全局变量 && 局部变量

先来看一个例子:

月底了,身为老板的你要核算成本来调整经营策略,假设餐馆的成本是由固定成本(租金)和变动成本(水电费 + 食材成本)构成的。

那么我们可以分别编写一个计算变动成本的函数和一个计算总成本的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rent = 3000

def cost():
utilities = int(input('请输入本月的水电费用'))
food_cost = int(input('请输入本月的食材费用'))
variable_cost = utilities + food_cost
print('本月的变动成本费用是' + str(variable_cost))

def sum_cost():
sum = rent + variable_cost
print('本月的总成本是' + str(sum))

cost()
sum_cost()

乍一看代码好像没有什么问题,但是一旦运行,终端就会报错:

1
2
3
line 10, in sum_cost
sum = rent + variable_cost
NameError: name 'variable_cost' is not defined

可以发现,第一个函数 cost() 运行没有问题,报错信息指出问题出在 sum_cost() 函数内的变量 variable_cost 没有被定义。

这就涉及一个变量作用域的问题 >>>>

程序中的变量并不是在哪个位置都可以被使用的,它是有作用域。

变量的作用域指的是变量的有效范围,就是变量可以在哪个范围以内使用。它由变量的定义位置决定,在不同位置定义的变量,它的作用域是不一样的。

目前我们只需要掌握下面两点即可:

  1. 一个在函数内部赋值的变量仅能在该函数内部使用(局部作用域),它们被称作 局部变量,如 cost() 函数里的 variable_cost

  2. 在所有函数之外赋值的变量,可以在程序的任何位置使用(全局作用域),它们被称作 全局变量,如程序第一行定义的变量 rent=3000

上述例子中,变量 rent 是在函数外被赋值的,所以它是全局变量,能被 sum_cost() 函数直接使用。而变量 variable_cost 是在 cost() 函数内定义的,属于局部变量,其余函数内部如 sum_cost() 无法访问。

事实上,当 cost() 函数调用执行完毕,在这个函数内定义的变量都会”消失”。

函数中局部变量是调用生成,调用结束后释放的 >>>>

def 语句后的代码块只是封装了函数的功能,如果没有被调用,那么 def 语句后面的代码永远不会被执行。当函数被调用时,Python 会为其分配一块临时的存储空间,所有在函数内部定义的变量,都会存储在这块空间中。而在函数执行完毕后,这块临时存储空间随即会被释放并回收,该空间中存储的变量自然也就无法再被使用。


获取指定作用域范围中的变量

在一些特定场景中,我们可能需要获取某个作用域内(全局范围内或者局部范围内)所有的变量,Python 提供了以下 3 种方式:

[1] >>>> globals()

globals() 函数为 Python 的内置函数,它可以返回一个包含全局范围内所有变量的字典,该字典中的每个键值对,键为变量名,值为该变量的值。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
# 全局变量
Pyname = "Python 教程"
Pyadd = "http://orangeshare.cn/python"

def text():
# 局部变量
Shename = "shell 教程"
Sheadd= "http://orangeshare.cn/shell"
print(globals())

# Output:
# { ...... , 'Pyname': 'Python 教程', 'Pyadd': 'http://orangeshare.cn/python', ......}

注意,globals() 函数返回的字典中,会默认包含有很多变量,这些都是 Python 主程序内置的,读者暂时不用理会它们。

并且,通过该字典,我们还可以访问指定变量,甚至如果需要,还可以修改它的值。例如,在上面程序的基础上,添加如下语句:

1
2
3
print(globals()['Pyname'])
globals()['Pyname'] = "Python 入门教程"
print(Pyname)

[2] >>>> locals()

locals() 函数也是 Python 内置函数之一,通过调用该函数,我们可以得到一个包含当前作用域内所有变量的字典。

这里所谓的 【当前作用域】 指的是,在函数内部调用 locals() 函数,会获得包含所有局部变量的字典;而在全局范文内调用 locals() 函数,其功能和 globals() 函数相同。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 全局变量
Pyname = "Python 教程"
Pyadd = "http://orangeshare.cn/python"
def text():
#局部变量
Shename = "shell 教程"
Sheadd= "http://orangeshare.cn/shell/"
print("函数内部的 locals:")
print(locals())
text()
print("函数外部的 locals:")
print(locals())

# Output:
# 函数内部的 locals:
# {'Sheadd': 'http://orangeshare.cn/shell/', 'Shename': 'shell 教程'}
# 函数外部的 locals:
# {...... , 'Pyname': 'Python 教程', 'Pyadd': 'http://orangeshare.cn/python/', ...... }

当使用 locals() 函数获取所有全局变量时,和 globals() 函数一样,其返回的字典中会默认包含有很多变量,这些都是 Python 主程序内置的,读者暂时不用理会它们。

注意,当使用 locals() 函数获得所有局部变量组成的字典时,可以向 globals() 函数那样,通过指定键访问对应的变量值,但无法对变量值做修改。


[3] >>>> vars(object)

vars() 函数也是 Python 内置函数,其功能是返回一个指定 object 对象范围内所有变量组成的字典。如果不传入object 参数,vars() 和 locals() 的作用完全相同。

由于目前读者还未学习 Python 类和对象,因此初学者可先跳过该函数的学习,等学完 Python 类和对象之后,再回过头来学习该函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 全局变量
Pyname = "Python 教程"
Pyadd = "http://orangeshare.cn/python/"
class Demo:
name = "Python 教程"
add = "http://orangeshare.cn/python/"
print("有 object:")
print(vars(Demo))
print("无 object:")
print(vars())

# Output:
# 有 object:
# {...... , 'name': 'Python 教程', 'add': 'http://c.biancheng.net/python/', ......}
# 无 object:
# {...... , 'Pyname': 'Python教程', 'Pyadd': 'http://c.biancheng.net/python/', ...... }

函数中使用同名全局变量

当函数内部的局部变量和函数外部的全局变量同名时,在函数内部,局部变量会“遮蔽”同名的全局变量。

这时,无论是访问还是修改该同名变量,操作的都是局部变量,而不再是全局变量。

这里举个实例:

1
2
3
4
5
6
7
8
name = "Python 教程"
def demo ():
# 访问全局变量
print(name)
demo()

# Output:
# Python 教程

上面程序中,print(name) 直接访问 name 变量,这是允许的。在上面程序的基础上,在函数内部添加一行代码,如下所示:

1
2
3
4
5
6
name = "Python教程"
def demo ():
# 访问全局变量
print(name)
name = "shell教程"
demo()

执行此程序,Python 解释器报如下错误:

1
UnboundLocalError: local variable 'name' referenced before assignment

发生什么了?!! >>>>

由于函数中局部变量名和全局变量名 name 同名(触发屏蔽机制),局部 name 变量就会“遮蔽”全局 name 变量,再加上局部变量 nameprint(name) 后才被初始化,违反了“先定义后使用”的原则,因此程序会报错。

如果就是想在函数中访问甚至修改被“遮蔽”的变量,怎么办呢?

这时候 global 语句就能派上用场了,它可以将局部变量声明为全局变量,作如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
name = "Python 教程"
def demo ():
global name
# 访问全局name变量
print(name)
# 修改全局 name 变量的值
name = "shell 教程"
demo()
print(name)

# Output:
# Python 教程
# shell 教程

增加了 global name 声明之后,程序会把 name 变量当成全局变量,这意味着 demo() 函数后面对 name 赋值的语句只是对全局变量赋值,而不是重新定义局部变量。


局部函数

我们知道,Python 函数内部可以定义变量,这样就产生了局部变量,有读者可能会问,Python 函数内部能定义函数吗?答案是肯定的。

Python 支持在函数内部定义函数,此类函数又称为 局部函数

和局部变量一样,默认情况下局部函数只能在其所在函数的作用域内使用。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#全局函数
def outdef ():
# 局部函数
def indef():
print("http://orangeshare.cn/python/")
# 调用局部函数(该函数只在当前作用域可见)
indef()

# indef() 不能直接调用,内部函数在当前作用域内不可见
# 调用全局函数
outdef()

# Output:
# http://orangeshare.cn/python/

可以看到,indef 函数定义于 outdef 函数中,所以其作用域就是 outdef 函数内。想要执行局部函数,只能在其作用域(outdef 函数)中进行调用。


[1] >>>> 局部函数变量同名“遮蔽”问题

需要注意,如果局部函数中定义有和所在函数中变量同名的变量,也会发生“遮蔽”的问题。

例如:

1
2
3
4
5
6
7
8
9
10
# 全局函数
def outdef ():
name = "所在函数中定义的 name 变量"
# 局部函数
def indef():
print(name)
name = "局部函数中定义的 name 变量"
indef()
# 调用全局函数
outdef()

执行此程序,Python 解释器会报如下错误:

1
UnboundLocalError: local variable 'name' referenced before assignment

怎么办?!!

由于这里的 name 变量是局部变量,globals 关键字,并不适用于解决此问题。

这里可以使用 Python 提供的 nonlocal 关键字。例如,修改上面程序为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 全局函数
def outdef ():
name = "所在函数中定义的 name 变量"
# 局部函数
def indef():
nonlocal name
print(name)
# 修改 name 变量的值
name = "局部函数中定义的 name 变量"
indef()
# 调用全局函数
outdef()

# Output:
# 所在函数中定义的 name 变量

[2] >>>> 函数作为返回值,扩大局部函数作用域

就如同全局函数返回其局部变量,就可以扩大该变量的作用域一样;通过将局部函数作为所在函数的返回值,也可以扩大局部函数的使用范围。例如,修改上面程序为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 全局函数
def outdef ():
name = "局部函数所在外部函数中定义的 name 变量"

# 局部函数
def indef():
nonlocal name
print(name)
# 调用局部函数
return indef

# 调用全局函数
new_indef = outdef()

# 调用全局函数中的局部函数
new_indef()

# Output:
# 调用局部函数

也就是说:如果所在函数没有返回局部函数,则局部函数的可用范围仅限于所在函数内部;反之,如果所在函数将局部函数作为返回值,则局部函数的作用域就会扩大,既可以在所在函数内部使用,也可以在所在函数的作用域中使用。

哎?连续调用了两回?怎么理解在全局作用域中调用局部函数?

这是由于 outdef() 外部函数返回的是一个其内部函数的引用,还记得前面关于函数名称的解读么?所以调用 outdef() 外部函数其返回值相当于其内部函数(局部函数)的一个别名,然后就可以全局作用域中调用局部函数。

带来的好处 >>>>

我们知道,只有当函数被调用时,Python 才会为其分配一块临时的存储空间,所有在函数内部定义的变量,都会存储在这块空间中。而在函数执行完毕后,这块临时存储空间随即会被释放并回收,该空间中存储的变量自然也就无法再被使用。

而以使用其局部函数作为返回值的外部函数,当外部函数结束时,其局部函数中使用到的外部函数相关联变量会被绑定到内部函数,这样你就可以使得这些变量始终保存在内存中,不会随外部函数的结束而清除(变量状态保存)。

其实,上面介绍到的就是 Python 中闭包的概念,更多请参加:Python 中的函数升阶


函数内部调用

Python 中在一个函数的内部,也是支持调用其它函数的。看如下样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rent = 3000

def cost():
global variable_cost
utilities = int(input('请输入本月的水电费用'))
food_cost = int(input('请输入本月的食材费用'))
variable_cost = utilities + food_cost
print('本月的变动成本是' + str(variable_cost))

def sum_cost():

cost()

sum = rent + variable_cost
print('本月的总成本是' + str(sum))

sum_cost()

需要注意的是,语句对函数调用,必须在函数调用之后,包括直接调用的函数调用的其他函数也必须在调用语句之前,否则报错。

然而,更多的是不再定义全局变量,而是将函数的预期执行结果作为当前函数的返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rent = 3000

def cost():

utilities = int(input('请输入本月的水电费用'))
food_cost = int(input('请输入本月的食材费用'))
variable_cost = utilities + food_cost
print('本月的变动成本是' + str(variable_cost))

return variable_cost

def sum_cost():

sum = rent + cost()
print('本月的总成本是' + str(sum))

sum_cost()

这应该才是最合理的使用方法。


递归函数

Python 中函数内部是支持调用其它函数的,那么,如果一个函数在内部是否可以调用自身本身呢?

可以的!!!这个函数就被称为递归函数。

来看一个实例,我们来计算阶乘 n! = 1 x 2 x 3 x ... x n,用函数 fact(n) 表示,可以看出:

fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n

所以,fact(n) 可以表示为 n x fact(n-1),只有 n=1 时需要特殊处理。

于是,fact(n) 用递归的方式写出来就是:

1
2
3
4
5
6
7
8
9
10
11
>>> def fact(n):
... if n==1:
... return 1
... return n * fact(n - 1)
...
>>> fact(1)
1
>>> fact(2)
2
>>> fact(5)
120

如果我们计算fact(5),可以根据函数定义看到计算过程如下:

1
2
3
4
5
6
7
8
9
10
===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

执行递归函数将反复调用其自身,每调用一次就进入新的一层,当最内层的函数执行完毕后,再一层一层地由里到外退出。

当一个函数不断地调用它自身时,必须在某个时刻函数的返回值是确定的,即不再调用它自身:否则,这种递归就变成了无穷递归,类似于死循环。因此,在定义递归函数时有一条最重要的规定: 递归一定要向已知方向进行。


[1] >>>> 递归中的栈溢出问题

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。

由于栈的大小不是无限的,所以递归调用的次数过多,会导致栈溢出。可以试试 fact(1000)

1
2
3
4
5
6
7
8
9
>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
File "<stdin>", line 4, in fact
File "<stdin>", line 4, in fact
[Previous line repeated 995 more times]
File "<stdin>", line 2, in fact
RecursionError: maximum recursion depth exceeded in comparison

[2] >>>> 尾递归解决栈溢出

解决递归调用栈溢出的方法是进行 尾递归 优化 >>>>

普通的递归,每一次递归都需要调用函数,会创建新的栈,生成一大堆中间变量;而尾递归不会保存中间变量,每一级调用直接返回函数的返回值更新调用栈,而不用创建新的调用栈。

如何实现尾递归 >>>> 在函数返回的时候,调用其自身,并且 return 语句不能包含表达式。

上面的 fact(n) 函数由于 return n * fact(n - 1) 引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:

1
2
3
4
5
6
7
def fact(n):
return fact_iter(n, 1)

def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)

可以看到,return fact_iter(num - 1, num * product) 仅返回递归函数本身,num - 1num * product 在函数调用前就会被计算,不影响函数调用。fact(5) 对应的 fact_iter(5, 1) 的调用如下:

1
2
3
4
5
6
===> fact_iter(5, 1)
===> fact_iter(4, 5*1)
===> fact_iter(3, 5*1*4)
===> fact_iter(2, 5*1*4*3)
===> fact_iter(1, 5*1*4*3*2)
===> 120

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

遗憾的是,某些编程语言没有针对尾递归做优化,Python 解释器就是其中之一,所以,即使把上面的 fact(n) 函数改成尾递归方式,也会导致栈溢出。


能人无数啊!!!网上有大佬用装饰器实现了 Python 的尾递归,试运行下面代码验证:

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
#!/usr/bin/env python3
# This program shows off a python decorator(
# which implements tail call optimization. It
# does this by throwing an exception if it is
# it's own grandparent, and catching such
# exceptions to recall the stack.

import sys

class TailRecurseException(BaseException):
def __init__(self, args, kwargs):
self.args = args
self.kwargs = kwargs

def tail_call_optimized(g):
"""
This function decorates a function with tail call
optimization. It does this by throwing an exception
if it is it's own grandparent, and catching such
exceptions to fake the tail call optimization.

This function fails if the decorated
function recurses in a non-tail context.
"""
def func(*args, **kwargs):
f = sys._getframe()
if f.f_back and f.f_back.f_back \
and f.f_back.f_back.f_code == f.f_code:
raise TailRecurseException(args, kwargs)
else:
while 1:
try:
return g(*args, **kwargs)
except TailRecurseException as e:
args = e.args
kwargs = e.kwargs
func.__doc__ = g.__doc__
return func

@tail_call_optimized
def factorial(n, acc=1):
"calculate a factorial"
if n == 0:
return acc
return factorial(n-1, n*acc)

print(factorial(10000))
# prints a big, big number,
# but doesn't hit the recursion limit.

@tail_call_optimized
def fib(i, current = 0, next = 1):
if i == 0:
return current
else:
return fib(i - 1, next, current + next)

print(fib(10000))
# also prints a big number,
# but doesn't hit the recursion limit.

Author

Waldeinsamkeit

Posted on

2018-01-10

Updated on

2022-03-20

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.