1. 函数简介
函数是执行特定任务的一段代码,程序通过将一段代码定义成函数,并为该函数制定一个函数名,这样就可以在需要的时候多次调用这段代码。将实现不同功能代码中使用相同的代码逻辑的部分封装成一个函数,可以减少代码冗余。
一个函数主要分为三部分:参数、逻辑处理和返回值
在定义一个函数的时候,上述三部分的定义要想清楚:
-
函数需要几个关键的需要动态变化的数据,这些数据就是函数的参数;
-
函数需要传出几个重要的数据,这些数据就是返回值。python中的返回值可以定义多个;
-
函数的内部实现过程,也就是需要对传入参数进行的逻辑处理。
2. 函数的定义和调用
定义函数的语法格式如下:
def 函数名(形参):
# 逻辑处理
[return 返回值]
其中
def
为关键字;函数名要是一个合法的标识符,尽量不要与内置函数重名,如果重名,内置函数将被自定义的函数取代;形参的目的是用来接收调用函数时传递的参数;
# example
def say_hello(name):
return "Hello {}!".format(name)
print(say_hello("Meng"))
#=====output======
Hello Meng!
# 也可以将函数的返回值赋值给一个变量
def get_larger(x, y):
return x if x > y else y
larger = get_larger(10, 30)
print(larger)
#======output======
30
从上面两个例子可以看出,函数的返回值既可以赋值给一个变量,也可以将函数的返回值作为函数的参数传给另一个参数。
2.1 参数传递问题
根据实的传递方式进行分类可以将参数分为位置参数和关键字(命名)参数。
在形参位置可以指定参数的默认值,除此之外,还可以使用*args
来接收可变长的位置参数,**kwargs
来接收可变的关键字参数,如果想要让某个参数必须以关键字方式传参,还可以强制其以关键字方式传参。下面将对各种方式进行解释:
**1. 位置参数:**在调用函数时可以按照参数位置,依次进行传递的参数。
def print_info(name, age):
print('name:', name, 'age:', age)
print("Meng", 18)# 正常调用,不会有报错
print_info("Meng") # 报错如下:TypeError: print_info() missing 1 required positional argument: 'age',也就是说缺少位置参数
2. 关键字(命名)参数:在调用函数时,如果不想按照参数位置顺序进行参数传递,可以按形参名称进行参数的传递,例如上面get_larger函数,在调用时可以按get_larger(y=30, x=10)
方式来进行调用。
# 要想在第一个位置传入age参数,第二个位置传入name参数,那么我们在传参的时候要对变量进行命名
def print_info(name, age=16):
print('name:', name, 'age:', age)
print_info(age=20, name="Meng") # 这样输出的结果也是没有错误的
#======output=======
name: Meng age: 20
**3. 默认参数:**又叫缺省参数,在定义函数的时候,给某个参数赋值一个默认值,在调用函数的时候,如果不传入该参数,则在函数内部默认使用参数的默认值。默认参数定义必须放在位置参数之后。
#为了让上述示例不报错我们可以给age参数设定一个默认值,如下
def print_info(name, age=16):
print('name:', name, 'age:', age)
print_info("Meng") # 这样就不会报错,其中的age输出为16
4. 可变参数:可变参数就是指在调用函数的时候,如果传入的参数可能多于定义的形参个数,没有对其进行命名且个数不确定,那么在定义函数的时候可以使用*args
来接收这些参数,以元组的形式存储,我们称其为可变位置参数;如果在调用函数的时候,对传入的多余的参数进行了命名,那么在定义函数的时候可以使用**kwargs
来接收这些参数,以字典的形式进行存储,我们称其为可变关键字参数。
def print_info(name, age=16, *args):
print('name:', name, 'age:', age)
print(type(args), args)
print_info("Meng", 20, "arg1", "arg2")
#=======output=======
name: Meng age: 20
<class 'tuple'> ('arg1', 'arg2')
如果把默认参数放在可变位置参数之后,只有在调用的时候进行命名传参,默认参数才不会使用默认值,也就是说在*args
后面的参数在调用该函数时必须指定对应参数名。
def print_info(name, *args, age=16):
print('name:', name, 'age:', age)
print(type(args), args)
print_info("Meng", "arg1", "arg2", 20) # 这样按进行参数传递,最后的20也将被*args接收
#========output========
name: Meng age: 16
<class 'tuple'> ('arg1', 'arg2', 20)
#======================
print_info("Meng", "arg1", "arg2", age=20) # 这样才能将20传递给age
#========output=========
name: Meng age: 20
<class 'tuple'> ('arg1', 'arg2')
同样的如果把位置参数放在后面,在调用的时候也需要指定参数名,这也算是强制命名参数的一种方式。
# 可变关键字参数的
def print_info(name, age, *args, **kwargs):
print('name:', name, 'age:', age)
print(type(args), args)
print(type(kwargs), kwargs)
print_info("Meng", 20, "arg1", "arg2", city="Beijing", job="Student")
#=======output======
name: Meng age: 20
<class 'tuple'> ('arg1', 'arg2')
<class 'dict'> {'city': 'Beijing', 'job': 'Student'}
从上面的示例中我们可以看出,**kwargs
可以接收多余的有命名的参数。当然它的位置只能放在最后。如果其面定义了别的参数,将会报SyntaxError: invalid syntax
的错误。
**5. 强制命名参数:**就是在调用函数时传参必须要传入参数名,例如定义如下函数def print_info(name, age, *, gender, job)...
,在*
号后面的参数调用时必须要传入对应的参数名,否则会报错,调用示例:print_info('Meng', 18, gender="male", job="student")
。
def print_info(name, *, age):
print('name:', name, 'age:', age)
print_info("Meng", 20) # 这样将会报错TypeError: print_info() takes 1 positional argument but 2 were given,也就是说本来需要一个位置参数,传入了两个,那么要想给age传入参数我们传参的时候必须命名。
print_info("Meng", age=20)# 这样是正确的
注意:
在定义函数的时候要注意参数定义的顺序问题:必选参数、默认参数、可变位置参数、强制命名参数、可变关键字参数。
在使用
**kwargs
来接收参数的时候,通过关键字传参数一定要放在位置参数之后,**kwargs
接收的参数是根据关键字找到对应参数位置之后剩余的通过关键字传入的参数,顺序无所谓。
#example
def print_info(name, *, age, **kwargs):
print('name:', name, 'age:', age)
print(kwargs)
print_info("Meng", gender="male", city="Beijing", age=20)
#=======output=======
name: Meng age: 20
{'gender': 'male', 'city': 'Beijing'}
# 这样即使age放到最后,也要把age传给对应的age参数,剩余的传入的关键字参数被**kwargs接收
6. 逆向参数收集
逆向参数收集指的是在程序已有列表、元组、字典等对象的前提下,把它们的元素拆开后传给函数的参数,也就是Python中的解包(unpacking)。
解包就是将容器中所有元组逐个取出来。python中的任何可迭代对象都可以进行解包。
在调用函数的时候,可以通过在列表或元组之前添加*
,在字典前添加两个*
来传入参数。
# example
def print_info(name,age):
print('name:', name, 'age:', age)
info_list = ["Meng", 18]
print_info(*info_list) # 这样就可以将"Meng"传递给name,18传递给age,按位置进行参数传递
info_dict = {"age": 18, "name": "Meng"}
print_info(**info_dict) # 这样就可以按关键字进行参数传递,按照字典中的key找到对应的参数位置。
2.2 函数的参数传递机制
关于参数的传递机制,许多博客中都引用了下面这段话:
Python不允许程序员选择采用传值还是传引用。Python参数传递采用的肯定是“传对象引用”的方式。实际上,这种方式相当于传值和传引用的一种综合。如果函数收到的是一个可变对象(比如字典 或者列表)的引用,就能修改对象的原始值——相当于通过“传引用”来传递对象。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象——相当于过“传值”来传递对象。
当然这段话如果能够理解好是没有太大问题的,但是如果理解不好出现一些问题,比如:
def try_elem_add(l):
l = l + [5]
print("函数中:", l)
list1 = [1, 2, 3, 4]
print("原始:", list1)
try_elem_add(list1)
print("调用函数后:", list1)
#=======output========
原始: [1, 2, 3, 4]
函数中: [1, 2, 3, 4, 5]
调用函数后: [1, 2, 3, 4]
这样就会产生一些疑问,也就是上面那段话中说的如果函数收到的是一个可变对象的引用,就能修改对象的原始值
,但是现在对象的原始值并没有发生改变,那么是这段话说错了吗,当然不是,下面是官网上的一段话:
The actual parameters (arguments) to a function call are introduced in the local symbol table of the called function when it is called; thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object). Actually, call by object reference would be a better description, since if a mutable object is passed, the caller will see any changes the callee makes to it (items inserted into a list).
这段话与上面的意思大致相同。上述问题的出现主要是出在l = l + [5]
这句代码上,这是进行了一个赋值操作,也就是相当于创建了一个新的可变对象,而创建可变对象是会分配一个新地址的,那么l
这个变量就重新指向了一个新的地址,而原来的list1
的指向的地址没有发生改变,所以就出现了上述情况。下面可以通过id()
函数来查看一下:
def try_elem_add(l):
print("函数中(1):\t", id(l))
l = l + [5]
print("函数中(2):\t", id(l))
list1 = [1, 2, 3, 4]
print("原始:\t\t", id(list1))
try_elem_add(list1)
print("调用函数后:\t", id(list1))
#=======output=========
原始: 3104475078728
函数中(1): 3104475078728
函数中(2): 3104475080136
调用函数后: 3104475078728
通过上面的示例我们可以清楚的看到其在经过赋值后l
的地址发生了变化。如果使用append()、pop()
等函数对变量l
进行修改,原来的对象list1
也将发生变化。
def try_elem_add(l):
l.append(5)
print("函数中:", l)
list1 = [1, 2, 3, 4]
print("原始:", list1)
try_elem_add(list1)
print("调用函数后:", list1)
#=======output========
原始: [1, 2, 3, 4]
函数中: [1, 2, 3, 4, 5]
调用函数后: [1, 2, 3, 4, 5]
总之:Python中函数参数的传递时传递的是对象的引用。Arguments are passed using *call by value* (where the *value* is always an object *reference*, not the value of the object).
2.3 返回值问题
如果函数有多个返回值,我们既可以将多个值封装成列表、元组或字典的形式返回,也可以直接返回多个值,直接返回多个值的时候,python会自动将其封装成元组。
def get_min_max(l):
min_num = min(l)
max_num = max(l)
#return [min_num, max_num] # 封装成列表返回
#return {"min_num": min_num, "max_num": max_num} # 封装成字典返回
return min_num, max_num # python会将其封装成元组然后返回
temp_list = [1, 20, 3, -10, 100, 9]
result = get_min_max(temp_list)
print(type(result), result)
#=======output==========
<class 'tuple'> (-10, 100)
当函数有多个返回值的时候,我们可以用多个变量来接收每个返回值,python将会自动执行解包操作。
def get_min_max(l):
min_num = min(l)
max_num = max(l)
return min_num, max_num
temp_list = [1, 20, 3, -10, 100, 9]
min_num, max_num = get_min_max(temp_list) # 在这一步直接利用了Python中提供的序列解包功能
print(min_num, max_num)
#=======output========
-10 100
3. 变量的作用域
变量的作用域指的就是变量的作用范围,说白了就是在哪块才能找到这个变量。
根据定义变量的位置,变量可以分为两种:
- 局部变量:在函数中定义的变量,包括参数都被称为局部变量,在函数内部可以正常使用,在函数外部不能正常访问。
- 全局变量:在函数外部、全局范围内定义的变量被称为全局变量,在函数外部可以正常使用,在函数内部也可以使用。
python中获取指定范围的“变量字典”:
globals()
:获取全局范围内所有变量组成的字典locals()
:获取当前局部范围内所有变量组成的字典vars(object)
:获取指定对象范围内所有变量组成的字典,如果不传入object
参数,vars()
和locals()
的作用完全相同。
既可以通过这三个函数对变量进行访问,也可以对其进行修改。
def change_global():
print(globals()['name']) # 访问
globals()['name'] = "HaHaHa" # 修改
name = "Meng"
change_global()
print(name)
#========output=======
Meng
HaHaHa
global关键字和nonlocal关键字
global
关键字修饰变量后标识该变量是全局变量,对该变量进行修改就是修改全局变量。global
关键字可以用在任何地方,包括最上层函数中和嵌套函数中,即使之前未定义该变量,global
修饰后也可以直接使用。
def define_global():
global name
name = "Hahaha"
define_global()
print(name)
#=======output======
Hahaha
nonlocal
关键字修饰变量后标识该变量是上一级函数中的局部变量,如果上一级函数中不存在该局部变量,会发生错误。nonlocal
关键字只能用于嵌套函数中。
def test_nonlocal():
name = "Meng"
def inner_test():
nonlocal name
print(name)
name = "Hahaha"
inner_test()
print(name)
test_nonlocal()
#======output====
Meng
Hahaha
4. lambda表达式(匿名函数)
lambda
表达式是一个匿名函数,即它没用函数名的函数。格式如下:
lambda 参数: 返回值
其中参数可以为多个,返回值只能有一句代码。
lambda
函数主要和map
函数,reduce
函数,filter
函数,sorted
函数连用。
当然,如果非得给匿名函数起个名字,也是可以的,例如:
cal_two_sum = lambda x, y: x + y
result = cal_two_sum(10, 20)
print(result)
#========output=======
30
lambda
表达式的优点:
- 对于单行函数,使用
lambda
表达式可以省去定义函数的过程,让代码更加简洁。 - 对于不需要多次重复的函数,使用
lambda
表达式可以在用完之后立即释放,提高了性能。