6.函数

4 阅读6分钟

1. 函数的基础概念

一般情况下,在不同地方被重复使用了多次的代码,建议通过函数(function)进行优化,使用 def 关键字定义函数。
调用:函数定义后默认不会执行,只有被调用时才会执行内部代码。函数的定义一定得在调用之前

# 定义函数
def draw_square():
    print("画一个正方形")

# 调用函数
draw_square()

函数的分类

  1. 内置函数:Python 系统自带的,如 type(), max(), len() 等。
  2. 自定义函数:开发者自己编写的。注意不要和内置函数重名

2. 参数 (Parameter)

参数的作用是将外面的数据传递到函数内部进行使用,让函数的功能更加灵活。

  • 形参 (Parameter):定义函数时用来接收数据的变量。
  • 实参 (Argument):调用函数时实际传递的数据。
  • 如果有多个参数,使用逗号 , 隔开。调用时每一个形参都必须有明确的实参
def draw_square(side): # side 是形参
    print(f"画一个边长为 {side} 的正方形")

draw_square(50) # 50 是实参

2.1 参数的默认值

如果某个参数经常是固定值,可以给它设置默认值,这样调用时就可以省略该实参。
限制:带默认值的参数必须放在形参列表的最右边,也就是从右往左必须是连续的。
为什么会有上面的限制呢? 你要中间参数搞一个默认值,我在调用的时间,传过去的参数,方法都不知道怎么一一对应了。

def hello(name, times=1):
    for _ in range(times):
        print(f'Hi, {name}!')

hello('小码哥')      # 默认 times=1
hello('MJ', 3)       # 传入 times=3

2.2 关键字参数

默认是从左往右按顺序传参(位置参数)。也可以按名字传递(关键字参数)。 好处是可读性高,且不需要死记顺序。

def move_circle(x, y, radius):
    pass

move_circle(50, 60, 70)                  # 位置参数
move_circle(radius=70, x=50, y=60)       # 关键字参数,无视顺序

3. 返回值 (Return Value)

根据有无返回值,函数可分为:

  • 有返回值的函数:执行完毕会返回具体数据(如 max, sum)。
  • 无返回值的函数:执行完毕不返回具体数据(如 print),相当于默认返回了 None

return 关键字

作用有两点:

  1. 返回函数的结果。
  2. 立即终止函数的运行(遇到 return 函数就结束了)。
def odd_sum(n):
    return sum([i for i in range(1, n + 1, 2)])

同时返回多个数据

可以通过返回元组来实现返回多个数据。

def odd_even_sum(n):
    o_sum = sum([i for i in range(1, n, 2)])
    e_sum = sum([i for i in range(2, n, 2)])
    return o_sum, e_sum  # 实际上返回的是一个元组 (o_sum, e_sum)

odd, even = odd_even_sum(10) # 解包获取

4. 空语句 pass

pass 相当于空代码,啥也不干。如果函数体还没想好怎么写,可以先用 pass 占位,防止语法报错

def sleep():
    pass

5. 可变参数与参数解包

5.1 可变参数 * (打包为元组)

记住 * 用于接收任意数量的位置参数,可变参数的作用就是说我的参数个数不确定,方法在调用的时间将可变参数打包成一个元祖

def hello(*names):
    for n in names:
        print(f'Hi, {n}!')

hello('MJ', 'Jack', 'Rose') # 内部 names 是 ('MJ', 'Jack', 'Rose')

注意:可变参数右边的形参,如果想传值,必须使用关键字参数!且可变参数不能设置默认值。

hello('MJ','Jack','Title', title='哈罗')

上面的 title参数必须这么写成title='哈罗'

5.2 关键字可变参数 ** (打包为字典)

** 用于接收任意数量的关键字参数。可以理解为 * 打包参数打包成元祖,两个** 打包成字典

def show_info(**info):
    if 'name' in info:
        print('姓名 =', info['name'])

show_info(city='广州', name='MJ', age=18)
# 内部 info 是 {'city': '广州', 'name': 'MJ', 'age': 18}

*** 混用时,* 形参必须放在 ** 形参的左边!

5.3 强制使用关键字参数

在形参中写一个单独的 *,那么它右边的参数必须全部强制使用关键字传参。其实还是前面说的,可变参数后面的其他参数,在调用的时间必须指名名字,要不函数对不上谁应该传给谁

def move_circle(*, x=0, y=0, radius=50):
    pass

# move_circle(10, 20, 30) # 报错
move_circle(x=10, y=20, radius=30) # 必须这样写

5.4 函数调用时的解包实参 (***)

上面说的是方法定义的时间使用 * 和 ** ,这里指的是在方法调用的时间,在调用的时候* 解包可迭代对象(列表、元组、集合)当做位置实参。** 解包字典对象当做关键字实参。

def move_circle(x, y, radius):
    pass

move_circle(*[50, 60, 70])  # 等价于 move_circle(50, 60, 70)

d = {'x': 10, 'y': 20, 'radius': 30}
move_circle(**d)            # 等价于 move_circle(x=10, y=20, radius=30)

6. 引用传递 (Pass-by-reference)

Python 将实参传递给形参时,使用的是引用传递(实参和形参指向同一个内存对象)。 后果: 如果传递的是可变类型(如列表),在函数内部使用方法(如 append)或者索引赋值修改了该对象,外部的变量也会跟着改变。 但如果是重新给形参赋值(如 a = [1, 2],则不会影响外部变量。

def test(a):
    a.append(66) # 外部列表跟着变
    
s = [11, 22]
test(s)
print(s) # [11, 22, 66]

7. 类型标注 (Type Annotation)与文档注释

Python 函数可以接收任意类型参数,为了防止传错类型,可以使用类型标注提高可读性并获得编辑器的语法提示(但它仅仅是注释,对执行没有实质约束力)。

def avg(a: float, b: float) -> float:
    """
    求a和b的平均值
    :param a: 第1个数值
    :param b: 第2个数值
    :return: 平均值
    """
    return (a + b) / 2

# 查看文档和标注
print(avg.__doc__)         # 打印文档字符串
print(avg.__annotations__) # 打印类型标注字典

8. 作用域 (Scope) 与 global/nonlocal

根据变量定义位置的不同,其作用范围分为:

  1. 局部 (Local):定义在函数内部,只能在函数内部使用。
  2. 全局 (Global):定义在函数外部,整个 Python 文件都可使用。
  3. 内置 (Built-in):Python 系统内置。 搜索顺序(就近原则):局部 -> 全局 -> 内置

global 关键字

如果在函数中要修改全局变量的值,需要先用 global 声明。

k = 10
def test():
    global k
    k = 20 # 这样就会真正修改外部的 k

内部函数与 nonlocal 关键字

函数里面还可以再嵌套定义函数。 如果在内部函数中要修改外部函数的局部变量,需要用 nonlocal 声明。

def test1():
    n = 20
    def test2():
        nonlocal n
        n = 200 # 这样就会修改 test1 里的 n
    test2()