编程语言中的函数与数学中的函数是有区别的:数学中的函数有参数(输入),就会有相应的结果(输出)。编程语言中的函数有输入,不一定会返回结果。 。那么哪些代码语句应该被整合到一起定义为一个函数呢?这取决于你想让这个函数完成的功能是什么。
为什么要将这个代码段定义成一个函数呢?这其实就是函数的作用。假设我们在编写一个可供用户选择的菜单程序,程序启动时需要打印一遍菜单列表,而且程序运行过程中用户也可以随时打印菜单列表,也就是说打印菜单列表的代码段可能要多次被用到,假设每次打印的菜单列表都是一样的,而且列表很长,那么我们是否应该每次在需要打印菜单的时候重复执行相同的代码呢?那么当我们需要增加或者减少一个菜单项时怎么办呢?显然我们需要在每个打印菜单的代码点都进行修改。如果我们把打印菜单的相关代码拿出来定义为一个函数,又会出现这样的场景呢?我们只需要在需要打印菜单列表的地方使用这个函数;当需要添加或减少一个菜单项时,只需要修改这个函数中的内容即可,程序的维护和扩展成本大大降低;同时,我们这个程序的代码会更加简洁,而且有条理性更加便于阅读,而不是一坨乱糟糟的让人看着就想重写的东西。当然,如果你要打印的是多级菜单,你可以通过函数的参数或全部变量通知该函数要打印的是几级菜单。总结一下,。
高级编程语言通常会提供很多内置的函数来屏蔽底层差异,向上暴露一些通用的接口,比如我们之前用到的 print() 函数和 open() 函数。除此之外,我们也可以自定义我们需要的函数。由于函数本身也是程序代码的一部分,因此为了标识出这段代码是一个函数通常需要使用特定的格式或关键字。另外还涉及到参数、方法名称、返回值等相关问题的约束。
- def 函数名称( 参数 ):
- """
- 函数使用说明、参数介绍等文档信息
- """
- 代码块
- return [表达式]
- def add(a, b):
- """
- 计算并返回两个数的和
- a: 被加数
- b: 加数
- """
- c = a + b
- return c
通常写成这个样子:
- def add(a, b):
- """
- 计算并返回两个数的和
- a: 被加数
- b: 加数
- """
- return a + b
Python 中函数的调用与其他大部分编程语言都一样(其实我目前使用过的编程语言当中,只有 shell 是个另类;好吧,其实它只是个脚本语言):函数名 (参数)
- def add(a, b):
- """
- 计算并返回两个数的和
- a: 被加数
- b: 加数
- """
- return a + b
- sum = add(1, 9)
先来说下形参和实参的概念:
重点需要说下函数的各种不同种类的参数。函数的参数可以分为以下几种:
不同编程语言对以上几种函数参数的支持各不相同,但是位置参数是最基本的参数类型,基本上所有的编程语言都支持。以下是一个常见编程语言的对比表格(Y 表示支持,N 表示不支持):
可见只有 Python 支持全部参数类型,而且只有 Python 支持关键字参数;另外,C、Java 和 Go 都不支持默认参数,其中 Java 和 Go 与它们支持的方法重载特性有关(具体可以看下),并且它们可以通过方法重载实现默认参数的功能。
下面我们以一个自定义的打印函数来对以上各种参数进行说明:
位置参数,顾名思义是和参数的顺序位置和数量有关的。函数调用时,实参的位置和个数要与形参对应,不然会报错。
- def my_print(name, age):
- print('NAME: %s' % name)
- print('AGE: %d' % age)
- >>> my_print('Tom', 18)
- NAME: Tom
- AGE: 18
- >>> my_print(18, 'Tom')
- NAME: 18
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "<stdin>", line 3, in my_print
- TypeError: %d format: a number is required, not str
- >>> my_print('Tom')
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: my_print() missing 1 required positional argument: 'age'
默认参数:是指给函数的形参赋一个默认值,它是一个有默认值的位置参数。当调用函数时,如果为该参数传递了实参则该形参取实参的值,如果没有为该参数传递实参则该形参取默认值。
默认参数的应用场景:参数值在大部分情况下是固定 / 相同的。比如这里打印一个班中学生的姓名和年龄,这个班大部分为同龄人(年龄相同),这时我们就可以给 "年龄" 这个形参赋一个默认的值。
- def my_print(name, age=12):
- print('NAME: %s' % name)
- print('AGE: %d' % age)
- >>> my_print('Tom', 18)
- NAME: Tom
- AGE: 18
age 取的是函数调用时传递过来的实参
- >>> my_print('Tom')
- NAME: Tom
- AGE: 12
函数调用时没有给形参 age 传值,因此 age 取的是默认值
- >>> my_print(18)
- NAME: 18
- AGE: 12
可见,我们明明是想传递 18 给形参 age 的,结果 18 被赋给了 name,而 age 仍然取得是默认值。上面已经提到过,位置参数只是可以让我们少传一些参数,但是不能改变参数的位置和顺序。另外,这也说明了默认参数为什么一定要放在后面:因为实参与形参是从前到后一一有序的对应关系,也就是说在给后面参数传值的时候,不论前面的参数是否有默认值,必须要先给前面的参数先赋值。
- >>> my_print('Tom', 18, 'F')
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: my_print() takes from 1 to 2 positional arguments but 3 were given
这里要说明的是:默认参数只能相应的减少实参的个数,但是不能增加实参的个数。这个很容易想明白,不做过多解释,只是为下面的可变长 (参数) 做铺垫。
可变 (长) 参数:顾名思义,是指长度可以改变的参数。通俗点来讲就是,可以传任意个参数(包括 0 个)。
可变 (长) 参数的应用场景:通常在写一个需要对外提供服务的方法时,为了避免将来添加或减少什么新的参数使得所有调用该方法的代码点都要进行修改的情况发生,此时就可以用一个可变长的形式参数。
- def my_print(name, age=12, *args):
- print('NAME: %s' % name)
- print('AGE: %d' % age)
- print(args)
再次强调:位置参数、默认参数、可变长参数在函数定义中的位置不能变。
- >>> my_print('Tom')
- NAME: Tom
- AGE: 12
- ()
方法调用时,只传递了一个实参,该实参会按照函数中参数的定义位置赋值给形参 name,因此 name 的值为'Tom';而形参 age 没有接收到实参,但是它有默认值,因此它取的是默认值 12;需要注意的是可变参数 args 也没有接收到传递值,但是打印出来的是一对小括号 (),说明 args 是一个 tuple(元祖) 类型,当没有接收到实参时便是一个空 tuple。
- >>> my_print('Tom', 18)
- NAME: Tom
- AGE: 18
- ()
与值传递一个实参的情况基本相同,只是默认参数接收到了传递值,不再取默认值。
比如,现在需要多接收并打印一个人的性别 (F: 表示女,M: 表示男),可以这样用:
- >>> my_print('Tom', 18, 'F')
- NAME: Tom
- AGE: 18
- ('F',)
比如,现在需要多接收并打印一个人的性别 (F: 表示女,M: 表示男) 和籍贯信息,可以这样用:
- >>> my_print('Tom', 18, 'F', 'Hebei')
- NAME: Tom
- AGE: 18
- ('F', 'Hebei')
当然,我们也可以直接将一个 tuple 实例传递给形参 args,但是 tuple 实例前也要加上 * 号作为前缀:
- >>> t = ('F', 'Hebei')
- >>> my_print('Tom', 19, *t)
- NAME: Tom
- AGE: 19
- SEX: F
- ADDRESS: Hebei
你甚至可以将传递给形参 name 和 age 的实参也放到要传递的 tuple 实例中,但是最好不要这样做,因为很容易发生混乱:
- >>> t = ('Jerry', 10, 'F', 'Hebei')
- >>> my_print(*t)
- NAME: Jerry
- AGE: 10
- SEX: F
- ADDRESS: Hebei
由于 args 接收到实参之后会被转换成一个 tuple(元祖)的实例,而 tuple 本身是一个序列(有序的队列),因此我们可以通过下标 (args[n]) 来获取相应的实参。但是我们需要在函数使用文档中写明 args 中各实参的传递顺序及意义,并且在获取 args 中的元素之前应该对 args 做非空判断。因此函数的定义及调用结果应该是这样的:
函数定义:
- def my_print(name, age=12, *args):
- """
- Usage: my_print(name[, age[, sex[, address]]])
- :param name: 姓名
- :param age: 年龄
- :param args: 性别、籍贯
- :return: None
- """
- print('NAME: %s' % name)
- print('AGE: %d' % age)
- if len(args) >= 1:
- print('SEX: %s' % args[0])
- if len(args) >= 2:
- print('ADDRESS: %s' % args[1])
函数调用及结果:
- >>> my_print('Tom')
- NAME: Tom
- AGE: 12
- >>> my_print('Tom', 18)
- NAME: Tom
- AGE: 18
- >>> my_print('Tom', 18, 'F')
- NAME: Tom
- AGE: 18
- SEX: F
- >>> my_print('Tom', 18, 'F', 'Hebei')
- NAME: Tom
- AGE: 18
- SEX: F
- ADDRESS: Hebei
- >>> t = ('F', 'Hebei')
- >>> my_print('Tom', 19, *t)
- NAME: Tom
- AGE: 19
- SEX: F
- ADDRESS: Hebei
关键字参数:顾名思义,是指调用函数时通过关键字来指定是为哪个形参指定的实参,如 name="Tom", age=10。
关键字参数应用场景:关键字参数一方面可以允许函数调用时传递实参的顺序与函数定义时声明形参的顺序不一致,提高灵活性;另一方面,它弥补了可变长参数的不足。想一下,如果想为上面定义了可变长参数的函数只传递 "籍贯" 参数就必须同时传递 "性别" 参数;另外还要不断地判断 tuple 的长度,这是相当不方便的。而关键参数可以通过关键字来判断某个参数是否有传递值并获取该参数的实参值。
- def my_print(name, age=12, *args, **kwargs):
- print('NAME: %s' % name)
- print('AGE: %d' % age)
- print(args)
- print(kwargs)
- >>> my_print('Tom')
- NAME: Tom
- AGE: 12
- ()
- {}
方法调用时,只传递了一个实参,该实参会按照函数中参数的定义位置赋值给形参 name,因此 name 的值为'Tom';而形参 age 没有接收到实参,但是它有默认值,因此它取的是默认值 12;可变参数 args 也没有接收到传递值,因此 args 的值是一个空元组;重点需要注意的是关键字参数 kwargs 也没有接收到传递值,但是其打印值为一个空字典 (dict) 实例。
- >>> my_print('Tom', 18)
- NAME: Tom
- AGE: 18
- ()
- {}
与值传递一个实参的情况基本相同,只是默认参数接收到了传递值,不再取默认值。
- >>> my_print(age=18, name='Tom')
- NAME: Tom
- AGE: 18
- ()
- {}
可以不按照形参声明的顺序传递实参
- >>> my_print('Tom', 18, 'F', 'Hebei')
- NAME: Tom
- AGE: 18
- ('F', 'Hebei')
- {}
可见后面多余的两个实参都传递给了可变长参数 args
- >>> my_print('Tom', 18, 'F', addr='Hebei')
- NAME: Tom
- AGE: 18
- ('F',)
- {'addr': 'Hebei'}
- >>>
- >>> my_print('Tom', 18, sex='F', addr='Hebei')
- NAME: Tom
- AGE: 18
- ()
- {'sex': 'F', 'addr': 'Hebei'}
由以上两个示例可见,对于除去传递给位置参数和默认参数之外多余的参数,如果是直接以 value 的形式提供实参,则会被传递给可变长参数 args 而成为一个元组中的元素;如果是以 key=value 的形式提供实参,则会被传递给关键字参数 kwargs 而成为一个字典中的元素。
- >>> t=('Jerry', 19, 'F', 'Hebei')
- >>> my_print(*t)
- NAME: Jerry
- AGE: 19
- ('F', 'Hebei')
- {}
- >>> d = {
- 'name': 'Tom',
- 'age': 18,
- 'sex': 'F',
- 'addr': 'Hebei'
- } >>> my_print( * *d) NAME: Tom AGE: 18() {
- 'sex': 'F',
- 'addr': 'Hebei'
- }
- >>> d = {
- 'sex': 'F',
- 'addr': 'Hebei'
- } >>> my_print(age = 18, name = 'Tom', **d) NAME: Tom AGE: 18() {
- 'sex': 'F',
- 'addr': 'Hebei'
- }
- >>> t=('Tom', 18, 'abc')
- >>> d={'sex':'F', 'addr':'Hebei'}
- >>> my_print(*t, **d)
- NAME: Tom
- AGE: 18
- ('abc',)
- {'sex': 'F', 'addr': 'Hebei'}
- >>> my_print(name='Tom', 18, sex='F', addr='Hebei')
- File "<stdin>", line 1
- SyntaxError: positional argument follows keyword argument
关于 Python 中的函数参数说了这么多,我觉得很多必要来个总结:
一个程序中的变量是有作用域的,作用域的大小会限制变量可访问的范围。根据作用域范围的大小不同可以分为:全局变量和局部变量。顾名思义,全局变量表示变量在全局范围内都可以被访问,而局部变量只能在一个很小的范围内生效。这就好比国家主席与各省的省长:在全国范围内国家主席都是同一个人,因此国家主席就是个全局变量;而各省的省长只能在某个省内生效,河北省省长是一个人,河南省省长又是另外一个人,因此省长就是个局部变量。对于 Python 编程语言而言,定义在一个函数内部的变量就是一个局部变量,局部变量只能在其被声明的函数内访问;定义在函数外部的变量就是全局变量,全局变量可以在整个程序范围内访问。
来看个示例:
- #!/usr/bin/env python
- # -*- encoding:utf-8 -*-
- name = 'Tom'
- def func1():
- age = 10
- print(name)
- print(age)
- def func2():
- sex = 'F'
- print(name)
- print(sex)
- print(name)
- func1()
- func2()
输出结果:
- Tom
- Tom
- 10
- Tom
- F
上面的示例中,name 是一个全局变量,因此它在程序的任何地方都可以被访问;而 func1 函数中的 age 变量和 func2 函数中的 sex 变量都是局部变量,因此它们只能在各自定义的函数中被访问。
- #!/usr/bin/env python
- # -*- encoding:utf-8 -*-
- name = 'Tom'
- def func3():
- name = 'Jerry'
- print(name)
- print(name)
- func3()
- print(name)
输出结果:
- Tom
- Jerry
- Tom
通过上面两个示例的输出结果我们可以得出这样的结论:
可以在函数内部通过 global 关键字声明该局部变量就是全局变量:
- #!/usr/bin/env python
- # -*- encoding:utf-8 -*-
- name = 'Tom'
- def func4():
- global name
- name = 'Jerry'
- print(name)
- print(name)
- func4()
- print(name)
输出结果:
- Tom
- Jerry
- Jerry
可见全局 name 的值的确被 func4 函数内部的操作改变了。
变量值的改变通常有两种方式:(1) 重新赋值 (2) 改变原有值。要想在函数内部通过重新赋值来改变全局变量的值,则只能通过上面介绍的使用 global 关键字来完成,通过传参是无法实现的。而要想在函数内部改变全局变量的原有值的属性就要看该参数是值传递还是引用传递了,如果是引用传递则可以在函数内部对全局变量的值进行修改,如果是值传递则不可以实现。具体请看下面的分析。
这个话题在几乎所有的编程语言中都会涉及,之所以把它放到最后是因为觉得这个问题对于编程新手来说比较难理解。与 相似的概念是 。前者主要是指函数调用时传递参数的时候,后者是指把一个变量赋值给其他变量或其他一些专门的拷贝操作(如深拷贝和浅拷贝)的时候。
这里我们需要先来说明下定义变量的过程是怎样的。首先,我们应该知道变量的值是保存在内存中的;以 name='Tom'为例,定义变量 name 的过程是这样的:
也就是说变量保存的不是真实的值,而是存放真实值的内存空间的地址。
"值拷贝" 和 "值传递" 比较好理解,就是直接把变量的值在内存中再复制一份;也就是说会分配并占用新的内存空间,因此变量指向的内存空间是新的,与之前的变量及其指向的内存空间没有什么关联了。而 "引用拷贝" 和 "引用传递" 仅仅是把变量对内存空间地址的引用复制了一份,也就是说两个变量指向的是同一个内存空间,因此对一个变量的值的修改会影响其他指向这个相同内存空间的变量的值。实际上,向函数传递参数时传递的也是实参的 "值拷贝或引用拷贝"。
- name1 = 'Tom'
- name2 = name1
- name2 = 'Jerry'
- print('name1: %s' % name1)
- print('name2: %s' % name2)
分析下上面操作的过程:
name1 指向的内存地址发生改变了吗?-- 没有,因为 name1 并没有被重新进行赋值操作。
name1 所指向的内存空间中的内容改变了吗? -- 没有,并没有对它做什么,并且字符串本就是个常量,是不可能被改变的。
So, 答案已经有了,name1 并没有被改变,因此输出结果是:
- name1: Tom
- name2: Jerry
- num1 = 10
- num2 = num1
- num2 += 1
- print('num1: %d' % num1)
- print('num2: %d' % num2)
与示例 1 过程相似,只是 += 操作也是一个赋值的过程,其他不再做过多解释。
输出结果:
- num1: 10
- num2: 11
- list1 = ['Tom', 'Jerry', 'Peter', 'Lily']
- list2 = list1
- list2.pop(0)
- print('list1: %s' % list1)
- print('list2: %s' % list2)
分析上面操作的过程:
list1 指向的内存地址发生改变了吗?-- 没有,因为 list1 并没有被重新进行赋值操作。
list2 所指向的内存空间中的内容改变了吗? -- 是的,因为 list1 和 list2 指向的是同一个内存地址,通过 list2 修改了该内存地址中的内容后就相当于修改了 list1。
So, 答案已经有了,list1 被改变了,因此输出结果是:
- list1: ['Jerry', 'Peter', 'Lily']
- list2: ['Jerry', 'Peter', 'Lily']
其实函数参数的传递过程也是类似的,比如:
- num1 = 10
- name1 = 'Tom'
- list1 = ['Tom', 'Jerry', 'Peter', 'Lily']
- def fun1(num2, name2, list2):
- num2 += 1
- name2 = 'Jerry'
- list2.pop(0)
- print('num2: %d' % num2)
- print('name2: %s' % name2)
- print('list2: %s' % list2)
- fun1(num1, name1, list1)
- print('num1: %d' % num1)
- print('name1: %s' % name1)
- print('list1: %s' % list1)
为了跟上面的示例做对比,我故意把 func1 函数中的形参的名称写为 num2、name2 和 list2,实际上他们可以为任意有意义的名称。
输出结果:
- num2: 11
- name2: Jerry
- list2: ['Jerry', 'Peter', 'Lily']
- num1: 10
- name1: Tom
- list1: ['Jerry', 'Peter', 'Lily']
其实这是相同的问题,因为上面说过了:参数传递的过程实际上就像先拷贝,然后将拷贝传递给形参。如果是值拷贝,那么调用函数传参时就是值传递;如果是引用拷贝,那么调用函数传参时就是引用(内存地址)传递。其实通过上面的示例,我们大概可以猜测到对于列表类型的变量貌似是引用传递,但是数字和字符串类型的变量是值传递还是引用传递呢?,关于这个问题我们可以通过 Python 内置的一个 id() 函数来进行验证。id() 函数会返回指定变量所指向的内存地址,如果是引用传递,那么实参和被赋值后的形参所指向的内存地址肯定是相同的。事实上,确实如此,如下所示:
- num1 = 10
- name1 = 'Tom'
- list1 = ['Tom', 'Jerry', 'Peter', 'Lily']
- def fun1(num2, name2, list2):
- print(id(num2), id(name2), id(list2))
- print(id(num1), id(name1), id(list1))
- fun1(num1, name1, list1)
输出结果:
- 1828586224 1856648389328 1856648385800
- 1828586224 1856648389328 1856648385800
实参和形参的内存地址一致,说明 Python 中的参数传递确实是 "引用传递"。
这篇文章写了很久,想说的东西太多。有时候手放到键盘上放了许久,却不知从何写起。算是对知识点的梳理,也希望对他人有所帮助。关于 Python 中关于函数的其它内容,如:函数递归、匿名函数、嵌套函数、高阶函数等,之后再讲。
来源: