函数

  1. 单一功能的封装。
  2. 实现代码复用。

Python语言中使用函数分为两个步骤:定义函数和调用函数。

  • 定义函数,即根据函数的输入、输出和数据处理完成函数代码的编写。

    定义函数只是规定了函数会执行什么操作,但并不会真正去执行。

  • 调用函数,即真正去执行函数中的代码,是根据传入的数据完成特定的运算,并将运算结果返回到函数调用位置的过程。

定义函数

1
2
3
4
5
def functionname([parameters]):
"""docstring
"""
function_suite
[return [expression]]

函数命名规范和变量命名一样

  • 必须使用字母或者下划线_开头
  • 仅能含有字母、数字和下划线

调用函数

语法格式:函数名称(), 括号中传入参数值。

1
2
3
4
def print_things(name):
print(name , 'hello,world!')

print_things("ppx")

函数的返回值

定义了函数之后,我们调用它来获得返回值

1
2
3
4
5
def square_new(x):
x * x
return # 在执行return语句终止函数时没有伴随一条表达式

print(square_new(3))
1
2
3
4
def square_new(x):
x * x # 没有return语句

print(square_new(3))

函数调用过之后进行返回的值,就是返回值。

  • 如果不显式使用 return 语句或 return 语句不使用表达式,
    那么函数返回None
  • 要从函数中返回多个值,只要简单地返回一个元组即可。
1
2
3
4
5
6
7
8
9
10
def division():
"""求商与余数
"""
a = 9 % 4
b = (9 - a) / 4
return b, a

b, a = division()
print(b)
print(a)

观察 return 语句,尽管看起来 division()返回了多个值,但实际上它只创建了一个元组而已。实际上元组是通过逗号来组成的,不是圆括号。

当调用的函数返回了元组,通常会将结果赋值给多个变量,实际上就是简单的元组解包。返回的值也可以赋给一个单独的变量

1
2
x = division()
print(x) # x就代表整个元组

函数是一等对象

“一等对象”定义为满足下列条件的程序实体:

  1. 在运行时创建
  2. 能赋值给变量或数据结构中的元素
  3. 能作为参数传给函数
  4. 能作为函数的返回结果

把函数视作对象

1
2
3
4
5
# 创建并测试一个函数,然后读取它的__doc__属性,再检查它的类型
def factorial(n):
"""return n!"""
print(f"计算 {n} 的阶乘")
return 1 if n < 2 else n * factorial(n - 1)

每个函数都有一个 __doc__ 属性,用于存储函数的文档字符串(docstring)。文档字符串是对函数功能、参数、返回值等的描述,有助于代码的可读性和维护性。

1
print(factorial.__doc__)

在我们定义的 factorial 函数中,文档字符串是 "return n!",打印 factorial.__doc__ 会输出这个字符串,帮助其他开发者(甚至是未来的自己)理解函数的功能。

在 Python 中,函数是 function 类型的对象。我们可以使用 type 函数来检查一个函数的类型:

1
print(type(factorial))

执行上述代码会输出 <class 'function'>,表明 factorial 是一个函数对象。

还可以通过别的名称使用函数,再把函数作为参数传递

1
2
fact = factorial
fact(5)

展示了函数对象的“一等”本性,我们可以把factorial函数赋值给变量fact,然后通过变量名调用。

还可以把它作为参数传递:

1
2
3
4
5
# map函数返回一个可迭代对象,里面的元素是把第一个参数(一个函数)应用到
# 第二个参数(一个可迭代对象,这里是 range(11))中各个元素上得到的结果
map(factorial, range(5))
# help(map)
list(map(factorial, range(5)))

高阶函数

接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)

示例1:根据单词长度给一个列表排序,只需把 len函数传给key参数。

1
2
3
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)
help(sorted)

任何单参数函数都能作为key参数的值。

总结

  • 函数定义以 def 关键词开头,后接函数标识符、圆括号()和冒号
  • 圆括号之间用于定义输入参数(可选)
  • 函数体必须缩进,第一行可以使用文档字符串(用于存放函数说明)
  • 函数是一等对象
  • 调用函数时,Python 会执行函数内部的代码,
    函数执行完之后,返回到主程序。

函数的参数

从函数定义和调用的角度可以将参数分为:

  • 形式参数
  • 实际参数

从参数在函数中的具体使用可分为下面 4 种类型:

  1. 位置参数:def func(a, b): pass
  2. 关键字参数:def func(a, b=1): pass
  3. 任意位置参数:def func(a, b=1, *c): pass
  4. 任意关键字参数:def func(a, b=1, *c, **d): pass

位置参数

位置参数是 Python 函数调用中最基础的参数传递方式,它通过参数的位置来确定实参与形参的对应关系。调用函数时,传入的实参顺序必须与函数定义时形参的顺序严格一致。

1
2
3
4
5
6
def add_numbers(a, b):
"""返回两个数的和"""
return a + b

result = add_numbers(3, 5)
print(result)

