函数
1. 初识函数
1.1 何为函数,何时用函数
什么是函数?
函数,可以当做是一大堆功能代码的集合。
什么时候用函数
一般在项目开发中有会有两种应用场景:
- 有重复代码,用函数增加代码的重用性。
- 代码太长,用函数增强代码的可读性。
1.2 函数的语法结构
def my_len(a, b):
'''函数注释'''
# 函数体
return '123'
- def 就是定义函数的关键字,它不能够省略,必须写
- my_len():函数名,函数名的命名遵循变量的命名规范,他也是必填的,不能省略的,括号不能省略
- a, b称为是函数的参数,是可选的,可有可无
参数就是执行函数的一些前提条件- ‘’‘函数注释’‘’
注释不参与函数的执行,只起到函数功能的提示作用,以及各个参数是什么意思
# 可有可无的,但是,推荐每个函数都加上函数注释- 函数体
才是函数的核心,它是函数所具备的核心功能,应该是必须有的,pass
没有函数体的函数是没有意义的函数,一般情况都是要有函数体的- 返回值 return 函数的返回值
# 返回值也是可选的,可以有返回值,也可以没有返回值,但是一般情况下都要有返回值
# 一个函数只能有一个返回值…
1.3 函数的定义与调用
- 函数必须先定义后调用,没有定义函数是一定不能够调用的
- 函数在定义阶段,只检测语法是否错误,不检测逻辑是否有问题
- 逻辑上的错误只会在调用阶段检测
- 函数一定是被调用之后才会执行函数内部的代码块,不调用函数一定不会执行函数的
如何调用函数?
函数名() # 只要函数名加括号一定会执行函数体代码
函数如果在定义阶段有参数,那么,在调用的时候,连参数一块写上
函数调用的内部原理:
- 先在内存空间中申请一块空间地址来存储函数体代码
- 把函数名和函数体所在的空间地址绑定在一起
- 以后只需要通过函数名()来访问函数体代码即可
1.4 函数的分类
1. 内置函数
# Python解释器中原本就存在的,可以直接拿来使用,无需定义,直接调用
len print input 之前学习的数据类型的各个方法
2. 自定义函数
# 程序员自己写的函数,就必须先定义后调用
1. 无参函数:函数在定义阶段没有参数
def my_len():
pass
my_len()
2. 有参函数:在函数定义阶段有参数的存在
def my_len(a, b):
pass
my_len(1, 2)
3. 空函数
def my_len():
pass
"""作用就在于可以帮助我们梳理业务逻辑"""
def register():
pass
def login():
pass
def address():
pass
def order():
pass
2. 参数
2.1 形参和实参
在定义函数时,如果在括号中添加变量
,我们称它为函数的形式参数:
# ###### 定义有三个参数的函数(a1/a2/a3一般称为形式参数-形参) #####
def func(a1,a2,a3):
print(a1+a2+a3)
# 执行函数并传入参数(执行函数传值时一般称为实际参数-实参)
func(11,22,33)
# 执行函数并传入参数
func(9,2,103)
2.2 位置传参
位置传参是一种函数参数传递的方式,它通过顺序给函数传递参数,将实参与形参进行一一对应。
def func(a, b, c):
print(a, b, c)
func(1,2,3) # 实参1,2,3会按照顺序传给形参a,b,c
2.3 关键字传参
关键字传参是指在函数调用时,使用指定的参数名来传递参数值,而不是按照参数在函数定义时的顺序来传递参数。
关键字传参的基本语法如下:
函数名(参数1=值1,参数2=值2,...,参数n=值n)
例如:
def func(x, y, z):
print("x=", x, "y=", y, "z=", z)
func(x=1, y=2, z=3)
上述代码中,定义了一个名为func的函数,函数有三个参数,分别为x、y、z。在调用函数时,使用了关键字传参方式,按照参数名指定了参数值,因此输出结果为x=1 y=2 z=3。
关键字传参的优点是可以使代码更加易读、易懂,尤其是当函数参数比较多时,可以清晰地表明每个参数的含义,避免了因参数顺序错误而产生的难以查找的错误。同时也可以使函数的调用更加灵活,可以只传递需要的参数,而不用按照参数顺序传递所有参数。
2.4 默认参数
默认参数是指在函数定义时,为一个或多个参数指定一个默认值,当函数调用时不传递这些参数时,默认值会被使用。
默认参数的基本语法如下:
def func(arg1, arg2=default_value):
pass
其中,arg1是必须传递的参数,而arg2有一个默认值default_value,如果在函数调用时不传递arg2参数,则默认使用default_value作为参数值。
例如,下面的代码定义了一个函数greeting,其中name参数有一个默认值"Guest":
def greeting(name="Guest"):
print("Hello,", name)
greeting()
greeting("Alice")
上述代码中,第一次调用greeting函数时,没有传递任何参数,因此使用了默认值"Guest",输出结果为Hello, Guest;第二次调用greeting函数时,传递了一个参数"Alice",因此输出结果为Hello, Alice。
使用默认参数的优点是可以减少函数调用时必须传递的参数数量,增加函数的灵活性,同时也可以使函数的调用更加简洁明了。不过需要注意的是,当默认参数被多次调用时,如果不小心修改了参数值,可能会影响到以后的函数调用,因此需要谨慎使用。
2.5 动态参数
动态参数是指在函数定义时,能够接收不定长度的参数列表,分为两种类型:可变参数和关键字参数。
2.5.1 可变参数
可变参数使用*变量(变量一般用args)
来接收一个不定长度的参数列表,它表示接收一个元组类型的参数,在函数内部可以通过遍历操作访问元组中的每个元素。示例代码如下:
def add(*args):
result = 0
for num in args:
result += num
return result
print(add(1, 2, 3, 4)) # 输出:10
print(add(1, 2)) # 输出:3
2.5.2 关键字参数
关键字参数使用**变量(常用kwargs)
来接收一个不定长度的关键字参数列表,它表示接收一个字典类型的参数,在函数内部可以通过访问字典中的键值对来获取参数值。示例代码如下:
def print_info(**kwargs):
for key, value in kwargs.items():
print(key, ":", value)
print_info(name="Alice", age=20, gender="female")
在这个示例代码中,函数print_info接收一个不定长度的关键字参数列表**kwargs,并遍历字典中的所有键值对进行输出。
同时,还可以在函数的参数列表中混合使用可变参数和关键字参数,例如:
def function(arg1, arg2, *args, **kwargs):
pass
这个函数定义中,arg1和arg2是必须传递的参数,*args接收一个不定长度的位置参数列表,**kwargs接收一个不定长度的关键字参数列表。当函数调用时,位置参数需要在关键字参数之前传递。
2.6 命名关键字参数
命名关键字参数是Python3中添加的一种函数参数形式,它可以用来限制关键字参数的名称,强制关键字参数的使用者必须通过指定参数名字对参数进行传递,在传递参数时其它名称将会报错。命名关键字参数通过在函数参数列表中使用*后缀来表示,其语法形式如下:
def function(arg1, arg2, *, kwarg1, kwarg2):
pass
使用命名关键字参数有以下几个优点:
- 命名关键字参数可以提高函数的可读性和可维护性。因为在函数定义时,强制要求传递的参数必须使用关键字来指定,不能使用无意义的字符串或者数字,使得函数更加易于理解和维护。
- 命名关键字参数可以提高函数的调用安全。因为当参数非常多时,使用位置参数传递容易出现传递顺序错误,而使用命名关键字参数则可以明确指定传递参数的名称,从而避免这种问题。
示例如下:
def print_person_info(name, age, *, gender):
print(f"Name: {name}")
print(f"Age: {age}")
print(f"Gender: {gender}")
print_person_info("Alice", 20, gender="female") # 正确的调用方式
print_person_info("Bob", 25, "male") # 报错,必须使用关键字参数
2.7 混用注意事项
在Python中,函数的参数支持多种形式的传参方式,如位置参数、默认参数、动态参数和命名关键字参数。在使用这些参数的时候需要注意以下几点:
- 参数顺序
位置参数必须在默认参数、动态参数和命名关键字参数之前,否则会导致语法错误。- 默认参数
默认参数必须放在位置参数后面,但在调用函数时可以省略默认参数。- 动态参数
动态参数必须放在位置参数和命名关键字参数之后,否则会导致语法错误。- 命名关键字参数
命名关键字参数必须在动态参数之后,否则会导致语法错误。- 混合使用
在使用多种参数形式时,应该避免出现歧义和混淆。在函数定义时应该结合函数的实际需求,合理地选用不同的参数形式,从而提高代码的可读性和可维护性。
示例代码:
def func(a, b=10, *args, c, d):
pass
# 正确的调用方式
func(1, c=3, d=4)
func(1, 2, 3, 4, c=5, d=6)
# 错误的调用方式
func(1, 2, 3, 4, 5, d=6) # 参数太多,缺少c参数
func(1, 2, 3, c=4, d=5) # 缺少必要的位置参数b
func(1, b=2, 3, 4, c=5, d=6) # 参数位置错误
# 1. ** 必须放在 * 的后面
def func1(*args, **kwargs):
print(args, **kwargs)
# 2. 参数和动态参数混合时,动态参数只能放在最后。
def func2(a1, a2, a3, *args, **kwargs):
print(a1, a2, a3, args, **kwargs)
# 3. 默认值参数和动态参数同时存在
def func3(a1, a2, a3, a4=10, *args, a5=20, **kwargs):
print(a1, a2, a3, a4, a5, args, kwargs)
func3(11, 22, 33, 44, 55, 66, 77, a5=10, a10=123)
3. 名称空间
所谓名称空间是程序中用来组织和管理标识符的一种机制,它将标识符分成了不同的组,确保同名标识符之间不会冲突。一个名称空间可以被看作是一个存储标识符的容器,每个容器中的标识符都有其特定的作用域。在一个名称空间中,每个标识符都有一个唯一的名称,该名称可以在程序中被引用和操作。在程序执行期间最多存在三种名称空间,即内建名称空间,全局名称空间,局部名称空间。
3.1 内建名称空间
内建名称空间是Python解释器自带的一些名称空间,其中包含了Python语言中的内建函数和常量。内建名称空间属于最外层的名称空间,也被称为全局名称空间。在Python程序中,可以使用内建函数dir()来查看当前作用域下的所有名称,包括内建名称空间中的名称。例如,在Python交互式命令行中输入dir(__builtins__),即可查看内建名称空间中的所有名称。
示例如下:
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
3.2 全局空间名称
全局空间(Global Namespace)是指在Python程序中没有被定义在任何函数或类的作用域中的名称空间。在Python程序执行时,全局空间会在程序启动时创建,并在整个程序执行期间一直存在。在全局空间中定义的变量,函数,类等名称都可以在程序的任何地方被调用和使用。
在Python程序中,使用globals()函数可以查看当前全局空间中的所有名称。例如,在Python交互式命令行中输入print(globals()),即可查看当前全局空间中的所有名称极其对应的值。
>>> print(globals())
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
3.3 局部名称空间
局部名称空间(Local Namespace)是指在Python程序中定义在函数或类作用域中的名称空间。每个函数或类都有自己的局部名称空间,该名称空间只有在函数或类被调用时才被创建,当函数或类执行完毕后,局部名称空间也会被销毁。
在Python程序中,可以使用locals()函数查看当前局部名称空间中的所有名称,例如,在一个函数内部输入print(locals()),即可查看该函数的局部名称空间中的所有名称极其对应的值。
需要注意的是,局部名称空间旨在函数或类作用域中有效,外部无法直接访问局部名称空间中的名称。如果需要在函数或类外部访问局部名称空间中的某些变量或函数,可以通过函数或类的返回值来实现。
4. 作用域
作用域是指一个标识符所存在的区域,该区域规定了标识符的可见范围和生命周期。在一个作用域内,同名的标识符被认为是相同的,因此它们之间会发生冲突。一般情况下,作用域可以分为全局作用域和局部作用域两种类型。在不同的作用域内,相同名称的标识符可以拥有不同的值和类型
4.1 全局作用域
位于全局名称空间、内建名称空间中的名字属于全局范围,全局作用域的生命周期与程序运行的生命周期相同,在程序运行期间都存在。如果程序运行结束,全局作用域也会随之销毁。
需要注意的是,全局作用域中的变量可以被任何函数或方法引用,因此需要谨慎使用全局变量,避免出现意料之外的结果或Bug。同时,为了使代码更加清晰易懂,应该尽量避免在全局作用域中定义过多的变量和函数。
4.2 局部作用域
位于局部名称空间中的名字属于局部范围,局部作用域只在函数或方法内部有效。其生命周期同函数或方法的生命周期相同,即函数或方法开始被定义,函数或方法执行完毕后,局部作用域也会随之被销毁。
局部作用域的存在可以使函数或方法之间的变量相互独立,避免了变量之间的冲突和混淆,同时也有利于代码的重复利用和模块化。
4.3 作用域优先级
Python中的作用域有:局部作用域、嵌套作用域和全局作用域。作用域的优先级顺序为:局部作用域 > 嵌套作用域 > 全局作用域。如果在一个作用域中找不到定义的变量或函数,Python解释器会按照这个优先级的顺序继续在上层作用域中查找,直到找到该变量或函数或查找到全局作用域仍然找不到为止。
具体来说,Python解释器在访问一个变量时,会按照以下的顺序进行查找:
当前作用域(包括局部作用域、嵌套作用域和全局作用域)中是否有该变量;
如果当前作用域中没有该变量,则依次向上一层嵌套作用域中查找,直到查找到该变量或查找到全局作用域为止;
如果全局作用域中也没有该变量,则会抛出NameError异常。
如果在函数或方法内部定义了一个与全局作用域中同名的变量,那么在函数或方法内部访问这个变量时,会优先访问局部变量而非全局变量。
4.4 global 和 nonlocal
在Python中,我们可以使用global关键字来在函数内部访问并修改全局变量。例如:
x = 10
def change_x():
global x
x = 20
change_x()
print(x) # 输出 20
global 关键字用于在函数内部访问全局变量,以及在函数内部对全局变量进行修改。
默认情况下,在局部作用域对全局变量只能进行:读取和修改内部元素(可变类型),无法对全局变量进行重新赋值。
而 nonlocal 关键字则用于在函数内部访问嵌套作用域中的变量。具体来说,如果在函数内定义了一个函数,且内部函数访问了外部函数的变量,那么这个变量就属于嵌套作用域中的变量。例如:
def outer():
x = 10
def inner():
nonlocal x
x = 20
inner()
print(x)
outer() # 输出 20
Ps:global 和 nonlocal 关键字的使用都会影响到函数中变量的作用域。因此,在使用这两个关键字时,要谨慎选择变量的命名,以免产生意外的错误。
5. 函数对象
在Python中,函数也是一种对象,我可以将其赋值给变量,也可以将其作为参数传递给其他函数,这便是函数对象。
可以简单理解为函数名也是一个变量,只不过此变量指代的是一个函数,指向的是函数所在的内存空间。
在解释型语言中,函数需要先定义再使用。
5.1 引用函数
将函数名赋值给其他变量,函数名其实就个变量,代指某函数;如果将函数名赋值给另外一个变量,则此变量也会代指该函数,例如:
def func(a1,a2):
print(a1,a2)
xxxxx = func
# 此时,xxxxx和func都代指上面的那个函数,所以都可以被执行。
func(1,1)
xxxxx(2,2)
5.2 函数做元素
然函数就相当于是一个变量,那么在列表等元素中可以把函数当做元素。
例如:
def func():
return 123
data_list = ["antony", "func", func , func() ]
print( data_list[0] ) # 字符串"antony"
print( data_list[1] ) # 字符串 "func"
print( data_list[2] ) # 函数 func
print( data_list[3] ) # 整数 123
res = data_list[2]()
print( res ) # 执行函数 func,并获取返回值;print再输出返回值。
print( data_list[2]() ) # 123
PS:函数同时也可被哈希,所以函数名通知也可以当做 集合的元素、字典的键。
def send_message():
"""发送消息"""
pass
def send_image():
"""发送图片"""
pass
def send_emoji():
"""发送表情"""
pass
def send_file():
"""发送文件"""
pass
def xxx():
"""收藏"""
pass
function_dict = {
"1": send_message,
"2": send_image,
"3": send_emoji,
"4": send_file,
"5": xxx
}
print("欢迎使用xx系统")
print("请选择:1.发送消息;2.发送图片;3.发送表情;4.发送文件")
choice = input("输入选择的序号") # "1"
func = function_dict.get(choice)
if not func:
print("输入错误")
else:
# 执行函数
func()
5.3 函数做参数名和返回值
函数名其实就一个变量,代指某个函数,所以,他和其他的数据类型一样,也可以当做函数的参数和返回值。
参数示例:
def plus(num):
return num + 100
def handler(func):
res = func(10) # 110
msg = "执行func,并获取到的结果为:{}".format(res)
print(msg) # 执行func,并获取到的结果为:110
# 执行handler函数,将plus作为参数传递给handler的形式参数func
handler(plus)
返回值示例:
def plus(num):
return num + 100
def handler():
print("执行handler函数")
return plus
result = handler()
data = result(20) # 120
print(data)
6. 闭包
闭包函数(Closure)是一种在编程语言中常见的概念,它可以让一个函数访问其词法作用域外部的变量,即使在该函数在定义时所处的作用域已经不再存在。
通俗地说,闭包函数允许一个函数捕获和保持了其所在作用域的一些变量,使得这些变量可以在该函数被调用的时候依然存在。这使得闭包函数可以在其创建的环境中执行,而不受外部环境的影响。
“闭”代表函数是内部的,“包”代表函数外’包裹’着对外层作用域的引用。因而无论在何处调用闭包函数,使用的仍然是包裹在其外层的变量。
闭包,简而言之就是将数据封装在一个包(区域)中,使用时再去里面取。(本质上 闭包是基于函数嵌套搞出来一个中特殊嵌套)。
闭包的特殊性质,使得闭包函数成为了一个强大的工具,可以在许多场景下被用来实现一些特殊的逻辑,尤其是在需要保持某些状态或配置的情况下。例如:
应用场景1:封装数据防止污染全局。
name = "antony"
def f1():
print(name, age)
def f2():
print(name, age)
def f3():
print(name, age)
def func(age):
name = "antony"
def f1():
print(name, age)
def f2():
print(name, age)
def f3():
print(name, age)
f1()
f2()
f3()
func(123)
应用场景2:状态保持
闭包可以保持特定状态,使得函数在多次调用之间能够记住之前的状态。
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter1 = counter()
counter2 = counter()
print(counter1()) # 输出 1
print(counter1()) # 输出 2
print(counter2()) # 输出 1
应用场景3:函数工厂
通过闭包实现一个通用的函数工厂,该工厂可以用于生成不同的函数,从而避免了重复编写相似的代码。
示例:
def make_adder(n):
def adder(x):
return x + n
return adder
add_2 = make_adder(2)
add_5 = make_adder(5)
print(add_2(3)) # 输出 5
print(add_5(3)) # 输出 8