函数基础
函数
-
定义
- 函数是一段封装了特定功能的可重复使用的代码块。
- 通过定义函数,你可以将代码模块化,使其更易于理解、调试和维护。
-
优点
- 为了解决代码的重复书写问题
- 可以将功能的实现者和使用者分开,提高开发效率
- 便于复用与维护、把大问题拆成小问题
-
分类
- 内置函数(built-in):随解释器(就是
python命令启动的那个程序,它逐行读你的 .py 并当场执行)直接可用,不用import,如print、input、abs、len。 - 库函数:住在标准库(装 Python 就自带的模块,如
math、os)或第三方库(要自己pip install的)里,要先import才能用,如math.sqrt、os.getcwd、random.randint。 - 自定义函数:自己封装的函数。
- ⚠️注意:平时把
print、abs叫「库函数」其实不准确:它们是内置函数,在builtins模块里,解释器启动时就加载好了。
- 内置函数(built-in):随解释器(就是
-
格式
1
2def 函数名(参数列表): # 结尾必须是半角冒号 :
... # 占位函数体,写实际代码时替换掉- 参数可以没有,也可以有多个;文档里写成
[参数]的方括号表示「可选」,是记号不是 Python 语法。 - 中文输入法打出的全角冒号
:是新手最常见的SyntaxError来源之一:实测def f():直接报invalid syntax。 ...(Ellipsis)是合法的占位语句,所以模板本身也能编译通过。
- 参数可以没有,也可以有多个;文档里写成
-
函数名
- 遵循标识符的命名规范(与变量名相同)
-
函数调用
- 函数定义以后不会执行,需要调用才执行。
- 调用:
函数名([参数])。 - 定义要写在调用之前(同一个作用域内——作用域指一个名字在哪些位置能看见,函数里定义的名字在外面就取不到):Python 是自上而下执行的,先调用后定义会
NameError。
-
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 无参无返回值
def print_hello():
print('Hello')
# print_hello()
# 带参无返回值
def print_n_hello(n):
for i in range(n):
print('Hello')
# print_n_hello(5)
# 带参有返回值
def add(m, n):
# 返回结果
return m + n
print(add(3, 5)) -
函数参数
- 形参:形式参数,函数定义处的参数
- 实参:实际参数,函数调用处的参数
- 必传参数(位置参数):定义时没有默认值的参数,调用时必须给够,少给一个就
TypeError。按位置传时顺序要和定义一致,但按关键字传就不受顺序限制(show(b=456, a=123)合法)。 - 默认参数:带默认值的参数,必须放在没有默认值的参数后面,否则
SyntaxError。 - 可变长度参数:调用时传的参数比定义的多,多出来的会被打包进
*args和**kwargs——args是元组(收多余的位置参数),kwargs是字典(收多余的关键字参数)。两者同时出现时*args必须写在**kwargs前面。 - 完整顺序:仅限位置参数 → 普通位置参数 → 默认参数 →
*args→ 仅限关键字参数 →**kwargs。Python 3.8+ 还能用/显式标出「仅限位置参数」、用*标出「仅限关键字参数」:1
2
3
4
5def pos_only(a, b, /): # a、b 只能按位置传,pos_only(a=1, b=2) 会 TypeError
return a, b
def kw_only(a, *, b=2): # b 只能用名字传,kw_only(1, 3) 会 TypeError
return a, b - 示例1输出:
1
2
3
4
5
6
7
8
9def show(a, b='默认值'):
# a就是必传参数
print(a)
# b就是默认参数,就是有默认值的参数,必须放在最后
print(b)
# show(1, 2)
# 关键字参数,传递参数的时候指定了名字,顺序就无所谓了
show(b=456, a=123)默认值在定义时求值一次,不是每次调用都重新算;所以默认值千万别用可变对象:1
2123
4561
2
3
4
5
6
7
8
9
10
11
12def bad(x, acc=[]): # 这个列表在所有调用之间共享
acc.append(x)
return acc
bad(1) # [1]
bad(2) # [1, 2] ← 惊了,不是 [2]
def good(x, acc=None): # 惯用写法:默认 None,函数体内再造
if acc is None:
acc = []
acc.append(x)
return acc - 示例2输出:
1
2
3
4
5
6
7
8
9
10
11def var_len_args(a, b, name='默认名称', *args, **kwargs):
print(a)
print(b)
print(name)
# args是一个元组,用来存放所有的多出来的位置参数
print(args)
# kwargs 是字典,收所有多余的「关键字参数」
print(kwargs)
var_len_args(1, 2, 3, 4, 5, age=18)1
2
3
4
51
2
3
(4, 5)
{'age': 18}第三个位置参数
3把name的默认值顶掉了,所以name是 3 而不是「默认名称」;剩下多出来的4, 5进args,age=18进kwargs。
-
*与**:打包与解包同一个符号出现在定义处是打包、出现在调用处是解包,方向正好相反。
1
2
3
4
5
6
7
8
9
10
11
12
13def show(a, b):
print(a, b)
nums = [1, 2]
show(nums[0], nums[1]) # 略显啰嗦
show(*nums) # 解包列表 / 元组:等价于 show(1, 2)
def show2(a='aa', b='bb'):
print(a, b)
d = {'a': 'apple', 'b': 'banana'}
show2(a=d['a'], b=d['b']) # 啰嗦
show2(**d) # 解包字典:按「键 = 值」对上参数名另外几处常见用法:
1
2
3first, *rest = [1, 2, 3] # 解包赋值:rest 得到 [2, 3]
print(*[1, 2, 3], sep='-') # 1-2-3
merged = {**d1, **d2} # 字典合并(3.5+),同名键后者覆盖变量名别用
l、O、I这类单字母:l和数字1在很多字体里几乎一样,PEP 8 也建议避开。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16tu = (1, 2, 3, 4, 5)
def qiuji(length, args):
ji = 1
for i in range(length):
ji *= args[i]
return ji
def calc(func, *args):
print(args)
return func(len(args), args)
print(calc(qiuji, *tu))输出:
1
120
- 这里
calc(func, *args)在定义处用*把多余位置参数打包成元组, calc(qiuji, *tu)在调用处用*把元组拆开传进去,一进一出正好对上。- 顺带一提,这个「求积」在现代 Python 里一行就有:
math.prod(tu),或functools.reduce(operator.mul, tu, 1)。
- 这里
-
返回值
return会立即结束函数;一个函数里可以有多个return,但每次调用只会执行到一个。- 没有
return、或return不带值,函数返回None。 - 「返回多个值」其实是返回一个元组,调用处做序列解包:
1
2
3
4def min_max(seq):
return min(seq), max(seq) # 返回的是元组 (1, 5)
lo, hi = min_max([1, 2, 3, 4, 5]) # 解包赋值 - 返回值可能是
None时,判断要用is None,别用if f():——0、''、[]这些合法结果也会被当成假。
-
文档注释(docstring)
1
2
3
4
5
6
7
8
9
10
11def area(width, height):
"""计算矩形面积。
Args:
width: 宽
height: 高
"""
return width * height
help(area) # 交互式里查文档
print(area.__doc__) # 文档字符串就是函数的 __doc__ 属性 -
作用域
- 查找顺序是 LEGB:局部(Local) → 闭包外层函数(Enclosing) → 全局(Global) → 内置(Built-in)。
- 函数里读全局变量不用声明,但赋值就默认创建局部变量,于是会出现「先读后写」的经典报错:
1
2
3
4
5
6
7
8
9count = 0
def bump():
print(count) # UnboundLocalError:因为下面有 count = ...,count 在整个函数里都是局部名
count = 1
def bump_ok():
global count # 声明要改全局的那个
count += 1 - 改外层函数的变量用
nonlocal(写闭包计数器时常用),global只能改模块级变量。 - 尽量别用
global:函数依赖外部状态后不好测试和复用,把值当参数传进去、再返回出来更清楚。
-
参数是怎么传进去的
Python 既不是 C 那种「值传递」,也不是 C++ 那种「引用传递」,而是传对象引用(按赋值传递):
1
2
3
4
5
6
7
8
9def append_inplace(lst):
lst.append(9) # 改的是调用方那个对象本身 -> 看得见
def rebind(lst):
lst = lst + [9] # 只是让局部名字指向新对象 -> 调用方不受影响
nums = [1]
append_inplace(nums) # nums 变成 [1, 9]
rebind(nums) # nums 还是 [1, 9]- 可变对象(列表、字典、集合)传进函数被原地修改,外面会看到;不可变对象(数字、字符串、元组)改不了。
- 函数内对参数赋值不会传回去;要「返回新值」就
return。
-
递归
- 必须有终止条件,否则爆栈:
RecursionError: maximum recursion depth exceeded。 - 默认递归深度上限约 1000(
sys.getrecursionlimit()可查,sys.setrecursionlimit()可调,但调大只是延后崩溃,栈溢出仍可能发生)。 - 递归能写的循环都能写,Python 没有尾调用优化,深递归改成迭代更稳。
1
2def fact(n):
return 1 if n <= 1 else n * fact(n - 1) # fact(5) = 120 - 必须有终止条件,否则爆栈:
-
函数是对象,以及 lambda
-
函数可以赋值给变量、当参数传、当返回值、放进容器——这是装饰器和高阶函数的基础:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def shout():
return 'hi'
f = shout # 没有括号:把函数本身交给 f
f() # 'hi'
def call_twice(fn): # 函数当参数
return fn() + fn()
def make_mul(n): # 函数当返回值(闭包)
def mul(x):
return x * n
return mul
times3 = make_mul(3)
times3(4) # 12 -
lambda是只能写一个表达式的匿名函数,最常见的用处是给高阶函数当参数:1
2sorted(['bb', 'a', 'ccc'], key=len) # ['a', 'bb', 'ccc']
list(map(lambda x: x * 2, [1, 2, 3])) # [2, 4, 6] -
注意事项:
- 能用具名函数就别用
lambda赋值给变量(f = lambda x: x不如def f(x): return x,报错时栈信息更友好)。 - 本文示例里
calc(qiuji, *tu)就是把函数当参数传的典型用法。
- 能用具名函数就别用
-
-
练习
1
21.前面所有的练习,能用函数的封装成函数
2.封装函数:生成随机的字符串,参数:长度(默认为4),类型(默认为0)