函数
前面学了变量、控制流程和推导式,接下来瞅瞅 Python 中的函数。函数能把重复的代码封装起来,让程序更清晰、更可复用。
1. 什么是函数
函数是完成特定功能的、可重复调用的代码块。
函数是组织好的、可重复使用的代码段,用来实现单一或相关联的功能。它能提高代码的模块性和复用性。
Python 提供了很多内置函数,比如 print()、len()、range() 等,我们也可以自己定义函数,这种函数叫做用户自定义函数。
# 内置函数
print('Hello World!')
print(len('juzicoding'))
print(list(range(5)))2. 定义函数
2.1 基本语法
定义函数使用 def 关键字,后面跟函数名和圆括号,函数体以冒号开始并缩进。
# 定义一个函数
# 函数名: greet
# 参数: 无
# 返回值: 无
def greet():
print('Hello, Python!')
# 调用函数
# 输出: Hello, Python!
greet()函数定义的基本规则:
- 函数代码块以
def关键字开头,后接函数标识符名称和圆括号()。 - 传入的参数(实参)必须放在圆括号中。
- 函数体的第一条语句可以选择性地使用文档字符串,用于存放函数说明。
- 函数内容以冒号
:起始,并且缩进。 return [表达式]结束函数,选择性地返回一个值给调用方;不带表达式的return相当于返回None。
# 定义一个带参数的函数
def add(a, b):
return a + b
# 调用函数
# 输出: 5
print(add(2, 3))2.2 文档字符串
函数可以写一个文档字符串(docstring),用来说明函数的作用,通常放在函数体的第一条语句(紧跟在函数头之后)。
def add(a, b):
"""返回两个数的和"""
return a + b
# 输出: 返回两个数的和
print(add.__doc__)3. 调用函数
定义好函数后,可以通过函数名加圆括号来调用它,有参数时传入对应的参数。
# 定义函数
def print_message(msg):
print(msg)
# 调用函数
# 输出: 我要调用用户自定义函数!
print_message('我要调用用户自定义函数!')
# 输出: 再次调用同一函数
print_message('再次调用同一函数')4. 参数传递
Python 中参数是如何传递的。
在 Python 中,类型属于对象,变量没有类型,变量只是对对象的引用(可以理解为指向对象的标签)。当我们调用函数并把变量传入时,实际上是把变量所指向的对象的引用传给了函数内部的形参。
这就好比把家里钥匙复制了一把交给别人,别人拿到钥匙后:
- 如果他用这把钥匙去改房间里的东西(可变对象),你回家后会看到变化。
- 如果他用这把钥匙去换了一套房子的锁(重新赋值引用),你家的锁并不会变。
所以严格来说,Python 既不是「值传递」,也不是「引用传递」,而是「传对象引用」(也常称为按共享调用 / call by sharing):函数内部和调用方共享同一个对象。
a = [1, 2, 3]
# b 和 a 指向同一个列表对象
b = a
print(id(a) == id(b)) # True
b.append(4)
# 输出: [1, 2, 3, 4]
print(a)4.1 可变类型与不可变类型
Python 中的对象可以分为可变类型和不可变类型两大类。
| 类型 | 是否可变 | 常见类型 | 特点 |
|---|---|---|---|
| 不可变类型 | 否 | int、float、str、tuple | 对象一旦创建,内容不可修改 |
| 可变类型 | 是 | list、dict、set | 对象创建后,内容可以修改 |
判断一个对象是否可变,可以看它有没有提供原地(in-place)修改自身数据的方法。例如字符串没有 append() 方法,而列表有 append()、pop() 等方法。
4.2 不可变类型的参数传递
不可变类型的对象一旦创建,其内容就不能被修改。当函数内部对这类参数进行「修改」时,实际上是创建了一个新的对象,然后让形参指向这个新对象,而原来的实参仍然指向原来的对象。
4.2.1 数字
def change_num(num):
print('修改前 id:', id(num))
num = 10
print('修改后 id:', id(num))
a = 5
print('外部 id:', id(a))
change_num(a)
# 输出: 5
print('外部值:', a)输出结果中,函数内部修改前后的 id 不同,说明 num 指向了新的对象,而外部的 a 还是指向原来的对象。
4.2.2 字符串
def change_str(s):
print('修改前 id:', id(s))
s += ' world'
print('修改后 id:', id(s))
s = 'hello'
print('外部 id:', id(s))
change_str(s)
# 输出: hello
print('外部值:', s)字符串拼接后,函数内部的 s 指向了新的字符串对象 'hello world',而外部的 s 仍然是 'hello'。
4.2.3 元组
def change_tuple(t):
print('修改前 id:', id(t))
t += (4, 5)
print('修改后 id:', id(t))
t = (1, 2, 3)
print('外部 id:', id(t))
change_tuple(t)
# 输出: (1, 2, 3)
print('外部值:', t)元组拼接同样会创建新的元组对象。
4.3 可变类型的参数传递
可变类型的对象内容可以被修改。当函数内部通过这些对象提供的方法修改内容时,由于形参和实参指向的是同一个对象,所以外部的实参也会看到变化。
4.3.1 列表
def change_list(my_list):
print('修改前 id:', id(my_list))
my_list.append(4)
print('修改后 id:', id(my_list))
print('函数内:', my_list)
my_list = [1, 2, 3]
print('外部 id:', id(my_list))
change_list(my_list)
# 输出: 函数外: [1, 2, 3, 4]
print('函数外:', my_list)这里修改前后 id 相同,说明是同一个对象,只是内容被修改了。
4.3.2 字典
def change_dict(d):
d['age'] = 20
print('函数内:', d)
user = {'name': 'juzicoding'}
change_dict(user)
# 输出: 函数外: {'name': 'juzicoding', 'age': 20}
print('函数外:', user)4.3.3 重新赋值 vs 修改内容
这里是一个特别容易混淆的地方。同样是列表,「重新赋值」和「修改内容」效果完全不同。
def change_list1(my_list):
# 修改内容,外部会变化
my_list.append(4)
def change_list2(my_list):
# 重新赋值,外部不会变化
my_list = [1, 2, 3, 4]
a = [1, 2, 3]
change_list1(a)
# 输出: [1, 2, 3, 4]
print(a)
print('--------------华丽的分割线--------------')
b = [1, 2, 3]
change_list2(b)
# 输出: [1, 2, 3]
print(b)append() 是在原对象上修改,而 my_list = [1, 2, 3, 4] 是让形参指向了新对象,和实参不再有关系。
还有一个常见坑:
a += [1]和a = a + [1]对列表的效果不同。+=会调用列表的__iadd__方法,在原对象上修改;而a = a + [1]会创建新列表并重新赋值。
def change_with_iadd(my_list):
my_list += [4]
a = [1, 2, 3]
change_with_iadd(a)
# 输出: [1, 2, 3, 4]
print(a)
print('--------------华丽的分割线--------------')
def change_with_add(my_list):
my_list = my_list + [4]
b = [1, 2, 3]
change_with_add(b)
# 输出: [1, 2, 3]
print(b)4.4 浅拷贝与深拷贝(了解)
当可变对象中包含可变对象时,情况会更复杂一些。比如列表中嵌套列表。
def change_nested(my_list):
my_list[0].append(4)
a = [[1, 2, 3]]
change_nested(a)
# 输出: [[1, 2, 3, 4]]
print(a)如果只是用 copy() 进行浅拷贝,外层列表是新的,但内层列表还是同一个引用。下面用 copy.copy() 演示浅拷贝的效果:
import copy
a = [[1, 2, 3]]
b = copy.copy(a) # 浅拷贝
b[0].append(4)
# 输出: [[1, 2, 3, 4]]
print(a)
# 输出: [[1, 2, 3, 4]]
print(b)如果需要完全独立,可以使用 copy.deepcopy()。
import copy
a = [[1, 2, 3]]
b = copy.deepcopy(a)
b[0].append(4)
# 输出: [[1, 2, 3]]
print(a)
# 输出: [[1, 2, 3, 4]]
print(b)初学者先理解可变与不可变的区别即可,浅拷贝和深拷贝在遇到嵌套结构时再深入学习。
4.5 小结
- Python 参数传递的本质是「传对象引用」。
- 不可变类型传入函数后,函数内部的「修改」实际上是创建新对象,不影响外部。
- 可变类型传入函数后,函数内部通过方法修改内容会影响外部;重新赋值则不会影响外部。
- 区分「修改对象内容」和「重新绑定变量引用」是理解参数传递的关键。
5. 函数的参数
调用函数时可以使用的正式参数类型有:
- 必需参数
- 关键字参数
- 默认参数
- 不定长参数
5.1 必需参数
以位置方式传入时,必需参数须按正确顺序传入,调用时的数量必须和声明时一致;如果使用关键字参数,顺序可以不同。
def print_info(name, age):
print(f'名字: {name}')
print(f'年龄: {age}')
# 正确调用
print_info('juzicoding', 18)
# 缺少参数会报错
# print_info()5.2 关键字参数
使用关键字参数允许函数调用时参数的顺序与声明时不一致,Python 解释器会根据参数名匹配参数值。
def print_info(name, age):
print(f'名字: {name}')
print(f'年龄: {age}')
# 关键字参数,顺序可以调换
print_info(age=18, name='juzicoding')5.3 默认参数
调用函数时,如果没有传递参数,则会使用默认参数。默认参数必须放在没有默认值的参数之后,否则会报错。
def print_info(name, age=18):
print(f'名字: {name}')
print(f'年龄: {age}')
# 传入 age 参数
# 输出: 名字: juzicoding
# 输出: 年龄: 20
print_info('juzicoding', 20)
print('--------------华丽的分割线--------------')
# 不传入 age 参数,使用默认值
# 输出: 名字: juzicoding
# 输出: 年龄: 18
print_info('juzicoding')注意:默认参数不要使用可变类型,否则多次调用时可能产生意外结果。 这是因为默认值在函数定义时只计算一次,后续调用会共享同一个对象。
# 错误示例
# def append_item(item, items=[]):
# items.append(item)
# return items
# 正确做法
def append_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
# 输出: ['a']
print(append_item('a'))
# 输出: ['b']
print(append_item('b'))5.4 不定长参数(*args)
加了星号 * 的参数会把多余的位置参数收集成元组,存放所有未命名的变量参数。
def print_info(arg1, *args):
print('输出: ')
print(arg1)
print(args)
# 输出: 70
# 输出: (60, 50)
print_info(70, 60, 50)
print('--------------华丽的分割线--------------')
# 不传入可变参数时,得到空元组
# 输出: 10
# 输出: ()
print_info(10)习惯上把这个参数命名为
*args,名字可以换,但前面的*不能省。
通常在函数内部会用 for 循环遍历这些参数。
def print_info(arg1, *args):
print('输出: ')
print(arg1)
for var in args:
print(var)
# 输出: 10
print_info(10)
print('--------------华丽的分割线--------------')
# 输出: 70
# 输出: 60
# 输出: 50
print_info(70, 60, 50)5.5 关键字不定长参数(**kwargs)
加了两个星号 ** 的参数会把多余的关键字参数收集成字典,用于接收关键字参数。
def print_info(arg1, **kwargs):
print('输出: ')
print(arg1)
print(kwargs)
# 输出: 1
# 输出: {'a': 2, 'b': 3}
print_info(1, a=2, b=3)习惯上把这个参数命名为
**kwargs,名字可以换,但前面的**不能省。
5.6 仅限位置参数与仅限关键字参数
Python 3.8 新增了 / 语法,表示 / 之前的参数只能使用位置参数,不能使用关键字参数。
def func(a, b, /, c):
print(a, b, c)
# 正确
# 输出: 1 2 3
func(1, 2, 3)
# 错误,a 和 b 必须使用位置参数
# func(a=1, b=2, c=3)单独出现的星号 *,表示 * 之后的参数只能使用关键字参数。
def func(a, b, *, c):
print(a, b, c)
# 正确
# 输出: 1 2 3
func(1, 2, c=3)
# 错误,c 必须使用关键字参数
# func(1, 2, 3)两者也可以组合使用。
def func(a, b, /, c, d, *, e, f):
print(a, b, c, d, e, f)
# 输出: 10 20 30 40 50 60
func(10, 20, 30, d=40, e=50, f=60)组合使用时,参数顺序通常是:仅限位置参数、/、普通参数、*、仅限关键字参数。
6. return 语句
return [表达式](方括号表示表达式可选)语句用于退出函数,选择性地向调用方返回一个表达式。不带参数值的 return 语句返回 None。
def sum_num(arg1, arg2):
total = arg1 + arg2
print(f'函数内: {total}')
return total
# 输出: 函数内: 30
result = sum_num(10, 20)
# 输出: 函数外: 30
print(f'函数外: {result}')Python 函数可以返回多个值,多个值会以元组的形式返回。
def calc(a, b):
return a + b, a - b, a * b
# 输出: (5, 1, 6)
print(calc(3, 2))
# 分别接收
add, sub, mul = calc(3, 2)
print(add)
print(sub)
print(mul)7. 函数练习
最后来几个小练习巩固一下。
7.1 计算最大值
def my_max(a, b):
if a > b:
return a
else:
return b
# 输出: 5
print(my_max(4, 5))7.2 计算矩形面积
def area(width, height):
return width * height
# 输出: 20
print(area(4, 5))7.3 打印任意数量的字符串
def print_strings(*args):
for s in args:
print(s)
# 输出: hello
# 输出: world
# 输出: juzicoding
print_strings('hello', 'world', 'juzicoding')7.4 打印关键字信息
def print_user(name, **kwargs):
print(f'姓名: {name}')
for key, value in kwargs.items():
print(f'{key}: {value}')
# 输出: 姓名: juzicoding
# 输出: age: 18
# 输出: city: 深圳
print_user('juzicoding', age=18, city='深圳')函数是编程中非常重要的概念,把重复的代码抽成函数,可以让代码更简洁、更易维护。多写多练,熟能生巧。