Skip to content

函数的参数 ​

上一节已经会定义和调用函数,但示例中的参数还比较简单。学生信息管理系统继续扩展后,同一个函数可能要接收固定数据、可选数据,或者数量不确定的数据。

如何让调用者更准确地传入数据?如果函数收到的是列表或字典,修改它会不会影响函数外部?本节围绕这些问题整理函数参数的常见用法。

形参与实参 ​

先分清两个名称:

  • 形参:定义函数时,写在函数名后面括号里的参数
  • 实参:调用函数时,真正传入函数的数据
python
def compare_numbers(number1, number2):
    if number1 > number2:
        return f"{number1} > {number2}"
    if number1 == number2:
        return f"{number1} == {number2}"
    return f"{number1} < {number2}"


x = 4
y = 5

print(compare_numbers(x, y))
print(compare_numbers(5, 5))

在这个例子中:

  • number1、number2 是形参
  • x、y 和调用时写入的 5 是实参

如果少传或多传了参数,调用时通常会出现 TypeError:

python
compare_numbers(5)  # TypeError:缺少一个参数

形参占位与实参传值机制示意图

对象引用与数据修改 ​

把列表或数字传入函数后,函数内部的修改是否会影响外部变量?Python 调用函数时,会把对象的引用传给形参,可以结合“不可变对象”和“可变对象”来理解。

不可变对象 ​

整数、浮点数、字符串、布尔值和元组都属于不可变对象。

python
number = 100


def increment(value, step):
    value += step
    print("函数内部:", value)


increment(number, 10)
print("函数外部:", number)

运行结果:

text
函数内部: 110
函数外部: 100

value += step 会让局部变量 value 指向一个新的整数,不会修改外部的 number。

如果希望外部拿到计算结果,应使用 return:

python
def increment(value, step):
    return value + step


number = 100
number = increment(number, 10)
print(number)

可变对象 ​

列表、字典和集合属于可变对象。函数内部修改对象本身,外部也能看到变化:

python
numbers = [100]


def increment_first(data, step):
    data[0] += step


increment_first(numbers, 10)
print(numbers)

运行结果:

text
[110]

再看一个交换列表元素的函数:

python
def exchange(data, index1, index2):
    temp = data[index1]
    data[index1] = data[index2]
    data[index2] = temp


names = ["张三", "李四", "王五"]
exchange(names, 0, 2)
print(names)

运行结果:

text
['王五', '李四', '张三']

exchange() 没有返回新列表,但它直接修改了传入的原列表。

判断有没有修改原数据

只给形参重新赋值,通常不会改变外部变量;通过下标、键或可变对象的方法修改内容,通常会影响原对象。

可变对象与不可变对象作为参数的行为对比

固定参数 ​

函数的形参数量和名称已经确定时,最常用的是位置参数和关键字参数;部分参数还可以设置默认值,让调用更灵活。

位置与关键字传参 ​

位置参数: 实参会按照顺序依次交给形参:

python
def calculate_y(x, k, b):
    return k * x + b


print(calculate_y(5, 5, 6))

调用 calculate_y(5, 5, 6) 时:

  1. 5 传给 x
  2. 5 传给 k
  3. 6 传给 b

位置参数写起来简单,但参数较多时容易记错顺序。

关键字参数: 调用函数时,可以写出形参名称:

python
def calculate_y(x, k, b):
    return k * x + b


print(calculate_y(x=5, k=5, b=6))
print(calculate_y(b=6, x=5, k=5))

关键字参数有两个特点:

  1. 参数含义更清楚
  2. 调用时可以调整顺序

位置参数和关键字参数可以混合使用,但位置参数必须写在关键字参数前面:

python
print(calculate_y(5, k=5, b=6))

同一个参数不能接收两次数据:

python
calculate_y(5, x=6, k=5, b=6)  # TypeError:x 得到了两个值

默认参数 ​

如果某个参数经常使用同一个值,可以在定义函数时设置默认值:

python
def calculate_y(x, k=5, b=6):
    return k * x + b


