发布日期:2025-08-26 20:15点击次数:
函数
函数是把完成特定功能的代码块封装起来,取个名字,以方便其它代码直接调用,就不用重复编写完成该任务的代码了。
函数有2个重要的知识点,首先学习的是参数的传递,第二个就是函数存储到模块里,尤其是在类里(改名为方法)。
每个函数都应只负责一项具体的工作,这优于使用一个函数来完成两项工作。编写函数时,如果发现它执行的任务太多,请尝试将这些代码划分到两个函数中。
函数定义
def function_name():
关键字def说明要定义一个函数。
function_name为函数名,根据完成的任务取一个合适的名字;
括号内为需要传递的参数,指出函数为完成其任务需要什么样的信息。即使不需要任何信息,括号是空但也必不可少。
最后,定义以冒号结尾,紧跟在后面的所有缩进行构成了函数体。
参数传递
形参与实参
def fun1(arg)
定义函数func(arg)时,括号里的变量arg是一个形参,没有真实的信息,只是作为一个符号用于函数体内;
那么真正的参数信息在哪里呢?就是在调用函数时候括号里的变量,函数调用时,直接函数名+括号内参数(若函数有返回值则前面加个变量进行接收)。
real_arg=1
func(real_arg)
变量‘real_arg’是一个实参,是真正存储信息的。
我们调用函数时,在function(‘real_arg’)中,将实参‘real_arg’传递给了函数function(arg),这个值被存储在形参arg中。
二者的对比如下:
作用域:
- 形参:作用域在函数内部,是函数的局部变量。
- 实参:可以在函数外部定义,作用域根据定义的位置而定。
存在时间:
- 形参:在函数被调用时创建,函数执行结束后销毁。
- 实参:在函数调用前就已存在(除非是字面量),函数调用后通常仍然存在(除非在函数内部被修改且是可变对象,但实参变量本身不会销毁)。
绑定方式:
- 形参:在函数定义时只是占位符,没有具体的值。
- 实参:必须具有确定的值(可以是表达式的结果),该值会被传递给形参。
位置参数(Positional Arguments)和关键字参数(Keyword Arguments)
鉴于函数定义中可能包含多个形参,因此函数调用中也可能包含多个实参。向函数传递实参的方式很多,那么这些参数有什么区别吗?
是有的,我们想一下,事实上函数传递实参的目的是为了让形参接收到对应的实参,也就是说需要保证实参和形参是一一对应的,而为了保证这一点,函数设计了2种类型的参数,分为位置参数和关键字参数(可理解为列表的按下标和字典的键值对来确认对应关系)。
位置参数是在函数定义时按照顺序依次列出的参数,调用函数时,传递的参数必须按照定义时的顺序一一对应,参数的位置决定了它们会被赋值给哪个形参。位置参数有2个特性:
· 顺序性:调用函数时,实参的顺序必须和形参的顺序一致。
· 数量匹配:传递的实参数量必须和形参数量相同(除非有默认值)。
def add_numbers(a, b):
return a + b
# 调用函数,按照位置传递参数
result = add_numbers(3, 5)
print(result) # 输出: 8
从上面的代码可以看出来,位置参数可以省略实参的变量名称,直接按照顺序和形参一一对应。
关键字参数是在调用函数时,通过指定参数名来传递参数的方式,参数名和值之间用等号连接。使用关键字参数时,参数的传递顺序可以和函数定义时的顺序不同。
· 顺序无关性:可以不按照函数定义时的参数顺序传递参数,只要指定了参数名即可。
· 可选择性:可以只传递部分参数,前提是其他参数有默认值。
def describe_person(name, age, city):
print(f"{name} 今年 {age} 岁,住在 {city}。")
# 使用关键字参数调用函数
describe_person(age=25, city="New York", name="Alice")#调用顺序可以不同
结合使用位置参数和关键字参数
在调用函数时,可以同时使用位置参数和关键字参数,但位置参数必须放在关键字参数之前。
describe_person("Alice",age=25, city="New York" )#位置参数在前
默认参数
调用函数时候传递的实参数量是不是一定和形参一致呢?答案是否定的。看一些源码的时候,经常可以注意到,为了保证尽可能的全面,内部函数在定义时候会设置各种各样的情况,不同的情况对应不同的参数,但是在调用函数时候,往往并不需要传递相同数量的参数,这是因为内部函数定义时候给很多形参指定了默认值。
编写函数时,可给每个形参指定默认值。
def fun3(a,b=2):
在调用函数中给形参提供了实参时,Python将使用指定的实参值;否则,将使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参。使用默认值可简化函数调用,还可清楚地指出函数的典型用法。
提供的实参多于或少于函数完成其工作所需的信息(不一定是形参数,要看是否有默认值)时,将出现实参不匹配错误。这也是经常出现的错误。
*args和**kwargs
前面强调实参和形参需要一一对应,或者通过位置,或者通过关键字,那如果预先不知道函数需要接收多个实参怎么办呢?简单,Python给函数提供了2个可装任意数量实参的大包,并分别起了个名字,接收位置参数的大包命名为*args(其实就是空元组),接收关键字参数的大包命名为**kwargs(其实就是空字典)。
那么函数在定义时就变成了下面这样:
def func2(a,b,*args,**kwargs):
问题又来了,这些参数没有名字在函数体内部怎么使用呢?
就是把*args和**kwargs当做元组和字典去遍历,得到所有的参数。位置实参使用形参*args,星号让Python创建一个名为args的空元组,并将收到的所有值都封装到这个元组中。关键字实参使用形参**kwargs,两个星号让Python创建一个名为kwargs的空字典,并将收到的所有名称—值对都封装到这个字典中。
*args使用实例
def sum_numbers(*args):
total = 0
for num in args:
total += num
return total
# 调用时可以传递不同数量的位置实参
result1 = sum_numbers(1, 2, 3)
result2 = sum_numbers(10, 20, 30, 40)
result3 = sum_numbers(5)
**kwargs 的2种使用实例
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
# 调用函数并传入不同的关键字参数
print_info(name="Alice", age=25, city="New York")
print_info(product="Laptop", price=1000, brand="Dell")
还有一种常用的使用**kwargs参数的方法是结合setattr给对象属性赋值
def setattr_with_kwargs(object,**kwargs):
for k, v in kwargs.items(): #用于设置属性值,该属性不一定是存在的。
setattr(object, k, v) 实例说明一下,输出结果
setattr(object, k, v)是 Python 的内置函数,它的作用是给对象object设置一个属性,属性名是k,属性值是v。如果该属性原本不存在,就会创建这个新属性;如果原本存在,就会更新其值。
函数定义时的参数顺序规则
前面主要介绍的是调用函数时候的实参类型,为了配合调用函数时候传递参数的不确定性,在函数定义时,需要设定参数的顺序,一般为:位置参数、默认参数、*args(可变位置参数)、关键字参数(需指定参数名)、**kwargs(可变关键字参数)。注意,关键字参数需要有默认值,*args必须放在**kwargs之前,且不能交叉传递。
如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python先匹配位置实参和关键字实参,再将余下的实参都收集到最后一个形参中。
def mixed_function(*args, **kwargs):
print("位置参数:")
for arg in args:
print(arg)
print("\n关键字参数:")
for key, value in kwargs.items():
print(f"{key}: {value}")
# 调用函数
mixed_function(1, 2, 3, name="Bob", age=30)
形参能否影响实参
在 Python 中,函数的参数传递是通过“对象引用”进行的。这意味着当你传递一个参数给函数时,实际上传递的是对象的引用(即内存地址),而不是对象本身的副本。因此,函数内部对该引用所指向对象的操作可能会影响原始对象,也可能不会,这取决于对象的类型(可变或不可变)以及你进行的操作。
1. 不可变对象:包括整数、浮点数、字符串、元组等。当传递不可变对象时,函数内部对形参的重新赋值不会影响实参,因为不可变对象不能被修改。如果尝试修改,实际上是创建了一个新的对象。
#传递不可变对象(不会影响原值)def unchange_obj(num,str1): num = 10 # 重新赋值,创建了一个新的整数对象str1='new-string' print("Inside function:", num,str1)x = 5orig_str='abc'unchange_obj(x,orig_str)print("Outside function:", x,orig_str)# 输出: 5,abc
2. 可变对象:包括列表、字典、集合等。当传递可变对象时,函数内部对形参的修改(修改了传入的可变对象的内部状态,如修改列表元素、添加/删除元素)会直接影响实参,因为形参和实参引用的是同一个对象。但如果你在函数内部将形参重新绑定到一个新的对象(重新赋值),那么原始对象也不会被改变。因为此时形参指向了新的对象,而实参仍然指向原来的对象。
# 传递可变对象(会影响原值)
def change_list(my_list):
my_list.append(4) # 修改列表,添加一个元素
print("Inside function:", my_list)
lst = [1, 2, 3]
change_list(lst)
print("Outside function:", lst)
# 输出: [1, 2, 3, 4]
#传递可变对象但重新赋值(不影响原值)
def reassign_list(my_list):
my_list = [4, 5, 6] # 重新赋值,指向新的列表
print("Inside function:", my_list)
lst = [1, 2, 3]
reassign_list(lst)
print("Outside function:", lst)
# 输出: [1, 2, 3]
函数传递列表很有用,这种列表包含的可能是名字、数字或更复杂的对象(如字典)。将列表传递给函数后,函数就能直接访问其内容,可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。
若要禁止函数修改列表,可向函数传递列表的副本而不是原件;这样函数所做的任何修改都只影响副本,而丝毫不影响原件。切片表示法[:]创建列表的副本。
传递任意数量的实参。
默认参数的默认值
函数的默认参数在定义时只被求值一次,因此如果默认参数是可变对象,多次调用函数可能会共享同一个可变对象,导致意外的行为。因此,通常建议默认参数使用不可变对象,如果必须使用可变对象,可以在函数内部用None作为默认值,然后创建新的对象,相当于每次调用都会创建一个新的对象。
# 不好的做法def bad_append_to(element, target=[]): target.append(element) return target# 好的做法def good_append_to(element, target=None): if target is None: target = [] target.append(element) return targetbad_target1=bad_append_to(1)print('bad_target1:',bad_target1) # [1]bad_target2=bad_append_to(2)print('bad_target2:',bad_target2) #[1,2]good_target1=good_append_to(1)print('good_target1:',good_target1) #[1]good_target2=good_append_to(2)print('good_target2:',good_target2) # [2]
返回值
函数并非总是直接显示输出,或者只在函数体内完成一些操作,相反,它可以处理一些数据,并返回一个或一组值。函数返回的值被称为返回值,调用函数时用变量承接。在函数中,可使用return语句将值返回到调用函数的代码行。返回值让你能够将程序的大部分繁重工作移到函数中去完成,从而简化主程序。
将函数存储在模块中
函数的优点之一是,使用它们可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。还可以更进一步,将函数存储在被称为模块的独立文件中,再将模块导入到主程序中。import语句允许在当前运行的程序文件中使用模块中的代码。
这点在后面的进阶版进一步讨论。