python学习——函数进阶
首先来看下面这个函数。
1 def func(x,y): 2 bigger = x if x > y else y 3 return bigger 4 ret = func(10,20) 5 print(ret) 6 7 #运行结果 : 20
在上面的函数中我们把较大值通过return这个关键字返回回来了,如果我不返回而是直接打印可不可以?如下:
def func(x,y): bigger = x if x > y else y func(10,20) print(bigger) #运行结果 : NameError: name 'bigger' is not defined
此时它会说,bigger没有定义,这是为什么,在函数中我明明定义了bigger就是较大的那个数,那问题出在哪儿呢?
在这里我们首先回忆一下python代码运行的时候遇到函数是怎么做的。从python解释器开始执行之后,就在内存中开辟了一个空间,每当遇到一个变量的时候,就把变量名和值之间的对应关系记录下来。但是当遇到函数定义的时候解释器只是象征性的将函数名读入内存,表示知道这个函数的存在了,至于函数内部的变量和逻辑解释器根本不关心。等执行到函数调用的时候,python解释器会再开辟一块内存来存储这个函数里的内容,这个时候,才关注函数里面有哪些变量,而函数中的变量会存储在新开辟出来的内存中。函数中的变量只能在函数的内部使用,并且会随着函数执行完毕,这块内存中的所有内容也会被解释器释放了。
我们给这个“存放名字与值的关系”的空间起了一个名字——叫做命名空间
代码在运行的时候,创建的存储“变量名与值的关系”的空间叫做全局命名空间,在函数的运行中开辟的临时的空间叫做局部命名空间
一、命名空间和作用域
首先看一下这张图:
这是python之禅中说的,命名空间非常好!在python中命名空间有以下几种:
内置命名空间
全局命名空间
局部命名空间
内置命名空间中存放了python解释器为我们提供的名字:input,print,str,list,tuple...它们都是我们熟悉的,拿过来就可以用的方法。
三种命名空间之间的加载与取值顺序:
加载顺序:内置命名空间 (程序运行前加载 ) -> 全局命名空间 ( 程序运行中:从上到下加载 ) -> 局部命名空间 ( 程序运行中:调用时才加载 )
取值:
在局部调用:局部命名空间 -> 全局命名空间 -> 内置命名空间
1 a = 1 2 b = 2 3 def func(): 4 print(a) 5 print(b) 6 func() 7 print(10) 8 9 #运行结果 :1,2,10
所以存在这种情况:当调用函数时,函数被执行,但是函数内部(局部)并没有a,b这两个值,只能到函数外面(全局)来找,找到了,就打印。而之后打印的10在全局中就直接找到了。
在全局调用:全局命名空间 -> 内置命名空间
1 a = 1 2 b = 2 3 def func(a,b): 4 print(a) 5 print(b) 6 7 func(10,20) 8 print(a,b) 9 10 # 运行结果:10,20,1 2
此时对a,b传值了,所以首先打印出来,在全局再次打印a,b的时候,函数内部的a,b在函数执行完毕之后就被释放了,所以只能打印全局a,b的值;还有即使a,b没被释放,在全局也不能打印局部变量的值!
1 print(max(1,2,3,3)) 2 3 #结果:3
所以三种命名空间有以下这种关系:
所以他们的关系是这样的,内置命名空间在解释器一打开就被加载到内存中了,在定义函数之前的所有变量都是全局命名空间中的变量,而在函数内部的所有变量都是局部命名空间中的变量,当然只有函数被调用时才被加载到内存中,但随着函数执行完毕就被自动释放了。
二、函数的嵌套和作用链域
函数的嵌套调用
1 def func1(a,b): 2 return a if a > b else b 3 4 def func2(x,y,z): 5 ret1 = func1(x,y) 6 ret2 = func1(ret1,z) 7 return ret2 8 9 ret = func2(1,2,3) 10 print(ret) 11 12 #运行结果:3
函数的嵌套定义
1 # 函数的嵌套定义 一 2 3 def func1(): 4 print('in func1 now') 5 def func2(): 6 print('in func2 now') 7 func2() 8 9 func1() 10 11 # 函数的嵌套定义 二 12 13 def func1(): 14 def func2(): 15 def func3(): 16 print('in func3 now') 17 func3() 18 print('in func2 now') 19 func2() 20 print('in func1 now') 21 func1()
函数的作用链域
1 # 函数的作用链域 一 2 def func1(): 3 a = 1 4 def func2(): 5 print(a) 6 func2() 7 func1() 8 9 # 运行结果:1 10 11 # 函数的作用链域 二 12 def func1(): 13 a = 100 14 def func2(): 15 def func3(): 16 print(a) 17 func3() 18 func2() 19 20 func1() 21 22 # 运行结果:100 23 24 # 函数的作用链域 三 25 def func1(): 26 a = 1000 27 def func2(): 28 a = 10000 29 def func3(): 30 print('a in func1 ',a) 31 func3() 32 func2() 33 34 func1() 35 36 # 运行结果:a in func1 10000
nonlocal 关键字
1.外部必须有这个变量
2.在内部函数声明nonlocal变量之前不能再出现同名变量
3.内部修改这个变量如果想在外部有这个变量的第一层函数中生效
1 def func1(): 2 a = 1 3 def func2(): 4 nonlocal a 5 a = 2 6 func2() 7 print(a) 8 func1() 9 10 # 运行结果:2
三、函数名的本质
函数名本质上就是一个函数的内存地址(函数即变量)
1.可以被引用
1 def func(): 2 print('in func now ') 3 4 ret = func 5 print(ret) 6 7 # 运行结果:<function func at 0x0000023C04CDC730>
当打印函数名的时候返回的是一个内存地址。
2.可以被当作容器类型的元素
1 def func1(): 2 print('func1') 3 4 def func2(): 5 print('func2') 6 7 def func3(): 8 print('func3') 9 10 l = [func1,func2,func3] 11 d = {'func1':func1,'func2':func2,'func3':func3} 12 #调用 13 l[0] 14 print(l[0]) 15 l[0]() 16 print(d['func2']) 17 d['func2']() 18 19 #运行结果:<function func1 at 0x000001F345C0C7B8> func1 <function func2 at 0x000001F345C0C840> func2
当我们不调用时(不在后面加上“()”),返回函数名所在的内存地址,加上之后返回函数值。
3.可以当作函数的参数和返回值(就是把函数名当作普通变量来用)
第一类对象(first-class object)指 1.可在运行期创建 2.可用作函数参数或返回值 3.可存入变量的实体。
四、闭包函数
1、首先说什么是闭包
1 def func1(): 2 name = 'liulonghai' 3 def func2(): 4 print(name)
2、闭包函数
内部函数包含对外部作用域而非全局作用域名字的引用,该内部函数称为闭包函数 #函数内部定义的函数称为内部函数。
判断一个函数是否是闭包可以这样写
1 def func1(): 2 name = 'liulonghai' 3 def func2(): 4 print(name) 5 print(func2.__closure__) 6 print(func1.__closure__) 7 func2() 8 9 func1() 10 11 #运行结果:liulonghai 12 # (<cell at 0x000001B30E5F0108: function object at 0x000001B31163D7B8>, 13 # <cell at 0x000001B3103A7378: str object at 0x000001B3103A0370>) 14 # None
可以通过(__closure__)这个双下方法来查看一个函数名是不是闭包,当打印出"(<cell at 0x000001B30E5F0108: function object at 0x000001B31163D7B8>, <cell at 0x000001B3103A7378: str object at 0x000001B3103A0370>)" 这样就表面此函数是一个闭包,其实就是‘cell’ ,如果不是,则返回None
由于有了作用域的关系,我们就不能拿到函数内部的变量和函数了。如果我们就是想拿怎么办呢?返回?我们都知道函数内的变量我们要想在函数外部用,可以直接返回这个变量,那么如果我们想在函数外部调用函数内部的函数呢?是不是直接就把这个函数的名字返回就好了?
这才是闭包函数最常用的用法
1 def func(): 2 name = 'liulonghai' 3 def inner(): 4 print(name) 5 6 return inner 7 8 f = func() 9 f() 10 11 #运行结果:liulonghai
闭包函数的嵌套
1 def wrapper(): 2 name = 'liulonghai' 3 def outter(): 4 money = 1000000 5 def inner(): 6 print(name,money) 7 return inner 8 return outter 9 10 w = wrapper() 11 o = w() 12 o() 13 14 #运行结果:liulonghai 1000000
闭包函数获取网页
1 from urllib.request import urlopen 2 3 def index(): 4 url = "http://www.baidu.com" 5 def get(): 6 return urlopen(url).read() 7 return get 8 9 baidu = index() 10 content = baidu() 11 print(content)
五、迭代器
1.迭代
在说迭代器之前,我们来先了解以下python中的for循环,为什么当我们对一个对象进行for循环时,为什么能够取到那个对象里面的所有值。如下:
1 lst = [i for i in range(10)] 2 print(lst) 3 for i in lst: 4 print(i,end=' ') 5 6 #输出:0 1 2 3 4 5 6 7 8 9
再来看一下下面这个代码,为什么会这样。
1 for i in 1234: 2 print(i) 3 4 #输出:TypeError: 'int' object is not iterable
当我们这样写的时候,解释器会报错,来看一下这个错误TypeError:'int' object is not iterable "类型错误:int对象是不可迭代的",其实iterable就是for循环的关键,在python中,只要某个对象是可迭代的,那就能进行for循环。👇
1 from collections import Iterable 2 3 lst = [1,2,3,4,5] 4 tup = (1,2,3,4,5) 5 dct = {'a':1,'b':2,'c':3,'d':4} 6 st = {1,2,3,4,5} 7 sr = 'abcdef' 8 9 print(isinstance(lst,Iterable)) 10 print(isinstance(tup,Iterable)) 11 print(isinstance(dct,Iterable)) 12 print(isinstance(st,Iterable)) 13 print(isinstance(sr,Iterable)) 14 15 # 输出: 16 True 17 True 18 True 19 True 20 True
在上面我们知道了列表,元组,字典,集合,字符串它们都是一个可迭代的(对象),所有我们可以对它们进行for循环,对,就是这样。所有可迭代可以认为在某个容器类型中一个一个地进行取值。这就是迭代。
2.可迭代协议
我们现在是从结果分析原因,能被for循环的就是“可迭代的”,但是如果正着想,for怎么知道谁是可迭代的呢?假如我们自己写了一个数据类型,希望这个数据类型里的东西也可以使用for被一个一个的取出来,那我们就必须满足for的要求。这个要求就叫做“协议”。
可以被迭代要满足的要求就叫做可迭代协议。可迭代协议的定义非常简单,就是内部实现了__iter__方法。接下来我们就来验证一下:
1 print(set(dir(list))) 2 print(set(dir(tuple))) 3 print(set(dir(dict))) 4 print(set(dir(set))) 5 print(set(dir(str))) 6 7 print(set(dir(list))&set(dir(tuple))&set(dir(dict))&set(dir(str))&set(dir(set))) 8 9 # 输出结果: 10 {'__hash__', '__setattr__', '__reduce__', '__class__', '__len__', '__subclasshook__', '__sizeof__', '__getattribute__', '__iter__', '__doc__', '__init_subclass__', '__ge__', '__eq__', '__contains__', '__repr__', '__delattr__', '__ne__', '__dir__', '__new__', '__reduce_ex__', '__le__', '__format__', '__str__', '__lt__', '__gt__', '__init__'}
上面我把列表,元组,字典,集合和字符串对象中所有的方法全部变成一个集合来求了他们的交集,发现都有__iter__方法。接下来看一下__iter__究竟做了什么事来让for循环能一个一个的取值呢?👇
1 lst = [1,2,3,4,5] 2 print(lst.__iter__()) 3 print(lst.__iter__()) 4 print(lst.__iter__()) 5 print(lst.__iter__()) 6 print(lst.__iter__()) 7 8 # 输出: 9 <list_iterator object at 0x000001E0CEB12208> 10 <list_iterator object at 0x000001E0CEA66588> 11 <list_iterator object at 0x000001E0CEB12208> 12 <list_iterator object at 0x000001E0CEA66588> 13 <list_iterator object at 0x000001E0CEB12208>
上面我打印了5次,如果你仔细看你会发现每次打印之前都是list_iterator object,而且每一个地址都不一样,所以你应该知道,我的列表里面本来就只有5个元素,所以每个地址代表的就是每个元素的地址,而iterator就是迭代器的意思。
3.迭代器协议
在上面我们已经知道可迭代协议了,接下来再看一下什么是迭代器协议。👇
1 ''' 2 dir([1,2].__iter__())是列表迭代器中实现的所有方法,dir([1,2])是列表中实现的所有方法,都是以列表的形式返回给我们的,为了看的更清楚,我们分别把他们转换成集合, 3 然后取差集。 4 ''' 5 6 print(set(dir([1,2].__iter__()))-set(dir([1,2]))) 7 8 结果: 9 {'__length_hint__', '__next__', '__setstate__'}
我们看到在列表迭代器中多了三个方法,那么这三个方法都分别做了什么事呢?
1 iter_l = [1,2,3,4,5,6].__iter__() 2 #获取迭代器中元素的长度 3 print(iter_l.__length_hint__()) 4 #根据索引值指定从哪里开始迭代 5 print('*',iter_l.__setstate__(4)) 6 #一个一个的取值 7 print('**',iter_l.__next__()) 8 print('***',iter_l.__next__())
这三个方法中,能让我们一个一个取值的神奇方法是谁?没错!就是__next__(),在for循环中,就是在内部调用了__next__()方法才能取到一个一个的值。那接下来我们就用迭代器的__next__()方法来写一个不依赖for的遍历。
1 lst = [1,2,3,4] 2 3 lst_iter = lst.__iter__() 4 item = lst_iter.__next__() 5 print(item) 6 7 item = lst_iter.__next__() 8 print(item) 9 10 item = lst_iter.__next__() 11 print(item) 12 13 item = lst_iter.__next__() 14 print(item) 15 16 item = lst_iter.__next__() 17 print(item)
这是一段会报错的代码,如果我们一直取next取到迭代器里已经没有元素了,就会抛出一个异常StopIteration,告诉我们,列表中已经没有有效的元素了。这个时候,我们就要使用异常处理机制来把这个异常处理掉。
1 lst = [1,2,3,4] 2 lst_iter = lst.__iter__() 3 while True: 4 try: 5 item = lst_iter.__next__() 6 print(item) 7 except StopIteration: 8 break 9 10 # 输出: 11 1 12 2 13 3 14 4
那现在我们就使用while循环实现了原本for循环做的事情,我们是从谁那儿获取一个一个的值?是不是就是lst_iter?好了,这个lst_iter就是一个迭代器。
迭代器遵循迭代器协议:必须拥有__iter__方法和__next__方法。
最后再来说一下range(),请看 👇
1 from collections.abc import Iterator 2 print('__next__' in dir(range(12))) #查看'__next__'是不是在range()方法执行之后内部是否有__next__ 3 4 print('__iter__' in dir(range(12))) #查看'__next__'是不是在range()方法执行之后内部是否有__next__ 5 6 print(isinstance(range(100000000),Iterator)) #验证range执行之后得到的结果不是一个迭代器 7 8 # 输出: 9 False 10 True 11 False
range()虽然不是一个迭代器,但它是一个可迭代对象,我们同样能在里面进行for循环 👇
1 for i in range(10): 2 print(i,end=' ') 3 4 # 输出:0 1 2 3 4 5 6 7 8 9
4.为什么要用for循环
关于循环我们已经知道了,为什么推荐说当能使用for循环的时候就优先考虑用for循环而不用while循环呢?或者说不是可以用下标取值吗?怎么就一定要用循环呢?没错,序列类型字符串,列表,元组都有下标,你用上述的方式访问,perfect!但是你可曾想过非序列类型像字典,集合,文件对象的感受,所以,for循环就是基于迭代器协议提供了一个统一的可以遍历所有对象的方法,即在遍历之前,先调用对象的__iter__方法将其转换成一个迭代器,然后使用迭代器协议去实现循环访问,这样所有的对象就都可以通过for循环来遍历了,而且你看到的效果也确实如此,这就是无所不能的for循环。
六、生成器
1.初识生成器
我们知道的迭代器有两种:一种是调用方法直接返回的,一种是可迭代对象通过执行__iter__()方法得到的,迭代器的好处是可以节省内存。如果在某些情况下,我们也需要节省内存,就只能自己写。我们自己写的这个能实现迭代器功能的东西就叫生成器。
Python中提供的生成器:
①.生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重它离开的地方继续执行
②.生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表
生成器Generator:
本质:迭代器 (所以自带了__iter__方法和__next__方法,不需要我们去实现 )
特点:惰性运算,开发者自定义
2.生成器函数
一个包含yield关键字的函数就是一个生成器函数。yield可以为我们从函数中返回值,但是yield又不同于return,return的执行意味着程序的结束,调用生成器函数不会得到返回的具体的值,而是得到一个可迭代的对象。每一次获取这个可迭代对象的值,就能推动函数的执行,获取新的返回值。直到函数执行结束。👇
1 import time 2 3 def genrator_func(): 4 a = 1 5 print('你打印给我看一下') 6 yield a 7 b = 2 8 print('你再打印给我看一下') 9 yield b 10 c = 3 11 print('有本事你再来一下') 12 yield c 13 14 g = genrator_func() 15 print('g是{},一个生成器函数的内存地址'.format(g)) 16 print(next(g)) 17 time.sleep(1) 18 print(next(g)) 19 time.sleep(1) 20 print(next(g)) 21 22 # 输出: 23 g是<generator object genrator_func at 0x000001F0414E2A20>,一个生成器函数的内存地址 24 你打印给我看一下 25 1 26 你再打印给我看一下 27 2 28 有本事你再来一下 29 3
生成器有什么好处呢?就是不会一下子在内存中生成太多数据。假如我想让工厂给学生做校服,生产2000000件衣服,我和工厂一说,工厂应该是先答应下来,然后再去生产,我可以一件一件的要,也可以根据学生一批一批的找工厂拿。而不能是一说要生产2000000件衣服,工厂就先去做生产2000000件衣服,等工厂费气八力的把衣服做好了,学生都毕业了。。。
1 def produce_uniform(): 2 """生产衣服""" 3 for i in range(2000000): 4 yield "生产了第{}件衣服".format(i+1) 5 6 products = produce_uniform() 7 print(products.__next__()) #要一件衣服 8 print(products.__next__()) #再要一件衣服 9 print(products.__next__()) #再要一件衣服 10 num = 0 11 for i in products: #要一批衣服,比如50件 12 print(i) 13 num += 1 14 if num == 50: 15 break 16 17 #到这里我们找工厂拿了53件衣服,我一共让我的生产函数(也就是produces生成器函数)生产2000000件衣服。 18 #剩下的还有很多衣服,我们可以一直拿,也可以放着等想要的时候再拿 19 20 # 输出: 21 生产了第1件衣服 22 生产了第2件衣服 23 生产了第3件衣服 24 生产了第4件衣服 25 生产了第5件衣服 26 生产了第6件衣服 27 生产了第7件衣服 28 生产了第8件衣服 29 生产了第9件衣服 30 生产了第10件衣服 31 ……
send方法
1 def generator_func(): 2 print(123) 3 content = yield 4 4 print('=======',content) 5 print(567) 6 yield 8 7 8 g = generator_func() 9 ret = g.__next__() 10 print('***',ret) 11 ret = g.send('hello') #send的效果和next一样 12 print('***',ret) 13 14 #send 获取下一个值的效果和next基本一致 15 #只是在获取下一个值的时候,给上一yield的位置传递一个数据 16 #使用send的注意事项 17 # 第一次使用生成器的时候 是用next获取下一个值 18 # 最后一个yield不能接受外部的值 19 20 # 输出: 21 123 22 *** 4 23 ======= hello 24 567 25 *** 8
1 def averager(): 2 total = 0.0 3 count = 0 4 average = None 5 while True: 6 term = yield average 7 total += term 8 count += 1 9 average = total/count 10 11 g_avg = averager() 12 next(g_avg) 13 print(g_avg.send(10)) 14 print(g_avg.send(30)) 15 print(g_avg.send(5)) 16 17 # 输出: 18 10.0 19 20.0 20 15.0
1 def init(func): #在调用被装饰生成器函数的时候首先用next激活生成器 2 def inner(*args,**kwargs): 3 g = func(*args,**kwargs) 4 next(g) 5 return g 6 return inner 7 8 @init 9 def averager(): 10 total = 0.0 11 count = 0 12 average = None 13 while True: 14 term = yield average 15 total += term 16 count += 1 17 average = total/count 18 19 g_avg = averager() 20 # next(g_avg) 在装饰器中执行了next方法 21 print(g_avg.send(10)) 22 print(g_avg.send(30)) 23 print(g_avg.send(5)) 24 25 # 输出: 26 10.0 27 20.0 28 15.0
yield from
1 def gen1(): 2 for c in 'AB': 3 yield c 4 for i in range(3): 5 yield i 6 7 print(list(gen1())) 8 9 def gen2(): 10 yield from 'AB' 11 yield from range(3) 12 13 print(list(gen2())) 14 15 # 输出: 16 ['A', 'B', 0, 1, 2] 17 ['A', 'B', 0, 1, 2]
3.生成器函数相关的面试题
1 def demo(): 2 for i in range(4): 3 yield i 4 5 g=demo() 6 7 g1=(i for i in g) 8 g2=(i for i in g1) 9 10 print(list(g1)) 11 print(list(g2)) 12 13 # 输出: 14 [0, 1, 2, 3] 15 []
也许你不看输出是什么你根本就不知道答案是什么,或者你可能会想到输出应该是 [0, 1, 2, 3]和[0, 1, 2, 3] (反正我就是这样的)。既然是这样,这下你应该知道了 生成器之所以能够节省内存空间就是因为他们都很懒,反正你不叫他干活他是死活不干的,OK?
1 def add(n,i): 2 return n+i 3 4 def test(): 5 for i in range(4): 6 yield i 7 8 g = test() 9 for n in [1,10]: 10 g=(add(n,i) for i in g) 11 12 print(list(g)) 13 14 # 输出: 15 [20, 21, 22, 23]
我还是上面那句话,你可能看不懂。。。
1 import os 2 3 def init(func): 4 def wrapper(*args,**kwargs): 5 g=func(*args,**kwargs) 6 next(g) 7 return g 8 return wrapper 9 10 @init 11 def list_files(target): 12 while 1: 13 dir_to_search=yield 14 for top_dir,dir,files in os.walk(dir_to_search): 15 for file in files: 16 target.send(os.path.join(top_dir,file)) 17 @init 18 def opener(target): 19 while 1: 20 file=yield 21 fn=open(file) 22 target.send((file,fn)) 23 @init 24 def cat(target): 25 while 1: 26 file,fn=yield 27 for line in fn: 28 target.send((file,line)) 29 30 @init 31 def grep(pattern,target): 32 while 1: 33 file,line=yield 34 if pattern in line: 35 target.send(file) 36 @init 37 def printer(): 38 while 1: 39 file=yield 40 if file: 41 print(file) 42 43 g=list_files(opener(cat(grep('python',printer())))) 44 45 g.send('/test1') 46 47 # 输出:
毫无输出。。。