print(calculate_y(5))
print(calculate_y(5, 2))
print(calculate_y(5, 2, 3))
print(calculate_y(x=5, b=10))

调用时没有传入默认参数,就使用定义时设置的值;如果传入了新值,就覆盖默认值。

没有默认值的参数要写在有默认值的参数前面:

python
def calculate_y(x, k=5, b=6):
    return k * x + b

函数实参到形参的绑定机制示意图

案例:计算总评成绩

需求:期中成绩默认占 40%,期末成绩占 60%,同时允许调用时调整期中权重。

参考答案
python
def calculate_final_score(mid_score, final_score, mid_rate=0.4):
    score = mid_score * mid_rate + final_score * (1 - mid_rate)
    return score


print(f"{calculate_final_score(88, 79):.2f}")
print(f"{calculate_final_score(88, 79, 0.3):.2f}")
print(
    f"{calculate_final_score(mid_score=88, final_score=79, mid_rate=0.3):.2f}"
)

可变默认值共享 ​

默认参数在函数定义时创建,并不是每次调用都重新创建。列表、字典等可变对象作为默认值时,可能在多次调用之间共享数据。

下面这种写法容易出问题:

python
def add_name(name, names=[]):
    names.append(name)
    return names


print(add_name("张三"))
print(add_name("李四"))

运行结果:

text
['张三']
['张三', '李四']

如果每次调用都需要新列表,可以把默认值设为 None:

python
def add_name(name, names=None):
    if names is None:
        names = []
    names.append(name)
    return names

可变默认参数共享与  修复方式

不定长参数 ​

参数数量不确定时,可以使用 *args 收集位置参数,使用 **kwargs 收集关键字参数。

位置参数收集 ​

如果位置参数的数量不确定,可以在形参前加 *,使用 *args 收集这些参数:

python
def show_args(*args):
    print("类型:", type(args))
    print("内容:", args)


show_args()
show_args(3, 2, 1, "开始")

运行结果:

text
类型: <class 'tuple'>
内容: ()
类型: <class 'tuple'>
内容: (3, 2, 1, '开始')

args 是常用名称,也可以换成其他变量名。关键是形参前面的 *,它会把收到的位置参数收集成元组。

如果前面还有普通形参,*args 只收集剩余的位置参数:

python
def show_args(first, second, *args):
    print("first:", first)
    print("second:", second)
    print("args:", args)


show_args(1, 2, 3, 4, 5, 6)

运行结果:

text
first: 1
second: 2
args: (3, 4, 5, 6)

案例:求若干个数的最大值

需求:接收任意多个数字并返回最大值。

功能分析:先用第一个数作为最大值,再遍历 *args 收集的剩余数据。

参考答案
python
def find_max(first, *numbers):
    max_value = first

    for number in numbers:
        if number > max_value:
            max_value = number

    return max_value


print(find_max(1))
print(find_max(1, 2, 11, 2, 5))

这个函数至少要接收一个位置参数。第一个数交给 first,剩余数据被收集到 numbers 元组中。

关键字参数收集 ​

如果关键字参数的数量不确定,可以在形参前加 **,使用 **kwargs 收集这些参数:

python
def show_student(**kwargs):
    print("类型:", type(kwargs))
    print("内容:", kwargs)


show_student(name="张三", age=18, city="长沙")

运行结果:

text
类型: <class 'dict'>
内容: {'name': '张三', 'age': 18, 'city': '长沙'}

kwargs 是 keyword arguments 的缩写。**kwargs 会把收到的关键字参数收集成字典。

 与  的参数收集方式

普通参数、*args 和 **kwargs 也可以组合使用:

python
def show_data(title, *args, **kwargs):
    print("标题:", title)
    print("位置参数:", args)
    print("关键字参数:", kwargs)


show_data("学生信息", 88, 92, name="张三", age=18)

收集与解包:

* 和 ** 出现在不同位置时,作用正好相反:

