Python 学习笔记(06)函数
文章目录
6.1 函数基础
函数作为对【程序逻辑】进行结构化、过程化的具体表现,是面向过程的程序设计方式的基本单元。
6.1.1 函数定义
def func():
...
<return>
"""
关键字def name(): 表示定义一个函数体;
1) 关键字return负责返回函数结果,无返回值时可以选择:
1.不使用return,利用缩进和空行表示函数结束;
2.仅return,返回值为None
2) 当需要返回值时可以选择:
1.仅有一个返回值/对象 return xxx;
2.返回多个对象时,实质上以【元组】形式返回
执行到return时,停止执行函数内余下的语句,直接跳出函数体回到主程序
"""
def blank_func():
pass
"""
关键字pass 占位符,表示一条什么也不做的语句,主要作用为在开发、调试过程中用来先确定结构,或是在异常处理中暂时不采取任何措施。
"""
6.1.2 函数调用&函数引用
def func():
...
return
object_1 = func() # 函数调用,return返回值传递到object_1
other_func = func # 函数引用,相当于给函数再起一个名字
object_2 = other_func() # 同func(),调用函数
"""
函数的引用可以看作访问,即赋予一个别名变量。函数名本质上还是【变量】,可有多个别名但表示的都是对同一函数对象的引用。
函数也可以作为参数,在函数体内被调用。
"""
# 斐波那契数列实现
def calculate(n):
if n == 0:
sum = 0
elif n == 1:
sum = 1
else:
sum = calculate(n - 1) + calculate(n - 2)
return sum
print(calculate(10))
6.2 函数参数
6.2.1 parameter 和 argument
parameter为函数定义时的叫法,其指明了函数可接收的argument类型,可以理解为形参;
argument为函数调用时的叫法,可以理解为实参。
6.2.2 函数定义完整语法
**参数原则: **关键字参数不能在位置参数前面
def func(positional_args, keyword_args, *var_positional, **var_keyword)
def func(position_only, /, positional_or_keyword, *, keyword_only)
"""
1.positional_args 代表位置参数,即传参时前面不指定变量名称,按照传递顺序赋值给相应变量;
2.keyword_args 代表关键字参数,即顺序可变,按名称赋值给同名变量;
3.*var_positional 代表包裹位置参数,传参时不能带变量名,一般写作 *args,其作用为发送一个非键值对的可变数量的参数列表给一个函数, *args 会将之前的参数全部转化为位置参数, 之后的参数全部转化为关键字参数;
4.**var_keyword 代表包裹关键字参数,一般写作**kwargs,允许将不定长度的键值对作为参数传递给一个函数,如果想要在一个函数里处理带名字的参数, 则应该使用**kwargs。
1.position_only 代表仅位置参数,处在“/”之前,传参时不带变量名称;
2.positional_or_keyword 代表位置或关键字参数,我们自行定义函数以及参数时使用的就是这个部分;
3.keyword_only 代表仅关键字参数,在*后面定义的参数(或在*args后面定义),传参时必须带变量名;
必需参数:在定义时没有默认值的参数,调用时不可省略。
可选参数:在定义时有默认值的参数,调用时可省略。
"""
def func(a, b=2, /, c, d=4, *, e=5):
pass
"""
a, c为必需参数, b, d, e为可选参数;
a, b为仅位置参数,c, d为位置或关键字参数,e为仅关键字参数。
"""
def func(a):
pass
"""
def func(/, a, *):
pass
"""
参数定义顺序:
按传递方式:位置参数、关键字参数、包裹位置参数、包裹关键字参数。
按参数类型:必需参数、默认参数、可变参数、关键字参数。
6.2.3 参数传递方式
1)位置传递
位置参数、必需参数 依次按照顺序传入参数;
默认参数 声明了默认值的参数。
2)关键字传递
若提供了参数的名字,则对应名字传入;若未提供,则按照顺序依次传入。
参数的默认值必须指向【不变对象】。
# 关键字传递中关于list存在的问题
def func(x, list_1 = []):
for i in range(x):
list_1.append(i)
print(list_1)
if __name__ == "__main__":
func(4)
func(5)
Out:
[0, 1, 2, 3]
[0, 1, 2, 3, 0, 1, 2, 3, 4]
"""
可以看到,两次调用函数默认参数list并未清空,而是继续上一次的结果进行操作。
当定义函数时,实际上自动保存了函数中的默认参数list的值,也就是列表list_1 = [];
每次调用的时候如果传递了新的列表,则会使用传递的列表,如果没有传递那么将会自动使用定义函数时保存的默认list参数。
"""
# 第一种解决办法
def func(x, list_1 = []):
for i in range(x):
list_1.append(i)
print(list_1)
if __name__ == "__main__":
list_1 = []
func(4)
func(5, list_1)
# 第二种解决办法
def func(x, list_1 = []):
if list_1 != None:
list_1 = []
for i in range(x):
list_1.append(i)
print(list_1)
if __name__ == "__main__":
func(4)
func(5)
3)包裹传递
包裹传递的目的在于传入【不定个数】的参数组,包括元组(非关键字参数)、字典(关键字参数)。
包裹传递有两种方式:包裹位置参数传递,包裹关键字参数传递。
# 包裹位置参数传递方式
def func(*args):
pass
if __name__ == "__main__":
list_1 = [1, 2, 3]
func(*list_1) # 先组装成list/tuple再传入
func(1, 2, 3) # 直接传入一串值
"""
将任意数量的位置参数(非键值对、传参时不指定变量名称)封装成元组,args收集所有的参数,根据位置顺序合并成tuple。
"""
# 包裹关键字参数传递方式
def func(**kwargs):
pass
if __name__ == "__main__":
dict_1 = {'key_1':'value_1', 'key_2':'value_2'}
func(**dict_1) # 先组装成dict再传入
func(key_1='value_1', key_2='value_2') # 直接传入多个键值对
"""
将任意数量的关键字参数(键值对)封装成字典,允许参数缺失、不按顺序,kwargs收集所有的参数,组装成一个字典。
"""
# 解包裹
def func(*args, **kwargs):
print(args)
print(kwargs)
if __name__ == "__main__":
list_1 = [1, 2, 3]
dict_1 = {'key_1':'value_1', 'key_2':'value_2'}
func(*list_1, 1, 2, 3, **dict_1, key_3='value_1', key_4='value_2')
Out:
(1, 2, 3, 1, 2, 3)
{'key_1': 'value_1', 'key_2': 'value_2', 'key_3': 'value_3', 'key_4': 'value_4'}
"""
1.首先拆解args,按顺序传给必须参数、默认参数、可变参数;
2.再拆解kwargs,传给关键字参数。
包裹和解包裹为两个相对独立的过程,并非相反操作。
"""
# 关于 * 以及 ** 的问题
def func_1(*argc):
print("func_1:", argc)
def func_2(argc):
print("func_2:", argc)
if __name__ == "__main__":
list_1 = [1, 2, 3]
fun_1(*list_1)
fun_1(list_1)
fun_2(*list_1)
fun_2(list_1)
Out:
func_1: (1, 2, 3)
func_1: ([1, 2, 3],)
TypeError: func_2() takes 1 positional argument but 3 were given
func_2: [1, 2, 3]
"""
* ** 分别用来拆解list&tuple和dict,拆解为单个参数;
func(*args),结果元组中的每一个元素对应一个位置参数;
func(**kwargs),结果字典中的每个键值对对应一个关键字参数。
"""
6.2.4 参数传递形式
参数传递形式可以分为值传递和指针传递。
1)值传递
参数为基本数据类型,值传递方式传递参数时,变量传递给函数后函数会在内存中复制一个新的变量,对原有变量没有影响,函数指向这个新的引用对象。
2)指针传递
参数为list,指针传递方式传递参数时,变量传递给函数的是指针,指针指向序列在内存中的位置。在函数中对list进行操作时会在原有内存中进行,从而影响原有变量。
6.3 装饰器
python装饰器是一种拓展原来函数功能的函数,这个函数的特殊之处在于它的返回值也是一个函数,使用装饰器的好处在于不用修改原函数代码的基础上给函数增加新的功能。
# 原函数
import time
def func():
print("hello")
time.sleep(1)
print("world")
# 侵入式修改
import time
def func():
startTime = time.time()
print("hello")
time.sleep(1)
print("world")
endTime = time.time()
print("time is %d ms" % ((endTime - startTime)*1000))
# 类装饰器式修改
import time
def decorate(func):
startTime = time.time()
func()
endTime = time.time()
print("time is %d ms" % ((endTime - startTime)*1000))
def func():
print("hello")
time.sleep(1)
print("world")
if __name__ == "__main__":
decorate(func)
# 简单装饰器
import time
def decorate(func):
def wrapper(*args, **kwargs):
startTime = time.time()
func(*args, **kwargs)
endTime = time.time()
print("time is %d ms" % ((endTime - startTime)*1000))
return wrapper
@decorate
def func_1(a, b):
print("hello")
time.sleep(1)
print("%d world" % (a + b))
@decorate
def func_2(a, b, c):
print("hello")
time.sleep(1)
print("world %d" % (a + b + c))
if __name__ == "__main__":
func_1(1, 2)
func_2(1, 2, 3)
"""
这里的decorate()函数就是最原始的装饰器,其参数为一个函数,返回值也是一个函数。作为参数的函数在返回函数wrapper()内部执行,定义完成后只需要在原函数前面加上@decorate即可,相当于执行语句 func = decorate(func)
装饰器就像一个注入符号一样,使用装饰器就可以不侵入函数内更改代码,也不需要重复执行原函数。
*args 是用来发送一个非键值对的可变数量的参数列表给一个函数;
**kwargs 允许将不定长度的键值对作为参数传递给一个函数,如果想要在一个函数里处理带名字的参数, 则应该使用**kwargs;
在这个函数中只使用了*args,因为需要传递的参数没有键值对。
"""
# 多个装饰器堆叠
import time
def decorate_1(func):
def wrapper(*args, **kwargs):
print("Decorate_1:")
startTime = time.time()
func(*args, **kwargs)
endTime = time.time()
print("time is %d ms" % ((endTime - startTime)*1000))
print("Decorate_1 ends")
return wrapper
def decorate_2(func):
def wrapper(*args, **kwargs):
print("Decorate_2:")
func(*args, **kwargs)
print("Decorate_2 ends")
return wrapper
@ decorate_1
@ decorate_2
def func_1(a, b):
print("hello")
time.sleep(1)
print("%d world" % (a + b))
if __name__ == "__main__":
func_1(1, 2)
Out:
Decorate_1:
Decorate_2:
hello
3 world
Decorate_2 ends
time is 1002 ms
Decorate_1 ends
"""
多个装饰器的执行顺序可以看作栈(Stack),即一种先进后出的数据结构。
整个执行过程为:装饰器1 -> 装饰器2 -> 函数体 -> 装饰器2 -> 装饰器1
"""
6.4 函数式编程
函数式编程属于一种【编程范式】,即编写程序的一种方法。其本质属于【结构化编程】,采用子程序、程式码区块以及for\while循环组合方式进行编程。
函数式编程的主要思想是将运算过程尽量写作一系列嵌套的函数调用。其特点在于允许将函数作为参数,传入另一个函数中,最终结果将返回一个函数。
python作为解释型脚本语言并非函数式编程语言,但也支持一些函数式编程语言的构建,例如匿名函数、内建函数中的**filter()、map()**以及偏函数等,本质上是通过【封装对象】实现【函数式编程】。
6.4.1 匿名函数 lambda()
在python中,匿名函数lambda的语法是唯一的,其形式如下:
lambda argument_list: expression
"""
其中,lambda是python中预留的关键字,argument_list和expression由用户自定义;
这里的argument_list为参数列表,可以使用位置参数、关键字参数、包裹位置参数以及包裹关键字参数;
这里的expression是一个关于参数的表达式。表达式中出现的参数需要在argument_list中有定义,并且表达式只能是单行的;
"""
特性
1)lambda函数是匿名的,所谓匿名是指其没有函数名,只是一条语句的形式;
2)lambda函数具有输入及输出,lambda函数的输入就是参数列表argument_list,其输出为根据表达式expression计算的值;
3)lambda函数一般实现简单的功能:单行的表达式决定lambda函数不能用来实现复杂的逻辑。
lambda x, y: x * y
lambda *args: sum(args)
使用
# 赋值给一个变量
func = lambda x, y: x * y
func(3, 3)
"""
lambda函数生成了一个函数对象,其参数为x, y,返回值为x*y,最后将这个函数对象赋值给func
"""
# 顶替函数原有功能
import time
time.sleep = lambda x: None
"""
通过将lambda函数赋值给sleep函数,在后续调用该函数时将不会执行原有的功能。
"""
# 作为返回值返回
def func(x, y):
return lambda x, y: x * y
"""
函数的返回值也可以是函数。这时,lambda函数实际上是定义在某个函数内部的函数,称之为嵌套函数,或者内部函数。对应的,将包含嵌套函数的函数称之为外部函数。内部函数能够访问外部函数的局部变量,这个特性是闭包(Closure)编程的基础。
"""
6.4.2 过滤函数 filter()
**filter()**函数用于过滤掉序列中不符合函数条件的元素。
filter(func, seq)
"""
其中func可以是匿名函数或其他自定义函数,它会对后面的seq序列中的每个元素进行判定是否满足条件,返回True/False,最终只留下True的元素;
seq序列可以为列表、元组或字符串
"""
使用
filter(lambda n: n % 2, seq)
[n for n in seq if n % 2]
"""
以上两种写法表达的含义相同
"""
6.4.3 序列映射函数 map()
**map()**函数主要用于求一个序列或多个序列进行函数映射之后的值,即将原序列通过某函数改变为另一序列。
map(func, seq1[, seq2, ...])
"""
map函数将func函数的功能依次作用于seq中每一个元素,每次作用的结果储存在返回的list中,可以有n个seq对应func函数的n个参数。
"""
使用
map(lambda x: x ** 2, range(5)) # [n ** 2 for n in range(5)]
map((lambda x, y: x + y), seq1, seq2)
map(None, seq1, seq2) # zip(seq1, seq2)
6.4.4 压缩运算函数 reduce()
**reduce()**用于对序列进行压缩运算,最终得到一个值。
from functools import reduce
functools.reduce(func, seq[, int])
"""
其中func为一个二元函数,共接收两个参数,第一个参数为上一次的运算结果,第二个参数为序列的下一个元素;
int值设定一个初始化器,若进行设定则第一次计算为int和序列第一个元素。
"""
使用
from functools import reduce
functools.reduce(lambda x, y: x + y, range(5)) # (((0 + 1)+ 2)+ 3)+ 4
6.4.5 偏函数
偏函数结合了函数式编程、默认参数以及可变参数,作用为固定某些原有函数的某些参数即设置默认值,最终返回新的函数以供调用。
偏函数的意义在于使代码紧凑易读,当原函数的参数过多需要简化时,使用functools.partial
创建一个新的函数固定住原函数的部分参数。
from functools import partial
functools.partial(func, *args, **kwargs)
"""
func: 原函数
"""
int('12345', base=2) # 原函数
def int_2(x, base=2): # 函数式编程
return int(x, base)
int_2('12345')
int_2 = functools.partial(int, base=2) # 偏函数
6.5 递归函数
递归函数即函数在内部调用【自身】。
# 斐波那契数列实现
def calculate(n):
if n == 0:
sum = 0
elif n == 1:
sum = 1
else:
sum = calculate(n - 1) + calculate(n - 2)
return sum
print(calculate(10))
在使用递归时需要注意【栈溢出】问题:
1)原因
每进入一个函数调用时,栈就会增加一层栈帧;每当函数返回时,栈就会减一层栈帧,当递归调用次数过多时就会出现【栈溢出】。
2)解决
使用尾递归进行优化,本质上效果与循环相似,即某个函数的最后一步为调用另一个函数。
# 阶乘普通递归写法
def factorial(n):
if n == 1:
return n
return n * factorial(n - 1)
# 阶乘尾递归写法
def factorial(n):
return factorial_iter(n, 1)
def factorial_iter(n, product):
if n == 1:
return product
return factorial_iter(n - 1, n * product)
"""
由于尾调用为函数的最后一步操作,调用位置、内部变量等信息都不会再用到,所以不需要保留外层函数的调用记录。尾递归优化可以有效的防止栈溢出,但是尾递归优化需要编译器或者解释器的支持,遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的factorial(n)函数改成尾递归方式,也会导致栈溢出。
"""
6.6 变量作用域
标识符的作用域:其声明在程序里的可应用范围。
6.6.1 类的变量作用域
class variable:
a = "类变量"
def showvarible(self):
b = "函数变量"
print(a)
print(b)
if __name__ == "__main__":
variable().showvarible()
Out:
NameError: name 'a' is not defined
"""
类中的变量不可以在函数中直接访问
"""
class variable:
a = "类变量"
def showvarible(self):
b = "函数变量"
print(self.a)
print(variable.a)
print(b)
if __name__ == "__main__":
variable().showvarible()
Out:
类变量
类变量
函数变量
"""
可以使用self方法进行访问
"""
class variable:
def __init__(self, a):
self.a = "类变量"
def showvarible(self):
b = "函数变量"
print(self.a)
print(b)
if __name__ == "__main__":
variable(1).showvarible()
"""
通过构造函数给定一个参数,需要注意的是,实例化的时候必须给参数,由于python是动态语言不需要指定参数的类型,可以放int,比如1;也可以给一个字符串。
"""
6.6.2 全局变量及局部变量
全局变量和局部变量的最大区别在于局部变量只能通过函数访问,而全局变量可以直接访问。
全局变量在函数内可以被引用,但不能被修改除非声明global
;全局变量除非被删除,否则存活到脚本运行结束。
局部变量在函数体内被定义,只允许函数内访问;局部变量的存在与否依赖于函数是否处于【活跃状态】。
6.6.3 python中的变量作用域
python中的变量作用域共有四种:
·L(local) 局部作用域
·E(Enclosing) 闭包函数外的函数中
·G(Global) 全局作用域
·B(Built-in) 内建作用域
python中查找变量的规则为 L -> E -> G -> B
,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内建中找。
var = 1
def func():
var = 2
return var
if __name__ == "__main__":
print(func())
print(var)
Out:
2
1
"""
在 def/class/lambda内进行赋值,就变成了其局部的作用域,局部作用域会覆盖全局作用域,但不会影响全局作用域。
"""
var = 1
def func():
var = var + 1
return var
if __name__ == "__main__":
print(func())
Out:
UnboundLocalError: local variable 'var' referenced before assignment
"""
在函数的内部,解释器探测到var被重新赋值了,所以var成为了局部变量,但是在没有被赋值之前就想使用var,便会出现这个错误。解决的方法是在函数内部添加 globals var 但运行函数后全局的var也会被修改。
"""
var = 1
def func():
global var
print(var)
var = 200
return var
if __name__ == "__main__":
print(func())
print(var)
Out:
1
200
200
6.7 返回(回调)函数
回调函数就是一个通过函数指针调用的函数,如果把函数的指针(地址)作为一个参数传递给另一个参数,当这个指针被用来调用其所指向的函数时,这就是所说的回调函数。简单来说,回调函数就是把函数当成一个参数传递到函数中。
首先至少要有 3 种类型的函数
- 主函数:相当于整个程序的引擎,调度各个函数按序执行
- 回调函数:一个独立的功能函数,如写文件函数
- 中间函数:一个介于主函数和回调函数之间的函数,登记回调函数,通知主函数,起到一个桥梁的作用
# 回调函数1
def callback_1(x):
return x * 2
# 回调函数2
def callback_2(x):
return x ** 2
# 中间函数
def middle(x, func):
return 100 + func(x)
if __name__ == "__main__":
x = 1
a = middle(x, callback_1)
print(a)
b = middle(x, callback_2)
print(b)
c = middle(x, lambda x: x + 2)
print(c)
Out:
102
101
103
"""
首先在主函数执行过程中需要用到一个功能 x * 2,而 callback_1 函数就提供这个功能,把这个函数称之为 回调函数;
假设主函数要调用它,但是有的时候在开发过程中遇到需要写硬盘的操作,这时候为了避免程序的阻塞,就需要用到异步 I/O。正是因为这种机制所以得有一个登记回调函数和通知主函数执行完成的“地方”,这个地方就是中间函数。
"""
回调函数执行流程:
- 主函数需要调用回调函数
- 中间函数登记回调函数
- 触发回调函数事件
- 调用回调函数
- 响应回调事件
回调实际上有两种:阻塞式回调 和 延迟式回调,也可以叫做 同步回调 和 异步回调。
两者的区别在于:
在阻塞式回调里,回调函数的调用一定发生在主函数返回之前;
在延迟式回调里,回调函数的调用有可能是在起始函数返回之后。
最后用一个例子说明一下到底说明是回调函数:
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。
在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做 触发回调事件,店员给你打电话叫做 调用回调函数,你到店里去取货叫做 响应回调事件。