七、函数
设计一个程序
期待结果
*
***
*****
*******
*********
***********
*********
*******
*****
***
*
*
***
*****
*******
*********
***********
*********
*******
*****
***
*
如果没有函数,我们的实现方式:
# 函数是一种代码组织形式
# 函数的有点
(1) 避免重复工呢
(2) 解耦
# 打印一个菱形 rows = 6 i = j = k = 1 # 菱形的上半部分 for i in range(rows): for j in range(rows - i): print(" ", end=" ") j += 1 for k in range(2 * i - 1): print("*", end=" ") k += 1 print("\n") # 菱形的下半部分 for i in range(rows): for j in range(i): print(" ", end=" ") j += 1 for k in range(2 * (rows - i) - 1): print("*", end=" ") k += 1 print("\n")
这种方式会出现大量重复代码。对于阅读和维护整个程序都会变得十分麻烦。
这时候,函数就出现了
简答说,函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,避免大量重读的代码。
刚才的程序函数版:
def print_ling(): rows = 6 i = j = k = 1 # 菱形的上半部分 for i in range(rows): for j in range(rows - i): print(" ", end=" ") j += 1 for k in range(2 * i - 1): print("*", end=" ") k += 1 print("\n") # 菱形的下半部分 for i in range(rows): for j in range(i): print(" ", end=" ") j += 1 for k in range(2 * (rows - i) - 1): print("*", end=" ") k += 1 print("\n") print_ling() print_ling()
7.1、函数声明
声明一个函数,也就是创建一个函数,可以理解为将一个重复使用的代码通过关键字def包裹起来。具体的语法格式如下:
""" def 函数名(参数列表): ''' # 函数文档 params: return ''' # 实现特定功能的多行代码 [return [返回这]] """
其中,用[]括起来的为可选部分,即可以使用,也可以省略。此格式中,各部分参数的含义如下:
- 函数名:一个符合 Python 语法的标识符,最好见名知意,多个单词可以使用
_
表示,比如cal_sum
- 形参列表:设置该函数可以接收多少个参数,多个参数之间用逗号( , )分隔。
- [return [返回值] ]:整体作为函数的可选参参数,用于设置该函数的返回值。
- python的函数体是通过冒号+缩进声明的
def foo(): print("fool函数")
7.2、函数调用
函数的声明并没有执行函数中的代码块,想要执行函数体,需要进行函数调用。一个函数可以调用多次。
函数调用语法
# 函数名() # 不考虑参数和返回值的调用
def foo(): print("foo函数") foo()
debug模式运行:
# 函数的声明 def bar(): print("bar1") print("bar2") print("bar3") print("bar4") # 函数的声明 def foo(): print("foo1") print("foo2") print("foo3") # 函数调用 foo() # 函数调用 bar() # 函数调用 foo()
7.3、函数参数
像上面我们局的例子,我想打印一个八层菱形和六层菱形,怎么涉及呢?
7.3.1、形参和实参
def cal_sum(): ret = 0 for i in range(1, 101): ret += i print(ret) cal_sum()
但是问题来了,如果我想计算1-200的和怎么呢,在声明一个函数吗?明显我们会发现计算1-100和1-200的逻辑是相同的,只有一个动态的变化值,所以我们引入了参数的概念,这样可以使用函数的功能更加强大灵活:
# 案例1 def cal_sum(temp): # temp就是引入的函数形式参数 ret = 0 for i in range(1, temp + 1): ret += i print(ret) cal_sum(100) # 每次调用可以根据需要传入需要的值,这个具体的值成为实际参数简称实参。 cal_sum(101) # 案例2 def add(): x = 10 y = 20 print(x + y) def add(x, y): # 声明的参数称之为形式参数,简称形参 print(x + y) # 调用add函数 # 将调用过程中传入的值称之为实际参数,简称实参 add(5, 6) # 将5赋值给x,将6赋值给了y ,函数体将x+y,即5+6计算出来,打印 # 调用add函数 add(10, 5) # 将10赋值给x,将6赋值给了5 ,函数体将x+y,即10+5计算出来,打印
在函数的定义阶段 括号内写的变量名,叫做该函数的形式参数,简称形参。在函数的调用阶段,括号内实际传入的值,叫做实际参数,简称实参。该例中,temp就是的函数形式参数,而每次调用根据需要传入的值,比如100,101都是实参。
形参就相当于变量名,而实参就相当于变量的值,函数调用传参的过程 就是给形参变量名赋值的过程。
函数参数只有在函数调用阶段有效,函数运行结束,参数作为垃圾释放。
7.3.2、位置参数
位置参数,又是也称为必备参数,指的是必须按照正确的顺序将实际的参数传到函数中,换句话说,调用函数时传入实际参数的数量和位置必须和定义函数时保持一直。
# 例1 def add(x,y): # x,y是形参,用来接收实参 print(x+y) add(2,3) # 2,3 是实际参数,分别传递给形参x,y # 例2 def add(x,y,z): print(x+y) add(2,3) # 缺少一个实际参数传递给z # 例3 def add(x,y): print(x+y) add(2,3,4) # 缺少一个形式参数接收给z
7.3.3、默认参数
Python 允许为参数设置默认值,即在定义函数时,直接给形式参数指定一个默认值。这样的话,即便调用函数时没有给拥有默认值的形参传递参数,该参数可以直接使用定义函数时设置的默认值。
# 默认参数,注意事项:默认参数一定要放在位置参数的后面
def print_stu_info(name,age,gender="male"): print("学员姓名:", name) print("学员年龄:", age) print("学员性别:", gender) print_stu_info("张三", 23)
当定义一个有默认值参数的函数时,有默认值的参数必须位于所有没默认值参数的后面,否则报错!
7.3.4、关键字参数
关键字参数可以避免牢记参数位置的麻烦,令函数的调用和参数传递更加灵活方便。关键字参数是指使用形式参数的名字来确定输入的参数值。通过此方式指定函数实参时,不再需要与形参的位置完全一致,只要将参数名写正确即可。
# 关键字参数一定放在位置参数后面
def print_stu_info(name, age, height, weight, job): print("学员姓名:", name) print("学员年龄:", age) print("学员身高:", height) print("学员体重:", weight) print("学员工作:", job) print_stu_info("张三", 23, "180cm", "80kg", "销售") print_stu_info(name="张三", height="180cm", weight="90kg", job="销售", age=23) print_stu_info("张三", height="180cm", weight="90kg", job="销售", age=23)
使用位置参数和关键字参数混合传参的方式。但需要注意,混合传参时关键字参数必须位于所有的位置参数之后。
7.3.5、不定长参数
在函数定义中使用*args
和**kwargs
传递可变长参数。*args
用作传递非命名键值可变长参数列表(位置参数);**kwargs
用作传递键值可变长参数列表。*args
的参数会以元组(tuple)的形式导入,存放所有未命名的变量参数。**kwargs
的参数会以字典的形式导入。
# *args :用来接收位置参数的 def add(*args): print(args) print(type(args)) ret = 0 for i in args: ret += i print(ret) add(12, 23, 45) # **kwargs:用来接收关键字参数的 def print_stu_info(**kwargs, ): print(kwargs) print_stu_info(name="张三", height=190)
同时使用*args和**kwargs:
def print_stu_info(name, age=18, *args, **kwargs): print(name, age) print(args) print(kwargs) print_stu_info("hao", 20, "china", "Shenzhen", heigh="188cm", weight="60kg")
注意点:
1、参数
arg
、*args
、**kwargs
三个参数的位置必须是一定的。必须是(arg,*args,**kwargs)
这个顺序,否则程序会报错。2、不定长参数的长度可以为零。
3、
args
和kwargs
其实只是编程人员约定的变量名字,args
是 arguments 的缩写,表示位置参数;kwargs
是 keyword arguments 的缩写,表示关键字参数。
7.4、函数返回值
到目前为止,我们创建的函数都只是对传入的数据进行了处理,处理完了就结束。但实际上,在更多场景中,我们还需函数将处理的结果反馈回来。通过关键字return语句可以返回任意类型的数值。
# 声明函数 def 函数名(形参,...): 函数体 return 值 # 函数返回值 # 如果没有写return,默认返回None
ret = 函数名(实参) # ret 用来接受函数的返回值
7.4.1、基本使用
def add(x, y): return x + y # return是函数的终止语句 ret = add(2, 3) print(ret)
7.4.2、默认返回值
在Python中,以一个特殊的常量None (N必须大写)。和False不同,它不表示0,也不表示空字符串,而是表示没有值,也就是空值,None是NoneType数据类型的唯一值(其他编程语言可能称这个值为null、nil或undefined),也就是说,我们不能在创建其他NoneType类型的变量,但是可以将None赋值给任何变量。
Python一个函数中如果没有return语句或者return后面没有具体值,都默认返回None,比如print()函数与就没有返回
7.4.3、返回多个值
return可以返回多个值,Python其实会将多个值放在一个元组返回
def login(user, pwd): flag = False if user == "hao" and pwd == 123: flag = True return flag, user # ret = login("hao",123) flag, user = login("hao", 123) if flag: print("{}登录成功!".format(user)) else: print("用户名或者密码错误!")
7.5、函数嵌套
def foo(): def bar(): print("bar功能") print("foo功能") foo()
7.6、作用域
所谓作用域(Scope),就是变量的有效范围,就是变量可以在哪个范围以内使用。有些变量可以在整段代码的任意位置使用,有些变量只能在函数内部使用。
LEGB含义解释
字母 | 英语 | 释义 | 简称 | 作用空间 |
---|---|---|---|---|
L | Local(function) | 当前函数内的作用域 | 局部作用域 | 局部 |
E | Enclosing Functions Locals | 外部嵌套函数的作用域 | 嵌套作用域 | 局部 |
G | Global(module) | 函数外部所在的命名空间 | 全局作用域 | 全局 |
B | Built In(python) | Python内置模块的命名空间 | 内建作用域 | 内置 |
# 案例1 def foo(): x = 10 foo() print(x) # 案例2 x = 100 def foo(): x = 10 foo() print(x) # 案例3 x = 100 def foo(): # 全局声明 global x x = 10 print(x) foo() print(x) # 案例4 x = 100 def foo(): print(x) foo() # 案例5 x = 100 def foo(): x = 12 def bar(): x = 1 print(x) bar() foo()
7.7、匿名函数
lambda表达式,又称匿名函数,常用来表示内部仅包含1行表达式的函数。如果一个函数的函数体仅有一行表达式,则该函数就可以用lambda表达式代替。
lambda表达式的语法格式如下:
# name = lambda [list] : 表达式
其中定义lambda表达式,必须使用lambda关键字;[list]最为可选参数,等同于函数是指定的参数列表;value为表达式的名称。
def add(x, y): return x + y print(add(2, 3)) (lambda x, y: x + y)(2, 3)
可以这样理解lambda表达式,其就是简单函数(函数体仅是单行的表达式)的简写版本。相比函数,lambda表达式具体有一下2个优势:
- 对于单行函数,使用lambda表达式可以省去定义函数的过程,让代码更加简洁:
- 对于不需要多次复用的函数,使用lambda表达式在用完之后理解释放,提高程序执行的性能。
7.8、高阶函数
一个高阶函数应该具备下面至少一个特点:
- 将一个或者多个函数作为形参
- 返回一个函数作为其结果
# 一切皆数据,函数亦是变量 def foo(): print("bar") bar = 10 foo()
import time def foo(): print("foo功能") time.sleep(2) def bar(): print("bar功能") time.sleep(2) def timer(func): start = time.time() # foo() func() end = time.time() print("时耗", end - start) timer(foo) timer(bar)
def foo(): def bar(): print("bar功能!") return bar func = foo() func()
7.8.2、常见函数
内置函数
bool() bin() # 十进制转二进制 oct() # 十进制转八进制 hex() # 十进制转十六进制 sum() # 求和 max() #最大值 min() # 最小值 abs() # 绝对值 pow() # 指数 divmod() # 求商和余数 round() # 小数点后N为(四舍五入) chr() # 根据码点(十进制)获取对应字符 ord() # 获取字符对应的Unicode码点(十进制) list() # 列表 dict() # 字典 set() # 集合 len() # 计算一个值的长度 any() # 是否存在True all() # 是否全部为True zip() # 用于将可迭代的对象作为参数,将对象中对应的元素打包成一个一个元祖,然后返回有这些元祖组成的对象,这样做的好处就是节约了不少的内存,我们可以使用list()转换开输出的列表。如果可以各个迭代的元素个数不一致,则返回列表长度域最短的对象相同。利用* 号操作符,可以将元祖解压为列表 sorted() # 排序 # ----------------------------------------------- sorted函数 ----------------------------------------------- stu_dict = {1001: {"name": "yuan", "score": {"chinese": 100, "math": 90, "english": 80, "average": 90}}, 1002: {"name": "alvin", "score": {"chinese": 100, "math": 100, "english": 100, "average": 100}}, 1003: {"name": "rain", "score": {"chinese": 80, "math": 70, "english": 60, "average": 60}} } # 按着平均成绩排序 stu_list = [v for k, v in stu_dict.items()] ret = sorted(stu_list, key=lambda stu: stu["score"]["average"], reverse=True) print(ret) # ----------------------------------------------- zip函数 ----------------------------------------------- # 可以将多个序列(列表、元组、字典、集合、字符串以及 range() 区间构成的列表)“压缩”成一个 zip 对象。 # 所谓“压缩”,其实就是将这些序列中对应位置的元素重新组合,生成一个个新的元组。 my_list = [1, 2, 3] my_tuple = [4, 5, 6] print(list(zip(my_list, my_tuple)))
常见高阶函数
# filter函数:对sequence的中的item依次执行function(item),将执行结果为True的item组成一个List/String/Tuple(取决于sequence的类型) def foo(x): if x % 2 == 0: return True l = (1, 2, 3, 4) print(list(filter(foo, l))) print(list(filter(lambda x: x % 2 == 0, l))) # map函数,对sequence中的item依次执行function(item),将function(item)执行结果(返回值)组成一个List返回 def bar(x): return x * x l = [1, 2, 3, 4] print(list(map(bar, l))) print(list(map(lambda x: x * x, l))) # reduce函数 # 函数将一个数据集合(列表,元祖等)中的所有数据进行下列操作: # 用传给reduce中的函数function(有两根传参数)先对集合汇总的第1、2个元素进行操作 # 得到的结果在于第三个数据用function 函数运算,最后得到一个结果,逐步迭代 from functools import reduce def bar(x, y): return x * y l = [1, 2, 3, 4] print(reduce(bar, l)) print(reduce(lambda x, y: x * y, l))
7.9、闭包
首先看一下维基上对闭包的解释:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。
简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包。
闭包需要满足以下三个条件:
1、必须是一个嵌套函数
2、必须返回嵌套函数
3、嵌套函数必须引用外部非全局的局部自由变量
def foo(): x = 10 def inner(): print(x) print("bar功能") return inner func = foo() func() def foo(x): def inner(): print(x) print("bar功能!") return inner func = foo(12) func()
价值:能够动态灵活的创建以及传递函数,提现出函数编程的特点,所有在一些场合,我们就多了一种编码方式的选择,适当的使用闭包可以简洁是的我们的代码简洁高效。
7.10、装饰器
装饰器
- 实现原理:基于@语法和函数闭包,将原函数封装在闭包中,然后将函数赋值为一个新的函数(内层函数),执行函数时再在内层函数中执行闭包中的原函数。
- 实现效果:可以在不改变原函数内部代码和调用方式的前提下,实现在函数执行扩展功能。
- 适应场景:多个函数系统统一在执行前后自定义一些功能。
import time def foo(): print("foo功能") time.sleep(2) def bar(): print("bar功能") time.sleep(3) def timer(target_func): def wrapper(): start = time.time() target_func() end = time.time() print("时耗", end - start) return wrapper foo = timer(foo) foo() bar = timer(bar) bar()
7.11、迭代器
(1)可迭代对象和迭代器
在具体学习迭代器之前,先了解一个概念:可迭代对象(Iterable
)。之前在数据类型中介绍的容器对象(列表,元组,字典,集合等)都是可迭代对象;从语法形式上讲,能调用__iter__
方法的数据对象就是可迭代对象:
>>> [1,2,3].__iter__() <listiterator object at 0x10221b150> >>> {'name':'alvin'}.__iter__() <dictionary-keyiterator object at 0x1022180a8> >>> {7,8,9}.__iter__() <setiterator object at 0x1021ff9b0>
obj.__iter__()
方法调用后返回的就是一个迭代器对象(Iterator)。迭代器对象的特性就是能够调用__next__
方法依次计算出迭代器中的下一个值。基于此就可以实现无论是否数据为序列对象,都可以通过迭代取值的方式完成查询功能。
>>> s={1,2,3} >>> i=s.__iter__() # 返回可迭代对象s的迭代器对象i >>> i.__next__() # 从第一个元素开始,i通过__next__方法就可以得到可迭代对象s的下一个值。 1 >>> i.__next__() 2 >>> i.__next__() 3 >>> i.__next__() #迭代结束,没有下一个值时调用__next__()抛出StopIteration的异常 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
1、针对一个没有索引的可迭代数据类型,我们执行s.__iter__()
方法便得到一个迭代器,每执行一次i.__next__()
就获取下一个值,待所有值全部取出后,就会抛出异常StopIteration
,不过这并不代表错误发生,而是一种迭代完成的标志。需要强调的是:此处我们迭代取值的过程,不再是通过索引而是通过__next__
方法。
2、可以用iter(s)
取代s.__iter__()
,其实iter(s)
本质就是在调用s.__iter__()
,这与len(s)
会调用s.__len__()
是一个原理,同理,可以用next(i)取代i.__next__()
。obj.__iter__()
方法的调用后返回的就是一个迭代器对象
class ListIterator(object): def __init__(self, l): self.l = l self.index = 0 def __iter__(self): return self def __next__(self): try: ret = self.l[self.index] self.index += 1 return ret except IndexError: raise StopIteration # l1 = [1, 2, 3] # l2 = [2, 3, 4] # print(id(l1.__iter__())) # print(id(l2.__iter__())) # print(id({"name": "yuan"}.__iter__())) iterator = ListIterator([100,101,102]) print(iterator.__next__()) print(iterator.__next__()) print(iterator.__next__()) for i in iterator: print(i)
你不了解的for循环
之前的学习只知道for循环是用来遍历某个数据对象的。但for循环内部到底是怎么工作的,关键字in后面可以放什么数据类型呢?让我们带着这些疑问一起去解析for循环的实现机制。
# for循环的形式: for val in obj: print(val)
解析:关键字in后面数据对象必须是可迭代对象。for 循环首先会调用可迭代对象内的__iter__
方法返回一个迭代器,然后再调用这个迭代器的next方法将取到的值赋给val,即关键字for后的变量。循环一次,调用一次next方法,直到捕捉StopIteration
异常,结束迭代。解析:关键字in后面数据对象必须是可迭代对象。for 循环首先会调用可迭代对象内的__iter__
方法返回一个迭代器,然后再调用这个迭代器的next方法将取到的值赋给val,即关键字for后的变量。循环一次,调用一次next方法,直到捕捉StopIteration
异常,结束迭代。
for i in iterator: print(i) l = [11, 22, 33] for i in l: # 调用iter方法返回一个关于[11,22,33]的迭代器 print(i) # 迭代器调用next方法返回的值复制给i 即i=next(iter(l)) it = [1, 2, 3, 4, 5].__iter__() for j in it: print(j) for j in it: print(j)
3.自定义迭代器
class Fib(object): def __init__(self, max): self.max = max self.n, self.a, self.b = 0, 0, 1 def __iter__(self): return self def __next__(self): if self.n < self.max: r = self.b self.a, self.b = self.b, self.a + self.b # 这次结果作为下次的初始值 self.n = self.n + 1 return r raise StopIteration() for i in Fib(10): print(i)
迭代器协议要求迭代对象具有
__iter__()
和__next__()
两个方法,__next__
之前讲过,是用于计算下一个值的,而__iter__
则是返回迭代器本身,目的是使for循环可以遍历迭代器对象,for循环的本质是调用被迭代对象内部的__iter__
方法将其变成一个迭代器然后进行迭代取值的操作,如果对象没有__iter__
方法则会报错。所以可以说,迭代器对象都是可迭代对象就是因为其内部定义了__iter__
方法。
7.12、生成器
生成器算的是Python语言中最吸引人的特性之一,生成器是一种特殊的迭代器,不过这种迭代器更加优雅。他不需要再像上面一样在类中定义__iter__()和__next__()方法了,只需要在函数中声明一个yiled关键字。所以生成器是一种特殊的迭代器(反之不成立),因此任何生成器也是一种懒加载的模式生成值。用生成器来实现斐波那契数列的例子是:
def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1 for i in fib(10): print(i)
1.生成器对象
简单说,生成器就是使用了yield关键字的函数:
def countdown(n): print('countdown start') while n > 0: yield n n -= 1 print('Done') print(countdown) # <function countdown at 0x7fa1f00e6040>
# 执行生成器函数时,函数体默认不会被执行,返回一个生成器对象。 gen = countdown(5) print(gen) # <generator object countdown at 0x7fa1f00f3f90> # 生成器对象也是迭代器对象,一定拥有iterm和next,由yield关键字在函数调用时封装好的,不用在自己定义 # print(gen.__iter__()) # print(gen.__iter__()) # print(gen.__iter__()) # print(gen.__iter__()) for i in gen: print(i)
解析:生成器函数调用时只会返回一个生成器对象。只有当生成器对象调用
__next__
方法时才会触发函数体代码执行,直到遇到关键字yield停止,将yield后的值作为返回值返回,所以,yield类似于return的功能,但不同于return的是,return返回,函数结束;而yield将函数的状态挂起,等待生成器对象再次调用__next__
方法时,函数从挂起的位置后的第一条语句继续运行直到再遇见yield并返回其后的值;如果不断调用__next__
方法,最后一次进入函数体,待执行代码不再有yield此时报出迭代异常的错误。另外,对比迭代器,生成器对象多几个方法:
(1)一个内置的close方法用来关闭自己
(2)一个内置的send方法,进入生成器,类似于next,但是多一个传值给yield变量的功能。
yield的功能总结:
(1)封装
iter
和next
方法 (2)执行函数时遇到yield返回其后的值,不同于return,yiled
可以返回多次值 (3)挂起函数的状态,等待下一次调用next方法时找到对应的暂停位置继续执行。
(2)生成器表达式
创建一个生成器对象有两种方式,一是通过在函数中创建yield关键字来实现。另一种就是生成器表达式,这是一种类似于数据类型中学过的列表生成式的语法格式,只是将[]换成(),即:
(expression for item in iterable if condition)
不同于列表生成式最后返回一个列表结果,生成器表达式顾名思义会返回一个生成器对象,比如:
>>> [x*x for x in range(4)] # 列表推导式 [0, 1, 4, 9] >>> gen=(x*x for x in range(4)) >>> gen <generator object <genexpr> at 0x101be0ba0>
当需要用到其中的值时,再通过调用next方法或者for循环将值一个个地计算出来:
>>> next(gen) 0 >>> next(gen) 1 >>> next(gen) 4 >>> next(gen) 9 >>> next(gen) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration #------------for 循环------------------ >>> gen=(x*x for x in range(4)) >>> for i in gen: ... print(i) ... 0 1 4 9
(3)可迭代对象、迭代器、生成器关系
def add(s, x): return s + x def gen(): for i in range(4): yield i base = gen() for n in [1, 10]: base = (add(i, n) for i in base) print(list(base))
配合debug模式运行,其中gi_frame, gi_code, gi_running.gi_code是生成器的代码,根据这个对象我们可以获取到生成器的函数名、文件名。gi_frame相当于栈帧,记录了生成器执行的状态,根据这个对象我们可以获取到执行到的行号信息。