位置* 的作用** 的作用
定义函数时把多个位置参数收集成元组把多个关键字参数收集成字典
调用函数时把可遍历对象拆成多个位置参数把字典拆成多个关键字参数

调用时使用星号(*)

python
def calculate_total(chinese, math, english):
    return chinese + math + english


scores = [88, 92, 85]
print(calculate_total(*scores))

*scores 相当于依次传入 88、92、85。

调用时使用双星号(**)

python
def show_student(name, age, city):
    print(f"姓名:{name},年龄:{age},城市:{city}")


student = {"name": "张三", "age": 18, "city": "长沙"}
show_student(**student)

字典的键要和函数的形参名对应。**student 相当于调用:

python
show_student(name="张三", age=18, city="长沙")

星号在定义时收集与调用时解包的对称机制示意图

案例:实现简化版 print() ​

print() 可以接收任意多个位置参数,还可以通过 sep 和 end 控制分隔符与结尾。

python
print(1)
print(1, 2, 3, 4)
print(1, 2, 3, 4, sep=",")
print(1, 2, 3, 4, sep=",", end="输出结束\n")

需求:实现一个简化版 changed_print(),支持 print() 的三项能力:

  1. 使用 *args 接收任意多个内容
  2. 使用 sep 设置内容之间的分隔符
  3. 使用 end 设置最后的结尾

功能分析:用 *args 收集要输出的内容,用关键字参数接收 sep 和 end,再按顺序完成输出。

python
def print_one(value):
    print(value, end="")


def changed_print(*args, sep=" ", end="\n"):
    for index, value in enumerate(args):
        print_one(value)

        if index != len(args) - 1:
            print_one(sep)

    print_one(end)


changed_print(1)
changed_print(1, 2, 3, 4)
changed_print(1, 2, 3, 4, sep=",")
changed_print(1, 2, 3, 4, sep=",", end="输出结束\n")

运行结果:

text
1
1 2 3 4
1,2,3,4
1,2,3,4输出结束

sep 和 end 写在 *args 后面,调用时必须使用关键字传入。因此,sep="," 和 end="输出结束\n" 都是关键字参数。

函数对象 ​

如果函数本身也需要被保存、传递或返回,就要把它当作对象处理。函数可以赋值给变量,也可以作为参数或返回值使用。

函数赋值:

python
def calculate_y(x):
    return 5 * x + 6


operation = calculate_y
print(operation(5))

这里没有写小括号,因此赋值的是函数本身。operation 和 calculate_y 指向同一个函数对象。

函数作为参数:

python
def calculate_y(x):
    return 5 * x + 6


def run_operation(operation, value):
    return operation(value)


print(run_operation(calculate_y, 5))

调用时传入的是 calculate_y,不是 calculate_y()。前者表示函数本身,后者表示调用函数后得到的结果。

函数作为返回值:

python
def add(number1, number2):
    return number1 + number2


def get_operation():
    return add


operation = get_operation()
print(operation(10, 20))

这一部分先能看懂即可。后面学习更复杂的函数用法时,还会再次遇到。

函数名对象引用与函数调用求值的本质区别示意图

小结 ​

本节围绕五个问题掌握函数参数:

  1. 如何传递固定数据?使用位置参数或关键字参数。
  2. 如何提供可选数据?使用默认参数,但不要直接使用可变对象作为默认值。
  3. 如何接收数量不确定的位置参数?使用 *args,它会收集成元组。
  4. 如何接收数量不确定的关键字参数?使用 **kwargs,它会收集成字典。
  5. 定义时的 *、** 用于收集,调用时用于解包。

还要注意:函数接收的是对象引用,修改传入的可变对象会影响原数据。

练习 ​

  1. 编写函数,接收 x、y 并返回两数之和,再分别使用位置参数和关键字参数调用
  2. 编写计算总评成绩的函数,把期中成绩权重设置为默认参数
  3. 编写函数,使用 *args 计算任意多个数的和
  4. 编写函数,使用 **kwargs 打印所有关键字参数
  5. 使用一个列表和一个字典,分别练习调用函数时的 * 和 ** 解包