第十一章、函数
函数在参数传递和返回值时,永远传递和返回的都是变量中保存的值,而非变量的内存地址
变量由四部分组成
- 变量类型
- 变量名
- 变量值
- 变量自身内存地址
一、概述(函数名,参数,返回值)
- 函数对应的英语单词:function
- 方法对应的英语单词:Method
函数或方法是由一连串的子程序(语句的集合)所组成的,可以被外部程序调用(invoke调用方法),向函数或方法传递参数之后,函数或方法可以返回一定的值。可以重复调用,实现代码复用
通常情况下,Python代码是自上而下执行的,不过函数体或方法体内部的代码则不是这样。如果只是对函数或方法进行了声明,其中的代码并不会执行,只有在调用函数或方法时才会执行函数体或方法体内部的代码。
在 Python 中,函数或方法不是数据类型,而是行为或操作。函数或方法是一个包含代码块的结构,用于执行某些任务或操作。函数或方法可以有一个或多个参数,它们可以是引用数据类型或基本数据类型。
1、函数对象初始化
在 Python 中,函数是一种可重用的代码块,用于执行特定的任务或操作。函数是模块化编程的重要组成部分,可以将一个复杂的程序拆分为更小的可管理的部分,使代码更加清晰、简洁和易于维护。
在Python中,函数初始化通常指的是函数的定义和声明。当你定义一个函数时,你在代码中创建了一个函数对象,使其在程序中可用。函数对象由Python解释器在运行时创建,它包含函数的代码和相关的元数据。
函数的定义通常包括以下几个部分:
def
关键字:函数定义以def
关键字开始。- 函数名:在
def
关键字后面,是函数的名称,用来标识该函数。函数名遵循标识符命名规则,并且不能与Python的保留关键字冲突。 - 参数列表:在函数名后面,用一对圆括号括起来,指定函数的参数。参数列表是可选的,可以为空,也可以包含一个或多个参数。
- 函数体:在参数列表后面,用一个冒号
:
表示函数体的开始,函数体是由一系列缩进的代码块组成,这些代码块是函数的实际执行逻辑。 - 返回值(可选):函数可以使用
return
语句返回一个值。如果没有return
语句或者return
后面没有表达式,则函数默认返回None
。
以下是一个简单的函数初始化的例子:
def add(x, y):
"""这是一个加法函数,用于计算两个数的和。"""
return x + y
result = add(3, 5)
print(result) # 输出: 8
在这个例子中,我们定义了一个名为 add
的函数,它有两个参数 x
和 y
。函数体内的代码实现了 x + y
的加法操作,并通过 return
语句返回结果。函数初始化的过程是在定义时创建了一个函数对象,并且它被命名为 add
,可以在代码中被调用和使用。
函数在 Python 中有许多用途,可以用于执行各种任务,例如执行数学运算、处理数据、操作文件、创建模块等。函数的使用可以使代码更加模块化、易于重用和维护,提高代码的可读性和可维护性。
总结:Python中的函数初始化指的是函数的定义和声明过程。在函数的定义中,需要指定函数名、参数列表和函数体。函数初始化的结果是创建了一个函数对象,使其在程序中可用。
2、函数是一个Python对象(函数对象存储在堆中)
def add(x, y):
return x + y
# 函数 add 是一个函数对象
print(type(add)) # 输出: <class 'function'>
3、函数对象是不可变的
在 Python 中,函数对象是不可变的。一旦创建了一个函数对象,它的定义和内容就不能被修改。
函数对象的不可变性包括以下几个方面:
-
函数体不可变: 一旦定义了一个函数,其函数体(也就是函数内的代码块)就不可再修改。你不能在运行时动态地更改函数体内的代码。如果你想要改变函数的行为,必须重新定义函数。
-
函数名不可变: 函数对象的名称也是不可变的。一旦你定义了一个函数,它的名称不能被重新赋值或修改。你不能将一个已经定义的函数重命名。
-
函数参数和默认值不可变: 函数对象的参数列表和默认值一旦定义,也不能被修改。你不能在运行时动态地增加或删除函数的参数,也不能修改已经定义的默认参数值。
因为函数对象是不可变的,所以你不能在运行时修改函数的定义。如果你需要在运行时动态地改变函数的行为,可以考虑使用闭包或者将函数作为参数传递给其他函数。
总结:函数对象是不可变的。函数体、函数名、参数列表和默认值一旦被定义,就不能在运行时被修改。函数的不可变性是Python语言的特性之一,它确保函数的定义和行为在运行时是稳定和可靠的。
4、函数对象是可哈希的,和局部变量无关
在Python中,函数对象是可哈希的。这意味着函数对象可以用作集合(Set)的元素,也可以用作字典(Dictionary)的键,因为它们的哈希值是确定的,并且在函数生命周期内不会改变。
函数对象的可哈希性是由其内建的 __hash__()
方法所确定的。默认情况下,函数对象继承了 object
类的 __hash__()
方法,这使得它们是可哈希的。在函数对象中,__hash__()
方法会返回函数的 id(标识符),这个 id 是唯一的且在函数的生命周期内不会改变。
局部变量和函数对象是两个不同的概念,并不会影响函数对象的可哈希性。局部变量是在函数内部定义的变量,它们只在函数的作用域内可见。而函数对象是函数的实际对象,包含函数的代码和相关的元数据,它在整个函数的生命周期内保持不变。
以下是一个示例来说明函数对象的可哈希性:
def greet():
print("Hello, World!")
def farewell():
print("Goodbye!")
# 函数对象是可哈希的
function_set = {greet, farewell}
print(function_set) # 输出: {<function greet at 0x...>, <function farewell at 0x...>
在这个例子中,我们将函数对象 greet
和 farewell
添加到集合 function_set
中,而集合能够正确存储这两个函数对象,说明它们是可哈希的。
总结:函数对象是可哈希的,并且可以用作集合的元素或字典的键。函数对象的可哈希性是由其内建的 __hash__()
方法所确定的。局部变量和函数对象是不同的概念,局部变量不影响函数对象的可哈希性。
二、函数定义
1、原则
函数定义的原则主要包括以下几点:
-
明确函数的功能:函数应该具有明确的功能和用途,每个函数应该完成一个特定的任务。函数的名称应该清晰地描述其功能,以便于代码的阅读和理解。
-
使用有意义的参数名:函数的参数名称应该具有描述性,能够清楚地表达参数的含义。这样可以增加代码的可读性,让其他人更容易理解函数的用法。
-
函数应该简洁明了:函数应该尽量保持简洁明了,避免函数过于复杂和冗长。每个函数应该只包含完成特定任务所需的代码,避免将多个不相关的功能放在同一个函数中。
-
避免全局变量:函数应该尽量避免使用全局变量,而是通过参数传递和返回值来进行数据交换。这样可以降低函数之间的耦合性,使得函数更加独立和可复用。
-
使用适当的注释:函数的定义应该包含适当的注释,用于解释函数的功能、参数的含义、返回值等重要信息。良好的注释可以提高代码的可维护性和可读性。
-
函数应该有适当的命名:函数的命名应该遵循一定的命名规范,通常使用小写字母和下划线来命名函数,以增加代码的可读性。
总的来说,函数定义的原则是尽量使函数简单、清晰、易于理解和复用。函数的设计应该符合代码的整体架构和功能需求,同时避免引入不必要的复杂性和耦合性。良好的函数设计可以提高代码的可维护性和可读性,使得代码更加健壮和可靠。
2、语法结构
def 函数名(参数1:[参数类型],参数2:[参数类型]):[-> 返回值类型] :
#函数体,实现功能逻辑
return result #可选的返回值
3、def关键字
在Python中,def
关键字用于定义函数。当我们需要创建一个函数时,可以使用def
关键字后跟函数名来定义函数,并在函数体中编写函数的具体实现。
为什么是def
呢?这是因为def
是“define”的缩写,表示“定义”的意思。Python设计者选择了这个关键字是为了简洁和直观地表示函数定义的意图。
def
关键字的使用意义在于创建一个函数对象,并将函数名与函数体绑定在一起。通过使用def
关键字,我们可以将一段功能性的代码封装为一个可复用的函数,从而提高代码的可维护性和可读性。函数允许我们将代码块组织成模块化的结构,便于在程序中的多个地方重复使用,避免代码的冗余和复杂性。
例如,以下是一个简单的Python函数定义的例子:
def greet(name):
return f"Hello, {name}!"
在上面的例子中,def greet(name):
定义了一个名为greet
的函数,它接受一个参数name
,并返回一个包含问候信息的字符串。通过使用def
关键字,我们将这段代码片段封装为一个可调用的函数,可以在程序的其他地方多次使用。
4、函数名
- 只要是合法的标识符就行
- 方法名最好见名知意
- 方法名最好是动词
- 方法名使用小写字母,用下划线分隔单词,如
my_variable
5、函数体
必须缩进,函数体当中的代码有顺序,遵循自上而下的顺序依次执行,并且函数体由Python语句构成
三、形式参数(形参,定义时)
在Python中,函数的形参(形式参数)是在定义函数时用于接收传递给函数的值的占位符。当函数被调用时,实参(实际参数)会传递给这些形参,并且函数使用这些值来执行特定的操作。形参允许我们将数据传递给函数并进行处理,使得函数可以在不同的调用中使用不同的数据。
下面是有关Python函数中形参的一些详解:
1、原则
1、形参是局部变量,只在函数局部作用域
形参只是作用在函数局部作用域中
2、形参的个数可以是0~n个
形式参数的个数可以是0~n个
3、多个形参之间用 ,号隔开
多个形式参数之间使用,号隔开
4、不要求显示数据类型
形参中起决定作用的是形参的数据类型,形参的名字就是局部变量的名字
5、方法形参中可以是基本数据类型也可以是引用数据类型,起决定作用的是数据类型
在Python中,函数参数都是引用传递的,这意味着函数中的参数是一个对象的引用,而不是对象本身。当你在函数中定义一个引用数据类型的参数时,并未实例化该对象,但是你仍然可以使用该参数调用该类中的实例方法。
这是因为该参数实际上是一个引用,它指向的是堆内存中已经实例化的对象。因此,当你在函数中使用该参数调用类中的实例方法时,实际上是在使用该对象的引用调用该方法。
需要注意的是,如果你尝试在一个没有实例化的对象上调用一个实例方法,例如在None引用上调用该方法,将会抛出AttributeError 异常。因此,在使用方法参数调用实例方法之前,一定要确保该参数所引用的对象已经被实例化为一个对象。
Python中的方法参数定义为引用数据类型时,传递给方法的是对象的引用,而不是对象本身。这使得在方法内部可以通过引用访问该对象的实例变量和实例方法,并且对该对象的任何修改都会反映在原始对象上。
2、形参分类
形式参数的顺序为:普通形参,默认形参,数量可变的位置形参,数量可变的关键字形参
1、普通形参(传参时可位置传参,也可关键字传参)
在定义函数时,我们在函数名后的括号内指定形参。形参的数量可以是零个、一个或多个,可以根据需要为函数定义不同数量的形参。形参名称应该是有效的标识符,遵循Python的命名规则。
def my_function(param1, param2, param3):
# 函数体
pass
在这个例子中,param1
、param2
和param3
是三个形参。
2、默认形参(传参时可位置传参,也可关键字传参)
可以为形参指定默认值,这样在调用函数时,如果没有传递对应的实参,将使用默认值。使用默认参数可以使函数调用更加简洁,同时还提供了更多的灵活性。
def greet(name="Guest"):
print(f"Hello, {name}!")
greet() # 输出: Hello, Guest!
greet("Alice") # 输出: Hello, Alice!
在上面的例子中,name
是一个带有默认值的形参。
3、数量可变的位置形参(具有贪婪性:必须使用位置传参)
定义函数时,无法事先确定传递的位置实参的个数时,使用个数可变的位置形参
使用*定义,输出结果为元祖
数量可变的位置形参在形参列表中只能有一个
3.1、数量可变的位置形参和普通形参组合:
- 数量可变的位置形参可在参数列表中的任意位置定义(有数量可变的关键字形参时,数量可变的关键字形参在最后面,数量可变的位置形参在数量可变的关键字形参前面的任意位置)
- 数量可变的位置形参后面的形参,在传递实参的时候必须采用关键字传参,这样才能确定哪些实参是在tuple元祖中的
def fun1(a,b,*args):
print(a)
print(b)
print(args)
fun1(1,2,3,4,5) #1 2 (3,4,5)
def fun2(a,*args,b):
print(a)
print(b)
print(args)
fun2(1,2,3,4,b = 5) # 1 5 (2,3,4)
def fun3(*args,a,b):
print(a)
print(b)
print(args)
fun3(1,2,3,a = 4,b = 5) #4 5 (1,2,3)
4、数量可变的关键字形参(具有贪婪性:必须使用关键字传参)
定义函数时,无法事先确定传递的关键字实参个数时,使用个数可变的关键字形参
使用**定义,输出结果为字典
数量可变的关键字形参在形参列表中只能有一个
4.1、数量可变的关键字形参和普通形参组合
数量可变的关键字形参只能在形参列表的最后面(任何情况下都是在最后面)
因为普通形参有可能位置传参,也有可能关键传参,Python中规定位置传参不能再关键字传参后面,也就是说关键字传参 后面只能是关键字传参,所以数量可变的关键字形参只能在形参列表的最后面
#任何情况下数量可变的关键字形参都必须在形参列表的最后面
def fun1(a,b,**kwargs):
print(a)
print(b)
print(kwargs)
#编译报错:regular parameter after ** parameter
# def fun2(a,**kwargs,b):
# print(a)
# print(b)
# print(kwargs)
#编译报错:regular parameter after ** parameter
# def fun3(**kwargs,a,b):
# print(a)
# print(b)
# print(kwargs)
5、普通形参,数量可变的位置形参,数量可变的关键字形参位置详解
- 任何情况下,数量可变的关键字形参必须在最后面,实参传递必须采用关键字传参
- 普通形参,默认形参,数量可变的位置形参都必须在数量可变的关键字形参前面
- 普通形参必须在默认形参前面
- 数量可变的位置形参后面的普通形参传参必须采用关键字形参
- 数量可变的位置形参后面的默认形参可以传参也可不传参,传参的话必须使用关键字传参
- 数量可变的位置形参前面的默认形参默认采用位置传参,因为关键字传参后面不能有位置传参
6、定义形参 * 特殊用法
*之后的只能采用关键字传参
四、返回值
在python中遇见return表示函数结束,选择性的给调用方一个值。
既可以定义有return语句的函数,也可以定义没有return语句的函数
不带return表达式的函数相当于返回了None return返回值可为任意数据
1、语法
return 返回值表达式
return #等同于return None
执行顺序:
先执行返回值表达式,再执行return,将值返回
2、原则
只要带有return 关键字的语句执行,return语句所在的函数结束
return 数据;返回值和返回值类型必须一致
- 一个函数是可以完成某个特定功能的,这个功能结束后大多数都是需要返回最终的执行结果的,执行结果可能是一个具体存在的数据。而这个具体存在的数据就是返回值
- 返回值是一具体存在的数据,数据都是有类型的,此处需要指定的是返回值的具体类型
- Python中的任意一种数据类型都可以,包括基本数据类型和引用数据类型
- 也可能这个函数执行结束后不返回任何数据,Python中规定,当一个方法执行结束后不返回任何数据的话,返回值类型位置必须编写:None关键字
- 若函数中没有“return 值” 这样的语句,则默认返回值类型为None
- 返回值是None的时候,在方法体中不能编写“return 值 ”这样的语句,但是能编写“return ”这样的语句
- 返回值类型可以是:None,int,float,bool,str......
- 返回值类型若不是None,表示这个方法执行结束之后必须返回一个具体的数据,当方法执行结束的时候没有返回任何类型的数据的话编译报错,
3、单个返回值:
函数的返回值,如果是一个,直接返回原数据类型
def add(x, y) -> int:
return x + y
result = add(3, 5)
print(result) # 输出: 8
在这个例子中,add
函数返回 x + y
的结果,这是一个单独的值。
4、多个返回值:
函数的返回值,如果是多个,返回结果为元祖
- 多个返回值用逗号连接
- 返回值尽量少,若多的话没有值则报错
- 返回值使用逻辑运算符 or 或 and连接:不推荐
or
运算符:当使用or
运算符时,它会返回第一个为真(非零或非空)的操作数,如果所有操作数都为假,则返回最后一个操作数。注意,它不返回布尔值True
或False
,而是返回满足条件的操作数本身or
运算符:当使用or
运算符时,它会返回第一个为真(非零或非空)的操作数,如果所有操作数都为假,则返回最后一个操作数。注意,它不返回布尔值True
或False
,而是返回满足条件的操作数本身
解析赋值形式:
def calculate(x, y):
sum_result = x + y
product_result = x * y
return sum_result, product_result
#解析赋值形式
# sum_result, product_result = calculate(3, 5)
# print(sum_result) # 输出: 8
# print(product_result) # 输出: 15
#返回一个tuple元祖
result = calculate(3,5)
print(result) #(8, 15)
在这个例子中,calculate
函数返回了两个值 sum_result
和 product_result
,实际上返回的是一个包含两个元素的元组。
5、返回空值:
- 在Python中,如果函数没有明确使用
return
语句指定返回值,或者使用return
而后面没有跟任何表达式,那么函数会隐式地返回None
,表示空值。- 如果函数没有返回值(函数执行完毕之后,不需要给调用处提供数据),则return可省略
def greet(name):
print(f"Hello, {name}!")
result = greet("Alice")
print(result) # 输出: None
在这个例子中,greet
函数没有明确的 return
语句,所以返回了 None
。
6、返回值的用途:
返回值在函数中有多种用途。它们可以用于在函数执行后向调用方传递计算结果,也可以用于指示函数执行的成功与否(通常通过返回布尔值 True
或 False
来实现),或者用于在函数执行过程中提前终止函数的执行。
def is_even(number):
return number % 2 == 0
result = is_even(4)
print(result) # 输出: True
def divide(x, y):
if y == 0:
return None # 返回空值表示除法不可行
return x / y
result = divide(10, 2)
print(result) # 输出: 5.0
result = divide(10, 0)
print(result) # 输出: None
总结:Python函数的返回值是通过 return
语句来指定的。它可以是单个值、多个值(实际上返回元组)或者空值(None
)。返回值在函数的调用过程中具有重要的用途,用于传递计算结果、指示函数执行情况或提前终止函数的执行。
五、函数调用(函数名和参数)
函数只定义,不去调用的时候不会执行,只有在调用的时候才会执行函数中的代码
1、语法
函数名(实参列表) #享受封装的成果
2、参数传递原则(值传递就是将原变量中保存的值复制一份保存到其他变量中进行替换)
函数调用的时候,涉及到参数传递的问题,传递的时候,Python只遵循一种语法机制,就是将变量中保存的“值”传递过去了,只不过有的时候这个值是一个字面值10,有的时候这个值是另一个Python对象的内存地址0x1234
3、函数调用执行顺序
- 先给参数局部变量赋值
- 再给函数内部局部变量赋值
- 若是函数内部局部变量和参数局部变量重名,则值进行替换,替换为函数内部局部变量的值
六、实际参数(实参,调用时)
实参:实际参数,指的是调用函数时的赋值,本质是实际值
实参是在函数调用时传递给函数的具体值。它们会赋值给函数的形参,使得函数能够在函数体内使用这些值。
1、原则
1、实参是局部变量,只在函数局部作用域
实参只是作用在函数局部作用域中
2、实参的数量及类型必须和形参数量及类型一致
实参的数量及类型必须和形参数量及类型一致
3、多个实参之间用 ,号隔开
多个实际参数之间使用,号隔开
2、实参分类
1、位置参数传递
当函数定义中的形参是按位置顺序定义的,函数调用时传递的实参也需要按照相同的顺序进行传递。每个实参将与对应位置的形参进行匹配。
def add(x, y):
return x + y
result = add(3, 5)
在这个例子中,3
会传递给形参 x
,5
会传递给形参 y
。
2、关键字参数传递
在函数调用时,可以使用关键字参数指定实参对应的形参名称。这样可以不必考虑形参定义的顺序,提高代码的可读性。
def greet(first_name, last_name):
return f"Hello, {first_name} {last_name}!"
message = greet(last_name="Smith",first_name="Alice")
print(message) #Hello, Alice Smith!
3、位置实参必须在关键字实参前面,关键字实参后面不能有位置实参(关键字实参后面必须全是关键字实参)此规则既适用于实参也适用于形参
如果不是关键字实参或者部分关键字实参则默认都按位置实参进行传递
关键字实参后面必须为关键字实参,不能有位置实参;
全部使用关键字实参时,才可以不用在意参数顺序
七、函数定义和执行原理(内存分析,主要是参数和返回值)
1、函数名存储在当前命名空间中
在Python中,函数的定义是通过使用def
关键字来声明的。函数定义指定了函数的名称、参数列表和函数体。当解释器遇到函数定义时,它将函数的名称添加到当前命名空间,并将函数对象存储在内存中。这样,函数在程序中的其他地方就可以通过函数名进行调用。
2、函数对象是存储在堆内存中的
函数对象在Python中是存储在堆内存中的。在Python中,一切皆为对象,函数也不例外。函数在定义时会创建一个函数对象,并将其存储在堆内存中。
当我们在Python中定义一个函数时,实际上我们正在创建一个函数对象,该对象包含了函数的代码和其他相关信息。这个函数对象在内存中的位置是由Python解释器管理的,它被存储在堆内存中。
由于函数对象是存储在堆内存中的,因此可以将函数作为参数传递给其他函数,也可以将函数作为返回值返回。这种灵活性使得在Python中可以更加方便地进行函数式编程和高阶函数的使用。
示例:
def add(x, y):
return x + y
# 函数 add 是一个函数对象
print(type(add)) # 输出: <class 'function'>
# 可以将函数作为参数传递给其他函数
def apply_operation(operation, a, b):
return operation(a, b)
result = apply_operation(add, 3, 5)
print(result) # 输出: 8
# 也可以将函数作为返回值返回
def get_operation():
return add
add_func = get_operation()
result = add_func(3, 5)
print(result) # 输出: 8
在上面的示例中,add
是一个函数对象,我们可以将其作为参数传递给 apply_operation
函数,也可以将其作为返回值从 get_operation
函数中返回。
总结:在Python中,函数对象是存储在堆内存中的。函数对象可以像其他对象一样进行操作,例如作为参数传递给函数或作为返回值返回。这种灵活性使得Python在函数式编程和高阶函数方面表现优秀。
3、函数调用是在栈中
函数的调用是通过函数名和实参进行的。当函数被调用时,Python会为该函数创建一个新的执行环境,称为调用栈帧。在调用栈帧中,函数的形参和局部变量被创建,并在函数执行期间保存其值。函数体内的代码会在调用栈帧中执行。在函数执行完毕后,调用栈帧被弹出,函数的局部变量也被销毁。
def add(x, y):
return x + y
result = add(3, 5)
在这个例子中,add
函数被调用,创建了一个新的调用栈帧。在该栈帧中,x
被赋值为 3
,y
被赋值为 5
,然后函数体内的代码执行将返回 8
,最终得到的结果 8
被赋值给 result
变量。然后,函数的调用栈帧被弹出,x
和 y
的值被销毁。
4、栈内存分析
在Python中,调用栈由一个个的栈帧构成。当函数被调用时,一个新的栈帧会被创建并添加到调用栈的顶部。栈帧保存了函数的局部变量、形参以及函数的返回地址等信息。
调用栈遵循后进先出(LIFO)的原则。也就是说,最后一个调用的函数首先执行,直到它执行完毕并返回结果。然后,它的调用栈帧被弹出,控制权回到上一个调用的函数。这个过程一直持续到主程序结束。
栈的大小是有限的,当函数嵌套调用过多或者递归层数过深时,可能会导致栈溢出。
注意:Python中的堆内存是用于存储对象和数据结构的,而栈内存主要用于保存函数调用的上下文。函数的局部变量和形参都存储在栈内存中。
八、函数的对象特性
1、函数可以赋值给一个变量
在Python中,函数是一等公民(first-class citizens),这意味着函数可以像其他数据类型一样被操作。因此,Python函数可以赋值给一个变量,就像将函数对象赋值给任何其他类型的变量一样。
将函数赋值给一个变量的过程实际上是将函数对象本身赋值给变量名,而不是执行函数本身。这样,我们可以通过该变量名来引用和调用这个函数。
以下是一个演示将函数赋值给变量的例子:
def say_hello():
print("Hello, World!")
# 将函数赋值给变量
greeting = say_hello
# 调用通过变量调用函数
greeting() # 输出: Hello, World!
在这个例子中,我们定义了一个函数 say_hello
,它用于打印 "Hello, World!"。然后,我们将函数对象 say_hello
赋值给一个变量 greeting
。通过 greeting()
来调用函数,实际上是在调用 say_hello()
。
这种将函数赋值给变量的方法在函数式编程中非常有用。通过将函数赋值给变量,我们可以更加灵活地处理函数,可以将函数作为参数传递给其他函数,或者作为返回值返回。
总结:Python函数是一等公民,函数可以像其他数据类型一样被操作。将函数赋值给一个变量实际上是将函数对象本身赋值给变量名,使得函数能够通过变量名被调用和使用。这种特性使得函数在函数式编程和高阶函数方面表现优秀。
2、函数可以作为参数传递给其他函数
在Python中,函数是一等公民(first-class citizens),这意味着函数可以像其他数据类型一样被操作。函数可以作为参数传递给其他函数,这种特性称为高阶函数(Higher-order functions)。
当将函数作为参数传递给其他函数时,实际上是将函数对象本身作为值传递给目标函数。这允许我们将一个函数的行为定制化,并在需要时将其传递给其他函数来执行特定的操作。
以下是解释 Python 函数可以作为参数传递给其他函数的例子:
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def apply_operation(operation, a, b):
return operation(a, b)
result_add = apply_operation(add, 3, 5)
print(result_add) # 输出: 8,将 add 函数作为参数传递给 apply_operation 函数
result_subtract = apply_operation(subtract, 10, 5)
print(result_subtract) # 输出: 5,将 subtract 函数作为参数传递给 apply_operation 函数
在这个例子中,我们定义了两个函数 add
和 subtract
。然后,我们定义了另一个函数 apply_operation
,它接受三个参数:一个函数 operation
和两个数字 a
和 b
。apply_operation
函数调用 operation(a, b)
,实际上执行了传递的函数(add
或 subtract
)。
通过将不同的函数作为参数传递给 apply_operation
函数,我们可以实现不同的操作。这样可以使代码更具灵活性,能够重用相同的函数逻辑来处理不同的情况。
总结:Python函数可以作为参数传递给其他函数,这允许我们实现高阶函数,提高代码的灵活性和重用性。这种特性使得Python在函数式编程中具有优势,同时也方便了设计和实现许多高级的编程模式。
3、 函数可以作为函数返回值返回
在Python中,函数是一等公民(first-class citizens),这意味着函数可以像其他数据类型一样被操作。函数可以作为另一个函数的返回值,这种特性称为高阶函数(Higher-order functions)。
返回函数的概念被称为闭包(Closure),闭包是由一个函数及其相关的引用环境组合而成的对象。当一个函数返回另一个函数时,返回的函数可以访问在外部函数中定义的变量和参数,即使外部函数已经执行完毕,这些变量和参数的值仍然保留在闭包中。
以下是解释 Python 函数可以作为函数返回值返回的例子:
def get_multiplier(factor):
def multiplier(x):
return x * factor
return multiplier
multiply_by_2 = get_multiplier(2)
multiply_by_3 = get_multiplier(3)
print(multiply_by_2(5)) # 输出: 10,返回的函数乘以 2
print(multiply_by_3(5)) # 输出: 15,返回的函数乘以 3
在这个例子中,我们定义了一个外部函数 get_multiplier
,它接受一个参数 factor
。内部定义了一个函数 multiplier
,这个函数用于将传入的参数与外部函数的 factor
相乘。然后,get_multiplier
返回了这个 multiplier
函数。我们可以将返回的函数存储在变量中(multiply_by_2
和 multiply_by_3
),并随后对其进行调用,它们将保留与外部函数关联的 factor
的值。
这种方式允许我们实现一种类似于工厂函数的功能。通过在函数内部定义函数并返回它,我们可以根据传入的参数来定制返回的函数的行为。
总结:Python 函数可以作为函数返回值返回,这样的函数被称为闭包。闭包允许内部函数访问外部函数的变量和参数,即使外部函数已经执行完毕,这些变量和参数的值仍然保留在闭包中。通过返回函数,我们可以实现更加灵活和定制化的函数功能。
4、函数可以作为元素添加到集合对象中
在 Python 中,集合(Set)是一种无序、不重复的数据集合。集合中的元素可以是任何可哈希(hashable)的数据类型,包括数字、字符串、元组等。由于函数在 Python 中是一等公民(first-class citizens),也是可哈希的,因此可以作为元素添加到集合对象中。
以下是一个演示将函数添加到集合中的例子:
def greet():
print("Hello!")
def farewell():
print("Goodbye!")
# 创建一个空的集合
function_set = set()
# 将函数作为元素添加到集合中
function_set.add(greet)
function_set.add(farewell)
# 调用集合中的函数元素
for func in function_set:
func()
# 输出:
# Hello!
# Goodbye!
在这个例子中,我们首先定义了两个函数 greet
和 farewell
。然后,我们创建一个空的集合 function_set
。通过 function_set.add(greet)
和 function_set.add(farewell)
,我们将函数 greet
和 farewell
添加到集合中。最后,我们通过遍历集合,并调用集合中的函数元素来执行它们。
这种将函数作为集合元素的方式在某些情况下可能会有用,特别是当我们需要对函数进行存储、去重或者进行其他集合操作时。
九、函数嵌套:函数内部还可以定义函数
在Python中,函数是一等公民,具有高度的灵活性和动态性。因此,在Python函数内部,完全可以定义另一个函数。这种在函数内部定义函数的方式称为嵌套函数(Nested functions)。
嵌套函数的特点是:一个函数被定义在另一个函数的代码块内部,并且嵌套函数可以访问外部函数的变量和参数。当外部函数被调用时,内部函数不会被执行,而是返回内部函数本身。这样可以创建一个闭包,内部函数可以保留外部函数的局部变量的状态。
以下是解释 Python 函数内部可以定义函数的例子:
def outer_function(x):
def inner_function(y):
return x * y
return inner_function
multiply_by_2 = outer_function(2)
multiply_by_3 = outer_function(3)
print(multiply_by_2(5)) # 输出: 10,调用内部函数,x = 2, y = 5
print(multiply_by_3(5)) # 输出: 15,调用内部函数,x = 3, y = 5
在这个例子中,我们定义了一个外部函数 outer_function
,它接受一个参数 x
。在外部函数中,我们定义了一个内部函数 inner_function
,这个内部函数用于将传入的参数 y
与外部函数的参数 x
相乘。然后,outer_function
返回了 inner_function
。我们可以将返回的内部函数存储在变量中,并随后对其进行调用,内部函数将保留与外部函数关联的 x
的值。
嵌套函数的使用使得我们可以在函数内部组织和封装代码,将相关的功能放在一起,提高代码的可读性和维护性。
总结:Python 函数内部可以定义函数,这样的函数称为嵌套函数。嵌套函数可以访问外部函数的变量和参数,形成闭包,保留外部函数的局部变量状态。通过嵌套函数,可以在函数内部组织和封装代码,提高代码的可读性和灵活性。
十、函数不支持函数重载
在Python中,函数重载(function overloading)并不是像其他一些编程语言(如Java或C++)中那样的显式特性。Python中没有直接的函数重载机制。
函数重载是指在同一个作用域中定义多个同名函数,但这些函数具有不同的参数列表或不同的参数类型。在其他语言中,编译器根据函数调用时传递的实参的类型或数量,来确定调用哪个重载函数。但是在Python中,函数的名称是全局唯一的,无法通过参数的数量或类型来区分不同的同名函数。
然而,在Python中仍然可以实现类似函数重载的功能,但是通过默认参数和可变参数来实现。例如,可以通过给函数的参数设置默认值,以实现不同参数列表的函数行为。
def add(x, y=0):
return x + y
result1 = add(3)
print(result1) # 输出: 3
result2 = add(3, 5)
print(result2) # 输出: 8
在这个例子中,我们定义了一个名为 add
的函数,它有两个参数 x
和 y
。我们给 y
设置了默认值为 0
,这样在调用函数时,如果不传递 y
的值,它将使用默认值。
另一种实现类似函数重载的方式是使用可变参数(*args
或 **kwargs
)。可变参数允许函数接受不定数量的位置参数或关键字参数,从而实现函数的灵活调用。
虽然Python没有直接的函数重载机制,但通过上述方法,我们可以在函数中实现不同的行为,以适应不同的参数组合。这样可以达到类似函数重载的效果。
十一、函数的递归调用
1、定义
在编程中,递归是指一个函数在其函数体内调用自身的过程。在Python中,函数支持递归调用,这意味着一个函数可以直接或间接地调用自己。
递归调用在解决某些问题时非常有用,尤其是在处理递归定义的数据结构(如树、图等)或者解决涉及到自相似性的问题时。
要实现一个递归函数,需要确保递归过程有一个终止条件,以防止函数无限循环调用自身,导致栈溢出。在递归函数中,递归调用应该朝着终止条件逼近。
2、递归控制条件
赋值运算符和return语句,都是要在左边的语句执行完成后再执行
方法递归算法:
- 使用if else语句控制结束条件,if控制结束条件,else控制调用条件
- 递归从左向右依次展开,结果从右向左依次运算
3、栈内存溢出:RecursionError
- 递归是很耗费栈内存的,递归算法可以不用的时候尽量别用
- 递归必须有结束条件,没有结束条件一定会发生栈内存溢出错误
- RecursionError: maximum recursion depth exceeded while calling a Python object
- 递归即使有了结束条件,结束条件是正确的,也有可能会发生栈内存溢出错误,因为递归的太深了
栈溢出(Stack Overflow)通常发生在递归调用过程中,特别是递归深度很大的情况下。在Python中,递归的深度是有限制的,超过这个限制将导致栈溢出。默认情况下,Python的递归深度限制较大,但仍然存在限制。
以下是一个简单的递归函数,它可能导致栈溢出:
def recursive_function():
recursive_function()
recursive_function()
在这个例子中,我们定义了一个递归函数 recursive_function
,它在函数体内调用自身。这个函数没有终止条件,因此它会无限递归调用自身,导致栈溢出。
在运行这段代码时,Python解释器会抛出RecursionError: maximum recursion depth exceeded
异常,这表明递归的深度超过了Python的限制,导致栈溢出。
需要注意的是,栈溢出是一种严重的错误,通常会导致程序崩溃。在编写递归函数时,一定要确保递归调用有终止条件,并且递归深度不会超过Python的限制,以避免栈溢出问题。
在实际编程中,尽量避免使用过深的递归,特别是在处理大型数据或复杂问题时,应该考虑使用循环或其他非递归的方法来解决问题。
4、斐波那契数列演示递归
#斐波那契数列:1,1,2,3,5,8,13,21......
#需求:求出第n个位置处的值
def fun(n:int)-> int:
if(n == 1 or n == 2):
return 1
else:
return fun(n - 2) + fun(n - 1)
print(fun(7)) #13
5、阶乘演示递归
#需求:求出数字n的阶乘
def fun(n:int) -> int:
if n == 0:
result = 0
elif n == 1:
result = 1
else:
result = n * fun(n - 1)
return result
print(fun(5)) #120
十二、函数中的局部变量与全局变量
在Python中,局部变量可以是可变数据类型和不可变数据类型。它们在函数执行过程中的行为有一些重要区别。
1、局部变量与全局变量的区别
变量的作用域:程序代码能访问该变量的区域
- 局部变量:在函数内部定义并使用的变量(包括形参和实参),只在函数内部有效
- 当在函数内部对一个变量进行赋值操作时,Python会将该变量视为局部变量
- 全局变量:函数体外定义的变量,可作用于函数内外
如果函数内部有局部变量,优先使用局部变量,否则使用全局变量;变量之间可重名,互不影响。全局变量顺序执行,重名后以最后一个为准。局部变量和全局变量重复时,局部变量会屏蔽全局变量。
在Python中,当在函数内部对一个变量进行赋值时,Python会将该变量视为局部变量。
由于a += 1,等同于a = a + 1,为同名局部变量a,当在局部作用域中找右边的a时,在局部作用域中找到了左边的a,但是a只有声明,并未赋值。
这意味着在函数内部,Python将会在函数的本地作用域中查找该变量,并且不会再访问或修改全局作用域中的同名变量。这就是为什么在你的例子中,对 a
进行 a += 1
赋值操作后,函数无法识别全局变量 a
的原因。
2、global声明函数内部的局部变量为全局变量
global
是Python中的一个关键字,用于在函数内部声明一个变量为全局变量,这样在函数内部对该变量的修改会反映到全局作用域,而不是在函数内部创建一个新的局部变量。
当在函数内部对一个变量进行赋值操作时,Python会将该变量视为局部变量。如果在函数内部对一个全局变量进行赋值操作,Python会创建一个新的局部变量,而不是修改全局变量。
为了在函数内部访问和修改全局作用域中的同名变量,我们可以使用 global
关键字。global
关键字用于声明变量是全局变量,这样在函数内部的赋值操作就会修改全局变量。
下面是 global
关键字的用法:
global variable_name
variable_name
是需要声明为全局变量的变量名。
以下是一个例子来说明 global
关键字的用法:
x = 10
def increment_x():
global x # 使用 global 声明 x 为全局变量
x += 1
print("Inside function:", x)
increment_x() # 输出: Inside function: 11
print("Outside function:", x) # 输出: Outside function: 11
在这个例子中,我们声明了一个全局变量 x
并赋值为 10。然后,我们定义了一个函数 increment_x()
,在函数内部使用 global
关键字声明 x
是全局变量。这样,当函数 increment_x()
内部对 x
进行修改后,全局变量 x
的值也被修改了,函数外部的 x
也变为 11。
3、函数执行过程中局部变量是可变数据类型和不可变数据类型的区别
1、可变数据类型
可变数据类型是指在创建后其内容可以被修改的数据类型。在函数内部,如果局部变量是可变数据类型(如列表、字典或集合),则函数可以修改该变量的内容,并且这种修改是在原地进行的,也就是说,修改后的结果会影响到函数外部。
示例:
def modify_list(lst):
lst.append(4)
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list) # 输出:[1, 2, 3, 4]
在上面的例子中,my_list
是一个列表,函数modify_list
可以修改my_list
的内容,在函数内部使用append
方法添加了元素4,这个修改影响了函数外部的my_list
。
2、不可变数据类型
不可变数据类型是指在创建后其内容不可被修改的数据类型。在函数内部,如果局部变量是不可变数据类型(如整数、字符串、元组),则函数无法直接修改该变量的内容,任何修改操作都会创建一个新的对象,而不会影响函数外部的变量。
示例:
def modify_string(s):
s += " World"
my_str = "Hello"
modify_string(my_str)
print(my_str) # 输出:Hello
在上面的例子中,my_str
是一个字符串,函数modify_string
尝试修改my_str
的内容,但由于字符串是不可变的,所以实际上在函数内部创建了一个新的字符串"Hello World",而并没有修改函数外部的my_str
。
3、结论
- 可变数据类型的局部变量在函数内部可以被修改,并影响函数外部的变量。
- 不可变数据类型的局部变量在函数内部无法直接修改,任何修改都会创建一个新的对象,不会影响函数外部的变量。
十三、使用序列解包方式进行实参传递
1、将序列中的每个元素都转换为位置实参 *
在Python中,你可以使用 *
运算符来将序列(如列表或元组)中的每个元素作为位置实参传递给函数。这个过程称为序列解包或可变位置参数传递。
在函数调用时,如果使用了 *
运算符来调用一个函数,并将一个序列作为参数传递给函数,Python会自动将序列中的每个元素拆分,并将它们作为位置实参传递给函数。
在使用
*
进行序列解包位置传参时,只能使用在列表和元组上,不能用于其他类型的序列,比如字符串和集合。
*
运算符专门用于可迭代对象(如列表和元组)。当你在函数调用时使用 *
对一个列表或元组进行解包,它会将序列中的元素拆分,并将它们作为单独的位置实参传递给函数。
以下是一个示例,演示如何在列表和元组上使用 *
进行序列解包位置传参:
def add(x, y, z):
return x + y + z
my_list = [1, 2, 3]
my_tuple = (4, 5, 6)
# 使用 * 对列表进行解包
result_list = add(*my_list)
print(result_list) # 输出: 6,相当于调用 add(1, 2, 3)
# 使用 * 对元组进行解包
result_tuple = add(*my_tuple)
print(result_tuple) # 输出: 15,相当于调用 add(4, 5, 6)
在这个例子中,我们定义了一个函数 add
,它接受三个位置实参,并返回它们的和。然后,我们创建了一个包含三个元素的元组 numbers
。使用 *numbers
的方式在调用 add
函数时,将元组中的元素 1, 2, 3
作为位置实参传递给函数,相当于调用 add(1, 2, 3)
。
这种序列解包的方式在函数调用和传参的时候非常有用,可以让你方便地将一个序列中的元素作为实参传递给函数。注意,使用
*
运算符时,序列中的元素数量必须和函数的参数数量相匹配,否则会引发错误。
2、将字典中的每个键值对都转换为关键字实参 **
在Python中,你可以使用 **
运算符来将字典中的每个键值对转换为关键字实参传递给函数。这个过程称为字典解包或可变关键字参数传递。
在函数调用时,如果使用了 **
运算符来调用一个函数,并将一个字典作为参数传递给函数,Python会自动将字典中的每个键值对解包,并将它们作为关键字实参传递给函数。
以下是一个示例来说明如何使用 **
运算符将字典中的键值对作为关键字实参传递给函数:
def greet(name, age):
print(f"Hello, {name}! You are {age} years old.")
# 定义一个包含关键字参数的字典
person_info = {"name": "Alice", "age": 30}
# 使用 ** 运算符将字典中的键值对作为关键字实参传递给函数
greet(**person_info)
# 输出:Hello, Alice! You are 30 years old.
在这个例子中,我们定义了一个函数 greet
,它接受两个关键字实参 name
和 age
,并输出相应的问候语。然后,我们创建了一个包含关键字参数的字典 person_info
,其中包含了 name
和 age
的键值对。使用 **person_info
的方式在调用 greet
函数时,将字典中的键值对解包,并将它们作为关键字实参传递给函数,相当于调用 greet(name="Alice", age=30)
。
这种字典解包的方式在函数调用和传参的时候非常有用,可以让你方便地将一个字典中的键值对作为关键字实参传递给函数。注意,使用
**
运算符时,字典的键必须和函数的关键字参数名相匹配,否则会引发错误。
第十二章、闭包机制
一、概述
闭包机制使得内部函数与外部函数之间形成封闭作用域,使得内部函数可以记住外部函数局部作用域中局部变量的状态
闭包(Closure)是指在函数内部定义的函数,并且该内部函数可以记住外部函数的局部变量的状态,可以进行访问。Python中的闭包机制允许函数在其定义的作用域之外“记住”其他作用域中的变量。这使得函数可以在函数外部使用封闭作用域中的变量,并且这些变量的值会在函数调用之间保持。
1、相关概念
闭包函数:外部函数中嵌套的内部函数称为闭包函数
闭包变量:闭包函数中的局部变量称为闭包变量
闭包作用域:闭包函数的作用域称为闭包作用域
二、作用
闭包通常用于创建函数工厂,即在外部函数中定义一个模板函数,然后根据不同的参数返回不同的内部函数。这样,可以实现一种灵活的功能定制,而不必每次都定义新的函数。
下面是一个简单的闭包示例:
def outer_function(x):
def inner_function(y):
return x + y
return inner_function
# 创建闭包函数 add_to_5,它记住了外部函数 outer_function 的 x 值
add_to_5 = outer_function(5)
# 调用闭包函数 add_to_5,并传入参数 3
result = add_to_5(3)
print(result) # 输出: 8,因为 outer_function 中的 x 是 5,add_to_5(3) 结果为 5 + 3 = 8
在这个例子中,我们定义了一个外部函数 outer_function
,它接受一个参数 x
,然后在函数内部定义了一个内部函数 inner_function
,内部函数可以访问外部函数的参数 x
。outer_function
返回内部函数 inner_function
,形成了一个闭包。我们通过调用 outer_function(5)
来创建一个闭包函数 add_to_5
,它“记住”了 x
的值为 5。随后,我们调用闭包函数 add_to_5(3)
,实际上是在 inner_function
中进行了计算,得到结果 8。
通过闭包,我们可以实现一些有趣且灵活的功能,尤其在函数工厂、装饰器和回调函数等场景下。使用闭包可以让代码更加简洁、模块化和可重用。
三、闭包绑定方式
在Python中,闭包有两种绑定方式:延迟绑定(late binding)和立即绑定(early binding)。这两种绑定方式涉及到闭包中的变量的作用域和生命周期。
1、延迟绑定(Late Binding):
延迟绑定是指闭包中的变量在函数被调用时才进行绑定,而不是在函数定义时进行绑定。
这意味着闭包中的变量在函数定义时可以是非局部的,而在函数调用时会根据实际的值进行绑定。延迟绑定使得闭包中的变量可以捕获到它在外部作用域中的最新值。
下面是一个延迟绑定的示例:
def outer_function():
x = 10
def inner_function():
print(x)
x = 20 # 修改 x 的值
return inner_function
closure = outer_function()
closure() # 输出: 20,而不是输出 10
在这个例子中,闭包函数 inner_function
延迟绑定了变量 x
。当 closure()
被调用时,它输出了 x
的当前值,即 20,而不是在 outer_function
定义时的值 10。
2、立即绑定(Early Binding):
使用默认值形参完成立即绑定
立即绑定是指闭包中的变量在函数定义时就进行绑定,而不是在函数被调用时。这意味着闭包中的变量在函数定义时就被确定下来,并且在函数的生命周期内保持不变。
下面是一个立即绑定的示例:
def outer_function():
x = 10
#立即进行闭包绑定
def inner_function(x = x):
print(x)
x = 30
return inner_function
closure = outer_function()
closure() # 输出: 10,而不会受到后续对 x 的修改影响
在这个例子中,闭包函数 inner_function
立即绑定了变量 x
。当 closure()
被调用时,它输出的是 x
在 outer_function
定义时的值,即 10。即使在调用 closure()
之前修改了 x
的值,闭包函数 inner_function
仍然输出的是定义时的值 10。
总结:
- 延迟绑定:闭包中的变量在函数被调用时绑定,可以捕获到外部作用域中的最新值。
- 立即绑定:闭包中的变量在函数定义时绑定,不受后续对外部作用域中同名变量的修改影响。