函数
- 单一功能的封装。
- 实现代码复用。
Python语言中使用函数分为两个步骤:定义函数和调用函数。
定义函数,即根据函数的输入、输出和数据处理完成函数代码的编写。
定义函数只是规定了函数会执行什么操作,但并不会真正去执行。
调用函数,即真正去执行函数中的代码,是根据传入的数据完成特定的运算,并将运算结果返回到函数调用位置的过程。
定义函数
1 | def functionname([parameters]): |
函数命名规范和变量命名一样
- 必须使用字母或者下划线
_
开头 - 仅能含有字母、数字和下划线
调用函数
语法格式:函数名称(), 括号中传入参数值。
1 | def print_things(name): |
函数的返回值
定义了函数之后,我们调用它来获得返回值
1 | def square_new(x): |
1 | def square_new(x): |
函数调用过之后进行返回的值,就是返回值。
- 如果不显式使用
return
语句或return
语句不使用表达式,
那么函数返回None
- 要从函数中返回多个值,只要简单地返回一个元组即可。
1 | def division(): |
观察 return 语句,尽管看起来
division()
返回了多个值,但实际上它只创建了一个元组而已。实际上元组是通过逗号来组成的,不是圆括号。
当调用的函数返回了元组,通常会将结果赋值给多个变量,实际上就是简单的元组解包。返回的值也可以赋给一个单独的变量
1 | x = division() |
函数是一等对象
“一等对象”定义为满足下列条件的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
把函数视作对象
1 | # 创建并测试一个函数,然后读取它的__doc__属性,再检查它的类型 |
每个函数都有一个 __doc__
属性,用于存储函数的文档字符串(docstring)。文档字符串是对函数功能、参数、返回值等的描述,有助于代码的可读性和维护性。
1 | print(factorial.__doc__) |
在我们定义的 factorial
函数中,文档字符串是
"return n!"
,打印 factorial.__doc__
会输出这个字符串,帮助其他开发者(甚至是未来的自己)理解函数的功能。
在 Python 中,函数是 function
类型的对象。我们可以使用
type
函数来检查一个函数的类型:
1 | print(type(factorial)) |
执行上述代码会输出 <class 'function'>
,表明
factorial
是一个函数对象。
还可以通过别的名称使用函数,再把函数作为参数传递
1 | fact = factorial |
展示了函数对象的“一等”本性,我们可以把factorial
函数赋值给变量fact
,然后通过变量名调用。
还可以把它作为参数传递:
1 | # map函数返回一个可迭代对象,里面的元素是把第一个参数(一个函数)应用到 |
高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)
示例1:根据单词长度给一个列表排序,只需把
len
函数传给key
参数。
1 | fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana'] |
任何单参数函数都能作为key
参数的值。
总结
- 函数定义以 def 关键词开头,后接函数标识符、圆括号()和冒号
- 圆括号之间用于定义输入参数(可选)
- 函数体必须缩进,第一行可以使用文档字符串(用于存放函数说明)
- 函数是一等对象
- 调用函数时,Python
会执行函数内部的代码,
函数执行完之后,返回到主程序。
函数的参数
从函数定义和调用的角度可以将参数分为:
- 形式参数
- 实际参数
从参数在函数中的具体使用可分为下面 4 种类型:
- 位置参数:
def func(a, b): pass
- 关键字参数:
def func(a, b=1): pass
- 任意位置参数:
def func(a, b=1, *c): pass
- 任意关键字参数:
def func(a, b=1, *c, **d): pass
位置参数
位置参数是 Python 函数调用中最基础的参数传递方式,它通过参数的位置来确定实参与形参的对应关系。调用函数时,传入的实参顺序必须与函数定义时形参的顺序严格一致。
1 | def add_numbers(a, b): |
在上述代码中,add_numbers
函数定义了两个形参a
和b
。调用函数时,实参3
对应形参a
,实参5
对应形参b
,这就是通过位置建立的参数对应关系。如果调用时改变实参顺序,如add_numbers(5, 3)
,虽然也能正确执行,但传递的参数含义会发生变化。
关键字参数
关键字参数允许在调用函数时通过参数名指定对应的值,这样可以避免因位置顺序错误导致的参数传递混乱,并且实参顺序可以与函数定义时形参顺序不同。
1 | def greet(name, message): |
上述代码中,greet
函数有name
和message
两个形参。调用时通过name=值
、message=值
的形式明确指定参数对应关系,即使交换实参顺序,也不影响参数的正确传递。
位置参数和关键字参数还可以混合使用,但要遵循位置参数必须在前、关键字参数在后的原则。例如:
1 | def divide(dividend, divisor): |
这里10
是位置参数对应dividend
,divisor=2
是关键字参数,位置和关键字参数各司其职,共同完成函数调用。
默认参数
默认参数是指在函数定义时,为形参指定一个默认值。当调用函数时,如果没有为该形参传递对应的实参,那么这个形参就会自动使用预先设定的默认值。
1 | def say_hello(name="陌生人"): |
在say_hello
函数中,name
形参的默认值为"陌生人"
。当直接调用say_hello()
时,name
使用默认值;而调用say_hello("Charlie")
时,name
被赋值为"Charlie"
,默认值被覆盖。
任意位置参数
任意位置参数可以接受任意数量的位置参数。
将一组可变数量的位置参数集合成参数值的元组。
1 | def calc(*numbers): |
任意关键字参数
任意关键字参数允许传入0个或任意个含参数名的参数
这些关键字参数在函数内部自动组装为一个字典。
1 | def person(name, age, **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 | def calc(*numbers): |
1 | # 参数已经存储在列表 l1 或元组 l2 |
注释掉的
calc(l1[0], l1[1], l1[2])
是一种传统的函数调用方式,需要依次写出列表中的每个元素作为参数传入。但这种方式在列表元素较多时会很繁琐。calc(*l2)
这里使用了*
操作符对元组l2
进行解包(拆分)。*
操作符会将元组l2
中的元素逐一提取出来,作为位置参数传递给calc
函数,等同于calc(1, 2, 3, 4, 5, 6)
。这样就实现了从元组中拆分参数来调用函数,避免了手动逐个列出参数的麻烦,当参数数量较多或者参数存储在列表、元组中时,这种方式更加简洁高效。
字典拆分出来的结果作为关键字参数
1 | def person(name, age, **kw): |
函数的实参传递
Python中“一切皆对象”,所有赋值操作都是“引用的赋值”。
Python实参总是通过引用传递的(通过对象传递)。当函数调用提供一个实参时,Python 将实参对象的*引用*(而不是对象本身)复制到相应的形参中。
这将大大提高性能。函数经常对大型对象进行操作——频繁地复制它们将消耗大量计算机内存并显著降低程序性能。
将对象传递给函数
1 | # 定义一个 cube 函数来显示其参数的标识,并返回该参数值的立方 |
cube 的形参number的标识与前面 x 显示的相同。因为每个对象都有唯一的标识,所以,在 cube 执行时,实参 x 和形参 number 都引用同一个对象。因此,当 cube 在计算中使用形参number时,它将从调用者中的原始对象中获取number值。
1 | # 还可以使用 Python 中的 is 操作符证明实参和形参引用相同的对象 |
从参数对象的类型来看可以将参数分为以下两类:
- 传递可变对象的引用
- 传递不可变对象的引用
传递不可变对象的引用
当一个函数接收一个不可变对象的引用作为参数时(例如整数、浮点数、字符串或元组),这意味着一旦它们被创建,其值就不能被修改。
当把不可变对象作为参数传递给函数时,函数接收到的是该对象的引用。但即便在函数内部操作这个引用,也无法改变原始对象的值。
例如,将一个整数传递给函数,在函数内对这个整数进行重新赋值等操作,不会影响到函数外部原始的整数变量。因为对不可变对象的操作,实际上是重新创建了一个新的对象,而不是修改原来的对象。
1 | b = 12 |
传递可变对象的引用
列表、字典等在 Python 里是可变对象,它们的值可以被修改。
当把可变对象作为参数传递给函数时,传递的同样是对象的引用。函数可以通过这个引用直接修改对象内部的值。
传递参数是不论是可变对象还是不可变对象,实际传递的还是对象的引用。
注意:在定义函数时,不要把可变的数据类型(列表、字典)当作关键字参数的参数值。
1 | def test0(n, alist=[]): |
如何避免这种情况
1 | def test0(n, alist=None): |
函数参数示例:
现在有一个分类器,我们用它来解决二分类问题,现在在测试集上的测试结果如下:
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 | def evaluate_classifier(predicted_labels, true_labels): |