在上述代码中,add_numbers函数定义了两个形参ab。调用函数时,实参3对应形参a,实参5对应形参b ,这就是通过位置建立的参数对应关系。如果调用时改变实参顺序,如add_numbers(5, 3),虽然也能正确执行,但传递的参数含义会发生变化。

关键字参数

关键字参数允许在调用函数时通过参数名指定对应的值,这样可以避免因位置顺序错误导致的参数传递混乱,并且实参顺序可以与函数定义时形参顺序不同。

1
2
3
4
5
6
7
8
def greet(name, message):
"""打印问候语"""
print(f"{name}, {message}")

# 正常顺序使用关键字参数
greet(name="Alice", message="欢迎!")
# 打乱顺序使用关键字参数
greet(message="你好!", name="Bob")

上述代码中,greet函数有namemessage两个形参。调用时通过name=值message=值的形式明确指定参数对应关系,即使交换实参顺序,也不影响参数的正确传递。

位置参数和关键字参数还可以混合使用,但要遵循位置参数必须在前、关键字参数在后的原则。例如:

1
2
3
4
5
6
7
def divide(dividend, divisor):
"""返回除法结果"""
return dividend / divisor

# 混合使用示例
result = divide(10, divisor=2)
print(result)

这里10是位置参数对应dividenddivisor=2是关键字参数,位置和关键字参数各司其职,共同完成函数调用。

默认参数

默认参数是指在函数定义时,为形参指定一个默认值。当调用函数时,如果没有为该形参传递对应的实参,那么这个形参就会自动使用预先设定的默认值。

1
2
3
4
5
6
7
8
def say_hello(name="陌生人"):
"""打印问候语,若未传入名字则使用默认值"""
print(f"你好,{name}!")

# 未传入参数,使用默认值
say_hello()
# 传入参数,覆盖默认值
say_hello("Charlie")

say_hello函数中,name形参的默认值为"陌生人"。当直接调用say_hello()时,name使用默认值;而调用say_hello("Charlie")时,name被赋值为"Charlie",默认值被覆盖。

任意位置参数

任意位置参数可以接受任意数量的位置参数。

将一组可变数量的位置参数集合成参数值的元组。

1
2
3
4
5
6
7
8
9
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
print(numbers)
return sum

calc(1, 2, 3, 4, 5, 6)
calc(1, 2, 3, 4, 5, 6, 7, 8)

任意关键字参数

任意关键字参数允许传入0个或任意个含参数名的参数

这些关键字参数在函数内部自动组装为一个字典。

1
2
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)

这时候你可以传入任意个数的关键字参数:

1
person('Jane', 6, city='shijiazhuang', gender='F', weight='30kg')
1
name: Jane age: 6 other: {'city': 'shijiazhuang', 'gender': 'F', 'weight': '30kg'}

使用**可以将多个关键字参数收集到一个字典中,参数的名字是字典的键,值是字典的值。

通常把 任意位置参数任意关键字参数 称为 可变参数不定长参数

一般不定长参数会写成 *args**kwargs

  • 只有星号是必要的,args 和 kwargs 是约定俗成的

拆分参数列表

如果一个函数所需要的参数已经存储在了列表、元组或字典中,则可以直接从列表、元组或字典中拆分出来函数所需要的这些参数。

列表、元组拆分出来的结果作为位置参数

1
2
3
4
5
6
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
print(numbers)
return sum
1
2
3
4
5
6
# 参数已经存储在列表 l1 或元组 l2
l1 = [1, 2, 3]
l2 = (1, 2, 3, 4, 5, 6)

# calc(l1[0], l1[1], l1[2])
calc(*l2)
  • 注释掉的 calc(l1[0], l1[1], l1[2]) 是一种传统的函数调用方式,需要依次写出列表中的每个元素作为参数传入。但这种方式在列表元素较多时会很繁琐。

  • calc(*l2) 这里使用了 * 操作符对元组 l2 进行解包(拆分)。* 操作符会将元组 l2 中的元素逐一提取出来,作为位置参数传递给 calc 函数,等同于 calc(1, 2, 3, 4, 5, 6) 。这样就实现了从元组中拆分参数来调用函数,避免了手动逐个列出参数的麻烦,当参数数量较多或者参数存储在列表、元组中时,这种方式更加简洁高效。

字典拆分出来的结果作为关键字参数

1
2
3
4
5
6
7
8
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)

# 参数已经存储在字典 extra 中
extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 6, **extra)

# name: Jack age: 6 other: {'city': 'Beijing', 'job': 'Engineer'}

函数的实参传递

Python中“一切皆对象”,所有赋值操作都是“引用的赋值”。

Python实参总是通过引用传递的(通过对象传递)。当函数调用提供一个实参时,Python 将实参对象的*引用*(而不是对象本身)复制到相应的形参中。

这将大大提高性能。函数经常对大型对象进行操作——频繁地复制它们将消耗大量计算机内存并显著降低程序性能。

将对象传递给函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 定义一个 cube 函数来显示其参数的标识,并返回该参数值的立方
def cube(number):
print('id(number):', id(number))
return number ** 3

