生成器装饰器和异常
一. 生成器
更多详细的生成器内容请参考:python生成器详解
前面章节中,已经详细介绍了什么是迭代器。生成器本质上也是迭代器,不过它比较特殊。
以 list 容器为例,在使用该容器迭代一组数据时,必须事先将所有数据存储到容器中,才能开始迭代;而生成器却不同,它可以实现在迭代的同时生成元素。
也就是说,对于可以用某种算法推算得到的多个数据,生成器并不会一次性生成它们,而是什么时候需要,才什么时候生成。
不仅如此,生成器的创建方式也比迭代器简单很多,大体分为以下 2 步:
- 定义一个以 yield 关键字标识返回值的函数;
- 调用刚刚创建的函数,即可创建一个生成器。
程序示例:
def intNum():
print("开始执行")
for i in range(5):
yield i
print("继续执行")
num = intNum()
由此,我们就成功创建了一个 num 生成器对象。显然,和普通函数不同,intNum() 函数的返回值用的是 yield 关键字,而不是 return 关键字,此类函数又成为生成器函数。
和 return 相比,yield 除了可以返回相应的值,还有一个更重要的功能,即每当程序执行完该语句时,程序就会暂停执行。不仅如此,即便调用生成器函数,Python 解释器也不会执行函数中的代码,它只会返回一个生成器(对象)。
要想使生成器函数得以执行,或者想使执行完 yield 语句立即暂停的程序得以继续执行,有以下 2 种方式:
- 通过生成器(上面程序中的 num)调用 next() 内置函数或者 next() 方法;
- 通过 for 循环遍历生成器。
例如,在上面程序的基础上,添加如下语句:
#调用 next() 内置函数
print(next(num))
#调用 __next__() 方法
print(num.__next__())
#通过for循环遍历生成器
for i in num:
print(i)
运行结果为:
开始执行
0
继续执行
1
继续执行
2
继续执行
3
继续执行
4
继续执行
这里有必要给读者分析一个程序的执行流程:
-
首先,在创建有 num 生成器的前提下,通过其调用 next() 内置函数,会使 Python 解释器开始执行
intNum()
生成器函数中的代码,因此会输出“开始执行”,程序会一直执行到yield i
,而此时的i==0
,因此 Python 解释器输出“0”。由于受到 yield 的影响,程序会在此处暂停。 -
然后,我们使用 num 生成器调用
__next__()
方法,该方法的作用和 next() 函数完全相同(事实上,next() 函数的底层执行的也是__next__()
方法),它会是程序继续执行,即输出“继续执行”,程序又会执行到yield i
,此时 i==1,因此输出“1”,然后程序暂停。 -
最后,我们使用 for 循环遍历 num 生成器,之所以能这么做,是因为 for 循环底层会不断地调用 next() 函数,使暂停的程序继续执行,因此会输出后续的结果。
注意,在 Python 2.x 版本中不能使用 next() 方法,可以使用 next() 内置函数,另外生成器还有 next() 方法(即以 num.next() 的方式调用)
除此之外,还可以使用 list() 函数和 tuple() 函数,直接将生成器能生成的所有值存储成列表或者元组的形式。例如:
def intNum():
print("开始执行")
for i in range(5):
yield i
print("继续执行")
num = intNum()
print(list(num))
num = intNum()
print(tuple(num))
运行结果为:
开始执行
继续执行
继续执行
继续执行
继续执行
继续执行
[0, 1, 2, 3, 4]
开始执行
继续执行
继续执行
继续执行
继续执行
继续执行
(0, 1, 2, 3, 4)
通过输出结果可以判断出,list() 和 tuple() 底层实现和 for 循环的遍历过程是类似的。
相比迭代器,生成器最明显的优势就是节省内存空间,即它不会一次性生成所有的数据,而是什么时候需要,什么时候生成。
二. 装饰器
更多详细的装饰器内容请参考:python装饰器详解
装饰器引入
初期及问题诞生
假如现在在一个公司,有A B C三个业务部门,还有S一个基础服务部门,目前呢,S部门提供了两个函数,供其他部门调用,函数如下:
def f1():
print('f1 called')
def f2():
print('f2 called')
在初期,其他部门这样调用是没有问题的,随着公司业务的发展,现在S部门需要对函数调用假如权限验证,如果有权限的话,才能进行调用,否则调用失败。考虑一下,如果是我们,该怎么做呢?
方案集合
- 让调用方也就是ABC部门在调用的时候,先主动进行权限验证
- S部门在对外提供的函数中,首先进行权限认证,然后再进行真正的函数操作
问题
- 方案一,将本不该暴露给外层的权限认证,暴露在使用方面前,同时如果有多个部门呢,要每个部门每个人都要周知到,你还不缺定别人一定会这么做,不靠谱。。。
- 方案二,看似看行,可是当S部门对外提供更多的需要进行权限验证方法时,每个函数都要调用权限验证,同样也实在费劲,不利于代码的维护性和扩展性
那么,有没有一种方法能够遵循代码的开放闭合原则,来完美的解决此问题呢?
装饰器引入
答案肯定是有的,不然真的是弱爆了。先看代码
def w1(func):
def inner():
print('...验证权限...')
func()
return inner
@w1
def f1():
print('f1 called')
@w1
def f2():
print('f2 called')
f1()
f2()
输出结果为
...验证权限...
f1 called
...验证权限...
f2 called
可以通过代码及输出看到,在调用f1 f2 函数时,成功进行了权限验证,那么是怎么做到的呢?其实这里就使用到了装饰器,通过定义一个闭包函数w1,在我们调用函数上通过关键词@w1,这样就对f1 f2函数完成了装饰。
装饰器原理
首先,开看我们的装饰器函数w1,该函数接收一个参数func,其实就是接收一个方法名,w1内部又定义一个函数inner,在inner函数中增加权限校验,并在验证完权限后调用传进来的参数func,同时w1的返回值为内部函数inner,其实就是一个闭包函数。
然后,再来看一下,在f1上增加@w1,那这是什么意思呢?当python解释器执行到这句话的时候,会去调用w1函数,同时将被装饰的函数名作为参数传入(此时为f1),根据闭包一文分析,在执行w1函数的时候,此时直接把inner函数返回了,同时把它赋值给f1,此时的f1已经不是未加装饰时的f1了,而是指向了w1.inner函数地址。
接下来,在调用f1()的时候,其实调用的是w1.inner函数,那么此时就会先执行权限验证,然后再调用原来的f1(),该处的f1就是通过装饰传进来的参数f1。
这样下来,就完成了对f1的装饰,实现了权限验证。
装饰器知识点
执行时机
了解了装饰器的原理后,那么它的执行时机是什么样呢,接下来就来看一下。
国际惯例,先上代码
def w1(fun):
print('...装饰器开始装饰...')
def inner():
print('...验证权限...')
fun()
return inner
@w1
def test():
print('test')
test()
运行结果为:
...装饰器开始装饰...
...验证权限...
test
由此可以发现,当python解释器执行到@w1时,就开始进行装饰了,相当于执行了如下代码:
test = w1(test)
两个装饰器执行流程和装饰结果
当有两个或两个以上装饰器装饰一个函数时,那么执行流程和装饰结果是什么样的呢?同样,还是以代码来说明问题。
def makeBold(fun):
print('----a----')
def inner():
print('----1----')
return '<b>' + fun() + '</b>'
return inner
def makeItalic(fun):
print('----b----')
def inner():
print('----2----')
return '<i>' + fun() + '</i>'
return inner
@makeBold
@makeItalic
def test():
print('----c----')
print('----3----')
return 'hello python decorator'
ret = test()
print(ret)
运行结果为:
----b----
----a----
----1----
----2----
----c----
----3----
<b><i>hello python decorator</i></b>
可以发现,先用第二个装饰器(makeItalic)进行装饰,接着再用第一个装饰器(makeBold)进行装饰,而在调用过程中,先执行第一个装饰器(makeBold),接着再执行第二个装饰器(makeItalic)。
为什么呢,分两步来分析一下。
- 装饰时机 通过上面装饰时机的介绍,我们可以知道,在执行到@makeBold的时候,需要对下面的函数进行装饰,此时解释器继续往下走,发现并不是一个函数名,而又是一个装饰器,这时候,@makeBold装饰器暂停执行,而接着执行接下来的装饰器@makeItalic,接着把test函数名传入装饰器函数,从而打印’b’,在makeItalic装饰完后,此时的test指向makeItalic的inner函数地址,这时候有返回来执行@makeBold,接着把新test传入makeBold装饰器函数中,因此打印了’a’
- 在调用test函数的时候,根据上述分析,此时test指向makeBold.inner函数,因此会先打印‘1‘,接下来,在调用fun()的时候,其实是调用的makeItalic.inner()函数,所以打印‘2‘,在makeItalic.inner中,调用的fun其实才是我们最原声的test函数,所以打印原test函数中的‘c‘,‘3‘,所以在一层层调完之后,打印的结果为
<b><i>hello python decorator</i></b>
。
对无参函数进行装饰
上面例子中的f1 f2都是对无参函数的装饰,不再单独举例
对有参函数进行装饰
在使用中,有的函数可能会带有参数,那么这种如何处理呢?
程序示例:
def w_say(fun):
"""
如果原函数有参数,那闭包函数必须保持参数个数一致,并且将参数传递给原方法
"""
def inner(name):
"""
如果被装饰的函数有行参,那么闭包函数必须有参数
:param name:
:return:
"""
print('say inner called')
fun(name)
return inner
@w_say
def hello(name):
print('hello ' + name)
hello('luokk')
运行结果为:
say inner called
hello luokk
具体说明代码注释已经有了,就不再单独说明了。
此时,也许你就会问了,那是一个参数的,如果多个或者不定长参数呢,该如何处理呢?看看下面的代码你就秒懂了。
def w_add(func):
def inner(*args, **kwargs):
print('add inner called')
func(*args, **kwargs)
return inner
@w_add
def add(a, b):
print('%d + %d = %d' % (a, b, a + b))
@w_add
def add2(a, b, c):
print('%d + %d + %d = %d' % (a, b, c, a + b + c))
add(2, 4)
add2(2, 4, 6)
运行结果为:
add inner called
2 + 4 = 6
add inner called
2 + 4 + 6 = 12
利用python的可变参数轻松实现装饰带参数的函数。
对带返回值的函数进行装饰
程序示例:
def w_test(func):
def inner():
print('w_test inner called start')
func()
print('w_test inner called end')
return inner
@w_test
def test():
print('this is test fun')
return 'hello'
ret = test()
print('ret value is %s' % ret)
运行结果为:
w_test inner called start
this is test fun
w_test inner called end
ret value is None
可以发现,此时,并没有输出test函数的‘hello’,而是None,那是为什么呢,可以发现,在inner函数中对test进行了调用,但是没有接受不了返回值,也没有进行返回,那么默认就是None了,知道了原因,那么来修改一下代码:
def w_test(func):
def inner():
print('w_test inner called start')
str = func()
print('w_test inner called end')
return str
return inner
@w_test
def test():
print('this is test fun')
return 'hello'
ret = test()
print('ret value is %s' % ret)
运行结果为:
w_test inner called start
this is test fun
w_test inner called end
ret value is hello
这样就达到预期,完成对带返回值参数的函数进行装饰。
带参数的装饰器
介绍了对带参数的函数和有返回值的函数进行装饰,那么有没有带参数的装饰器呢,如果有的话,又有什么用呢?
答案肯定是有的,接下来通过代码来看一下吧。
def func_args(pre='xiaoqiang'):
def w_test_log(func):
def inner():
print('...记录日志...visitor is %s' % pre)
func()
return inner
return w_test_log
# 带有参数的装饰器能够起到在运行时,有不同的功能
# 先执行func_args('luokk'),返回w_test_log函数的引用
# @w_test_log
# 使用@w_test_log对test_log进行装饰
@func_args('luokk')
def test_log():
print('this is test log')
test_log()
运行结果为:
...记录日志...visitor is luokk
this is test log
简单理解,带参数的装饰器就是在原闭包的基础上又加了一层闭包,通过外层函数func_args的返回值w_test_log就看出来了,具体执行流程在注释里已经说明了。
好处就是可以在运行时,针对不同的参数做不同的应用功能处理。
内置装饰器
class Person:
def __init__(self,name,age,sex = '男'):
self.name = name
self.age = age
self.sex = sex
@property # 调用方法可以像调用属性一样
def play(self):
print('%s 正在玩游戏,啊哈哈---' % self.name)
@classmethod # 第一个参数自动传入类
def learn(cls):
print('%s 需要学习,emm---' % cls)
@staticmethod # 不再自动传入self或cls
def sleep():
print('人都要睡觉')
三. 异常处理
3.1 异常处理简介
python解释器检测到错误,触发异常(也允许程序员自己触发异常)。程序员编写特定的代码,专门用来捕捉这个异常(这段代码与程序逻辑无关,与异常处理有关)。如果捕捉成功则进入另外一个处理分支,执行你为其定制的逻辑,使程序不会崩溃,这就是异常处理。
python解释器去执行程序,检测到了一个错误时,触发异常,异常触发后且没被处理的情况下,程序就在当前异常处终止,后面的代码不会运行,谁会去用一个运行着突然就崩溃的软件。所以你必须提供一种异常处理机制来增强你程序的健壮性与容错性。良好的容错能力,能够有效的提高用户体验,维持业务的稳定性。
程序运行中的异常可以分为两类:语法错误和逻辑错误。首先,我们必须知道,语法错误跟异常处理无关,所以我们在处理异常之前,必须避免语法上的错误。
3.2 异常处理的方式
1.使用if判断式
#我们平时用if做的一些简单的异常处理
num1=input('>>: ') #输入一个字符串试试
if num1.isdigit():
int(num1) #我们的正统程序放到了这里,其余的都属于异常处理范畴
elif num1.isspace():
print('输入的是空格,就执行我这里的逻辑')
elif len(num1) == 0:
print('输入的是空,就执行我这里的逻辑')
else:
print('其他情情况,执行我这里的逻辑')#这些if,跟代码逻辑并无关系,显得可读性极差,如果类似的逻辑多,那么每一次都需要判断这些内容,就会倒置我们的代码特别冗长。
使用if判断式可以异常处理,但是if判断式的异常处理只能针对某一段代码,对于不同的代码段的相同类型的错误你需要写重复的if来进行处理。而且在你的程序中频繁的写与程序本身无关,与异常处理有关的if,会使得你的代码可读性极其的差。
2.python提供的特定的语法结构
part1:基本语法
try:
被检测的代码块
except 异常类型:
try中一旦检测到异常,就执行这个位置的逻辑
part2:单分支
#单分支只能用来处理指定的异常情况,如果未捕获到异常,则报错
try:
a
except NameError as e: #我们可以使用except与as+变量名 搭配使用,打印变量名会直接输出报错信息
print(e) #name 'a' is not defined
part3:多分支
l1 = [('电脑',16998),('鼠标',59),('手机',8998)]
while 1:
for key,value in enumerate(l1,1):
print(key,value[0])
try:
num = input('>>>')
price = l1[int(num)-1][1]
except ValueError:
print('请输入一个数字')
except IndexError:
print('请输入一个有效数字')
#这样通过异常处理可以使得代码更人性化,用户体验感更好。
part4:万能异常
在python的异常中,有一个万能异常:Exception,他可以捕获任意异常。它是一把双刃剑,有利有弊,我们要视情况使用
如果你想要的效果是,无论出现什么异常,我们统一丢弃,或者使用同一段代码逻辑去处理他们,那么只有一个Exception就足够了。
如果你想要的效果是,对于不同的异常我们需要定制不同的处理逻辑,那就需要用到多分支了。我们可以使用多分支+万能异常来处理异常。使用多分支优先处理一些能预料到的错误类型,一些预料不到的错误类型应该被最终的万能异常捕获。需要注意的是,万能异常一定要放在最后,否则就没有意义了。
part5:try…else语句
try:
for i in range(10):
int(i)
except IndexError as e:
print(e)
else:
print('***********') #*********** 执行了此处
#当try语句中的代码没有异常,被完整地执行完,就执行else中的代码
part6:try…finally语句
def dealwith_file():
try:
f = open('file',encoding='utf-8')
for line in f:
int(line)
return True
except Exception as e:
print(e)
return False
finally:
'''不管try语句中的代码是否报错,都会执行finally分支中的代码'''
'''去完成一些连接操作的收尾工作'''
print('finally 被执行了')
f.close()
ret = dealwith_file()
print(ret)
part7:主动触发异常
try:
raise TypeError('类型错误')
except Exception as e:
print(e)
part8:自定义异常
class EvaException(BaseException):
def __init__(self,msg):
self.msg=msg
def __str__(self):
return self.msg
try:
raise EvaException('类型错误')
except EvaException as e:
print(e)
part9:断言
assert断言是声明其布尔值必须为真的判定,如果发生异常就说明表达示为假。可以理解assert断言语句为raise-if-not,用来测试表示式,其返回值为假,就会触发异常。
assert的异常参数,其实就是在断言表达式后添加字符串信息,用来解释断言并更好的知道是哪里出了问题。格式如下:
assert expression [, arguments]
assert 表达式 [, 参数]
assert len(lists) >=5,‘列表元素个数小于5’
assert 2==1,‘2不等于1’
备注:格式:assert 条件 , 条件为false时的错误信息 结果为raise一个AssertionError出来
# assert 条件
assert 1 == 1
assert 1 == 2
四. 作业
本节作业
1、利用装饰器,记录函数的运行次数
2、打开一个只读文件,如果文件不存在,则去创建这个文件
上节答案
1、测试列表推导和不用列表推导那一种速度更快
import time
class RunTime:
def __enter__(self):
self.start_time = time.time()
return self.start_time
def __exit__(self, exc_type, exc_val, exc_tb):
self.end_time = time.time()
self.run_time = self.end_time - self.start_time
print('Time consuming %s' % self.run_time)
with RunTime():
li = []
for i in range(10000000):
li.append(i)
with RunTime():
li = [i for i in range(10000000)]
运行结果:
Time consuming 1.4124648571014404
Time consuming 0.8428022861480713
2、range不可以使用小数做步长,实现一个可迭代对象,可以实现小数步长
class Float_range:
def __init__(self,start,end = None,step = 1):
if not isinstance(start,(int,float)):
raise TypeError
elif end != None and not isinstance(end,(int,float)):
raise TypeError
elif not isinstance(end,(int,float)):
raise TypeError
elif end and end < start:
print('开始值不能小于结束值')
elif step < 0:
print('步长不能小于0')
else:
if end == None:
self.start = 0
self.end = start
self.step = step
else:
self.start = start
self.end = end
self.step = step
def __iter__(self):
return self
def __next__(self):
import decimal
res = self.start
if float(self.start) < float(self.end):
self.start = decimal.Decimal(str(self.start)) + decimal.Decimal(str(self.step))
return res
else:
raise StopIteration
a = Float_range(1,3,0.1)
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
运行结果:
1
1.1
1.2
1.3
1.4
1.5
1.6
1.7
1.8