# 首先创建整型变量 x,后续使用 x 作为函数实参;
# x引用(指向)存储数值 7 的整数对象。
x = 7
# 获取对象标识
id(x)
# 使用参数 x 调用 cube, x指向存储数值 7 的整数对象。
cube(x)

# id(x): 4311131544
# id(number): 4311131544
# 343

cube 的形参number的标识与前面 x 显示的相同。因为每个对象都有唯一的标识,所以,在 cube 执行时,实参 x 和形参 number 都引用同一个对象。因此,当 cube 在计算中使用形参number时,它将从调用者中的原始对象中获取number值。

1
2
3
4
5
6
7
8
9
# 还可以使用 Python 中的 is 操作符证明实参和形参引用相同的对象
x = 7 # x是全局变量
def cube(number):
print('number is x:', number is x)
return number ** 3

cube(x)
# number is x: True
# 343

从参数对象的类型来看可以将参数分为以下两类:

  1. 传递可变对象的引用
  2. 传递不可变对象的引用

传递不可变对象的引用

当一个函数接收一个不可变对象的引用作为参数时(例如整数、浮点数、字符串或元组),这意味着一旦它们被创建,其值就不能被修改。

当把不可变对象作为参数传递给函数时,函数接收到的是该对象的引用。但即便在函数内部操作这个引用,也无法改变原始对象的值。

例如,将一个整数传递给函数,在函数内对这个整数进行重新赋值等操作,不会影响到函数外部原始的整数变量。因为对不可变对象的操作,实际上是重新创建了一个新的对象,而不是修改原来的对象。

1
2
3
4
5
6
7
8
9
10
11
12
b = 12
print(id(b))

def test0(m):
print(id(m))
m += 1 # 实际上创建了一个新对象,然后将新对象的引用赋值给形参 m
print(id(m))
print(m)

test0(b)
print(b)
print(m)

传递可变对象的引用

列表、字典等在 Python 里是可变对象,它们的值可以被修改。

当把可变对象作为参数传递给函数时,传递的同样是对象的引用。函数可以通过这个引用直接修改对象内部的值。

传递参数是不论是可变对象还是不可变对象,实际传递的还是对象的引用。

注意:在定义函数时,不要把可变的数据类型(列表、字典)当作关键字参数的参数值。

1
2
3
4
5
6
7
def test0(n, alist=[]):
alist.append(n)
return alist

print(test0(1))
print(test0(2)) #[1, 2]
print(test0(3)) #[1, 2, 3]

如何避免这种情况

1
2
3
4
5
6
7
8
9
def test0(n, alist=None):
if alist is None:
alist = []
alist.append(n)
return alist

print(test0(1)) #[1]
print(test0(2)) #
print(test0(3))

函数参数示例:

现在有一个分类器,我们用它来解决二分类问题,现在在测试集上的测试结果如下:

predicted_labels = [1, 0, 0, 1, 0, 1, 1, 0, 0, 0]

真实标签为:

true_labels = [1, 0, 1, 1, 0, 1, 0, 0, 0, 1]

请设计一个函数,用来进行模型评估,输入为预测标签列表和真实标签列表,输出模型预测正确率,查准率和查全率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def evaluate_classifier(predicted_labels, true_labels):
"""
评估二分类模型的性能

参数:
predicted_labels: 模型预测的标签列表
true_labels: 真实的标签列表

返回:
dict: 包含准确率(accuracy)、精确率(precision)和召回率(recall)的字典
"""
if len(predicted_labels) != len(true_labels):
raise ValueError("预测标签和真实标签的长度必须相同")

# 初始化计数器
true_positives = 0 # 真正例
false_positives = 0 # 假正例
false_negatives = 0 # 假负例
total_samples = len(true_labels)
correct_predictions = 0

# 计算各类样本数量
for pred, true in zip(predicted_labels, true_labels):
if pred == true:
correct_predictions += 1

if pred == 1 and true == 1:
true_positives += 1
elif pred == 1 and true == 0:
false_positives += 1
elif pred == 0 and true == 1:
false_negatives += 1

# 计算评估指标
accuracy = correct_predictions / total_samples

# 处理精确率的边界情况(避免除以零)
if (true_positives + false_positives) == 0:
precision = 0
else:
precision = true_positives / (true_positives + false_positives)

# 处理召回率的边界情况(避免除以零)
if (true_positives + false_negatives) == 0:
recall = 0
else:
recall = true_positives / (true_positives + false_negatives)

return {
"accuracy": accuracy,
"precision": precision,
"recall": recall
}

# 使用你提供的数据
predicted_labels = [1, 0, 0, 1, 0, 1, 1, 0, 0, 0]
true_labels = [1, 0, 1, 1, 0, 1, 0, 0, 0, 1]

results = evaluate_classifier(predicted_labels, true_labels)
print(f"准确率 (Accuracy): {results['accuracy']:.3f}")
print(f"精确率 (Precision): {results['precision']:.3f}")
print(f"召回率 (Recall): {results['recall']:.3f}")