python装饰器、迭代器和生成器

一 装饰器介绍

1.1 为何要用装饰器

软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。

软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。

1.2 什么是装饰器

’装饰’代指为被装饰对象添加新的功能,’器’代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景,装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

提示:可调用对象有函数,方法或者类,此处我们单以本章主题函数为例,来介绍函数装饰器,并且被装饰的对象也是函数。

二 装饰器的实现

函数装饰器分为:无参装饰器和有参装饰两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物。

2.1 无参装饰器的实现

如果想为下述函数添加统计其执行时间的功能

import time

def index():

    time.sleep(3)

    print('Welcome to the index page’)

    return 200

index() #函数执行

遵循不修改被装饰对象源代码的原则,我们想到的解决方法可能是这样

start_time=time.time()

index() #函数执行

stop_time=time.time()

print('run time is %s' %(stop_time-start_time))

考虑到还有可能要统计其他函数的执行时间,于是我们将其做成一个单独的工具,函数体需要外部传入被装饰的函数从而进行调用,我们可以使用参数的形式传入

def wrapper(func): # 通过参数接收外部的值

    start_time=time.time()

    res=func()

    stop_time=time.time()

    print('run time is %s' %(stop_time-start_time))

    return res

但之后函数的调用方式都需要统一改成

wrapper(index)

wrapper(其他函数)

这便违反了不能修改被装饰对象调用方式的原则,于是我们换一种为函数体传值的方式,即将值包给函数,如下

def timer(func):

    def wrapper(): # 引用外部作用域的变量func

        start_time=time.time()

        res=func()

        stop_time=time.time()

        print('run time is %s' %(stop_time-start_time))

        return res

    return wrapper

这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能,只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名 /函数名index,如下

index=timer(index)#得到index=wrapper,wrapper携带对外作用域的引用:func=原始的index()# 执行的是wrapper(),在wrapper的函数体内再执行最原始的index

至此我们便实现了一个无参装饰器timer,可以在不修改被装饰对象index源代码和调用方式的前提下为其加上新功能。但我们忽略了若被装饰的函数是一个有参函数,便会抛出异常。

def home(name):

    time.sleep(5)

    print('Welcome to the home page',name)

home=timer(home)

home('汤勇')

#抛出异常

TypeError: wrapper() takes 0 positional arguments but 1 was given

之所以会抛出异常,是因为home(‘汤勇’)调用的其实是wrapper(‘汤勇’),而函数wrapper没有参数。wrapper函数接收的参数其实是给最原始的func用的,为了能满足被装饰函数参数的所有情况,便用上args+*kwargs组合(,于是修正装饰器timer如下

def timer(func):

    def wrapper(*args,**kwargs):

        start_time=time.time()

        res=func(*args,**kwargs)

        stop_time=time.time()

        print('run time is %s' %(stop_time-start_time))

        return res

    return wrapper

此时我们就可以用timer来装饰带参数或不带参数的函数了,但是为了简洁而优雅地使用装饰器,Python提供了专门的装饰器语法来取代index=timer(index)的形式,需要在被装饰对象的正上方单独一行添加@timer,当解释器解释到@timer时就会调用timer函数,且把它正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名

@timer # index=timer(index)

def index():

    time.sleep(3)

    print('Welcome to the index page')

    return 200

@timer # index=timer(home)          def home(name):

    time.sleep(5)

    print('Welcome to the home page’,name)

如果我们有多个装饰器,可以叠加多个

@deco3

@deco2

@deco1

def index():

    pass

叠加多个装饰器也无特殊之处,上述代码语义如下:

index=deco3(deco2(deco1(index)))

2.2 有参装饰器的实现

了解无参装饰器的实现原理后,我们可以再实现一个用来为被装饰对象添加认证功能的装饰器,实现的基本形式如下

def deco(func):

    def wrapper(*args,**kwargs):

        编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res

    return wrapper

如果我们想提供多种不同的认证方式以供选择,单从wrapper函数的实现角度改写如下

def deco(func):

        def wrapper(*args,**kwargs):

            if driver == 'file':

                编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res

            elif driver == 'mysql':

                编写基于mysql认证,认证通过则执行res=func(*args,**kwargs),并返回res

        return wrapper

函数wrapper需要一个driver参数,而函数deco与wrapper的参数都有其特定的功能,不能用来接受其他类别的参数,可以在deco的外部再包一层函数auth,用来专门接受额外的参数,这样便保证了在auth函数内无论多少层都可以引用到

def auth(driver):

    def deco(func):

        ……

    return deco

此时我们就实现了一个有参装饰器,使用方式如下

先调用auth_type(driver='file'),得到@deco,deco是一个闭包函数,

包含了对外部作用域名字driver的引用,@deco的语法意义与无参装饰器一样

@auth(driver='file')

def index():   

    pass

@auth(driver='mysql')

def home():

    pass 

可以使用help(函数名)来查看函数的文档注释,本质就是查看函数的doc属性,但对于被装饰之后的函数,查看文档注释

@timer

def home(name):

    '''

    home page function

    :param name: str

    :return: None

    '''

    time.sleep(5)

    print('Welcome to the home page',name)

print(help(home))

'''

打印结果:

Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

None

在被装饰之后home=wrapper,查看home.name也可以发现home的函数名确实是wrapper,想要保留原函数的文档和函数名属性,需要修正装饰器

def timer(func):

    def wrapper(*args,**kwargs):

        start_time=time.time()

        res=func(*args,**kwargs)

        stop_time=time.time()

        print('run time is %s' %(stop_time-start_time))

        return res

    wrapper.__doc__=func.__doc__

    wrapper.__name__=func.__name__

    return wrapper

按照上述方式来实现保留原函数属性过于麻烦,functools模块下提供一个装饰器wraps专门用来帮我们实现这件事,用法如下

from functools import wraps

def timer(func):

    @wraps(func)

    def wrapper(*args,**kwargs):

        start_time=time.time()

        res=func(*args,**kwargs)

        stop_time=time.time()

        print('run time is %s' %(stop_time-start_time))

        return res

    return wrapper

三 迭代器介绍

迭代器即用来迭代取值的工具,而迭代是重复反馈过程的活动,其目的通常是为了逼近所需的目标或结果,每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,单纯的重复并不是迭代

while True:

    msg = input('>>: ').strip()

    print(msg)

下述while循环才是一个迭代过程,不仅满足重复,而且以每次重新赋值后的index值作为下一次循环中新的索引进行取值,反复迭代,最终可以取尽列表中的值

goods=['mac','lenovo','acer','dell','sony']

index=0

while index < len(goods):

    print(goods[index])

    index+=1

3.1 可迭代对象

通过索引的方式进行迭代取值,实现简单,但仅适用于序列类型:字符串,列表,元组。对于没有索引的字典、集合等非序列类型,必须找到一种不依赖索引来进行迭代取值的方式,这就用到了迭代器。

要想了解迭代器为何物,必须事先搞清楚一个很重要的概念:可迭代对象(Iterable)。从语法形式上讲,内置有__iter__方法的对象都是可迭代对象,字符串、列表、元组、字典、集合、打开的文件都是可迭代对象:

{'name':'egon'}.__iter__

{7,8,9}.__iter__

……


3.2 迭代器对象

调用obj.iter()方法返回的结果就是一个迭代器对象(Iterator)。迭代器对象是内置有iter和next方法的对象,打开的文件本身就是一个迭代器对象,执行迭代器对象.iter()方法得到的仍然是迭代器本身,而执行迭代器.next()方法就会计算出迭代器中的下一个值。 迭代器是Python提供的一种统一的、不依赖于索引的迭代取值方式,只要存在多个“值”,无论序列类型还是非序列类型都可以按照迭代器的方式取值

>>> s={1,2,3} # 可迭代对象s

>>> i=iter(s)  # 本质就是在调用s.__iter__(),返回s的迭代器对象i,

>>> next(i) # 本质就是在调用i.__next__()

1

>>> next(i)

2

>>> next(i)

3

>>> next(i)  #抛出StopIteration的异常,代表无值可取,迭代结束

四 for循环原理

有了迭代器后,我们便可以不依赖索引迭代取值了,使用while循环的实现方式如下

goods=['mac','lenovo','acer','dell','sony']

i=iter(goods) #每次都需要重新获取一个迭代器对象

while True:

    try:

        print(next(i))

    except StopIteration: #捕捉异常终止循环

        break

for循环又称为迭代循环,in后可以跟任意可迭代对象,上述while循环可以简写为

goods=['mac','lenovo','acer','dell','sony']

for item in goods: 

    print(item)

for 循环在工作时,首先会调用可迭代对象goods内置的iter方法拿到一个迭代器对象,然后再调用该迭代器对象的next方法将取到的值赋给item,执行循环体完成一次循环,周而复始,直到捕捉StopIteration异常,结束迭代。


五 迭代器的优缺点

基于索引的迭代取值,所有迭代的状态都保存在了索引中,而基于迭代器实现迭代的方式不再需要索引,所有迭代的状态就保存在迭代器中,然而这种处理方式优点与缺点并存:

5.1 优点:

1、为序列和非序列类型提供了一种统一的迭代取值方式。

2、惰性计算:迭代器对象表示的是一个数据流,可以只在需要时才去调用next来计算出一个值,就迭代器本身来说,同一时刻在内存中只有一个值,因而可以存放无限大的数据流,而对于其他容器类型,如列表,需要把所有的元素都存放于内存中,受内存大小的限制,可以存放的值的个数是有限的。


5.2 缺点:

1、除非取尽,否则无法获取迭代器的长度

2、只能取下一个值,不能回到开始,更像是‘一次性的’,迭代器产生后的唯一目标就是重复执行next方法直到值取尽,否则就会停留在某个位置,等待下一次调用next;若是要再次迭代同个对象,你只能重新调用iter方法去创建一个新的迭代器对象,如果有两个或者多个循环使用同一个迭代器,必然只会有一个循环能取到值。


六  生成器与yield

若函数体包含yield关键字,再调用函数,并不会执行函数体代码,得到的返回值即生成器对象

>>> def my_range(start,stop,step=1):

...    print('start...')

...    while start < stop:

...        yield start

...        start+=step

...    print('end...')

...

>>> g=my_range(0,3)

>>> g

<generator object my_range at 0x104105678>

生成器内置有__iter__和__next__方法,所以生成器本身就是一个迭代器

>>> g.__iter__

<method-wrapper '__iter__' of generator object at 0x1037d2af0>

>>> g.__next__

<method-wrapper '__next__' of generator object at 0x1037d2af0>

因而我们可以用next(生成器)触发生成器所对应函数的执行

>>> next(g) # 触发函数执行直到遇到yield则停止,将yield后的值返回,并在当前位置挂起函数

start...

0

>>> next(g) # 再次调用next(g),函数从上次暂停的位置继续执行,直到重新遇到yield...

1

>>> next(g) # 周而复始...

2

>>> next(g) # 触发函数执行没有遇到yield则无值返回,即取值完毕抛出异常结束迭代

end...

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

StopIteration

既然生成器对象属于迭代器,那么必然可以使用for循环迭代,如下:

>>> for i in countdown(3):

...    print(i)

...

countdown start

3

2

1

Done!

有了yield关键字,我们就有了一种自定义迭代器的实现方式。yield可以用于返回值,但不同于return,函数一旦遇到return就结束了,而yield可以保存函数的运行状态挂起函数,用来返回多次值

七  yield表达式应用

在函数内可以采用表达式形式的yield

>>> def eater():

...    print('Ready to eat')

...    while True:

...        food=yield

...        print('get the food: %s, and start to eat' %food)

...

可以拿到函数的生成器对象持续为函数体send值,如下

>>> g=eater() # 得到生成器对象

>>> g

<generator object eater at 0x101b6e2b0>

>>> next(e) # 需要事先”初始化”一次,让函数挂起在food=yield,等待调用g.send()方法为其传值

Ready to eat

>>> g.send('包子')

get the food: 包子, and start to eat

>>> g.send('鸡腿')

get the food: 鸡腿, and start to eat

针对表达式形式的yield,生成器对象必须事先被初始化一次,让函数挂起在food=yield的位置,等待调用g.send()方法为函数体传值,g.send(None)等同于next(g)。

​ 我们可以编写装饰器来完成为所有表达式形式yield对应生成器的初始化操作,如下

def init(func):

    def wrapper(*args,**kwargs):

        g=func(*args,**kwargs)

        next(g)

        return g

    return wrapper

@init

def eater():

    print('Ready to eat')

    while True:

        food=yield

        print('get the food: %s, and start to eat' %food)

表达式形式的yield也可以用于返回多次值,即变量名=yield 值的形式,如下

>>> def eater():

...    print('Ready to eat')

...    food_list=[]

...    while True:

...        food=yield food_list

...        food_list.append(food)

...

>>> e=eater()

>>> next(e)

Ready to eat

[]

>>> e.send('蒸羊羔')

['蒸羊羔']

>>> e.send('蒸熊掌')

['蒸羊羔', '蒸熊掌']

>>> e.send('蒸鹿尾儿')

['蒸羊羔', '蒸熊掌', '蒸鹿尾儿']

八 三元表达式、列表生成式、生成器表达式

8.1 三元表达式

三元表达式是python为我们提供的一种简化代码的解决方案,语法如下

res=条件成立时返回的值if条件else条件不成立时返回的值

针对下述场景

def max2(x,y):

    if x > y:

        return x

    else:

        return y

res = max2(1,2)

用三元表达式可以一行解决

x=1

y=2

res = x if x > y else y # 三元表达式

8.2 列表生成式

列表生成式是python为我们提供的一种简化代码的解决方案,用来快速生成列表,语法如下

[expression for item1 in iterable1 if condition1

for item2 in iterable2 if condition2

...

for itemN in iterableN if conditionN

]

#类似于

res=[]

for item1 in iterable1:

    if condition1:

        for item2 in iterable2:

            if condition2

                ...

                for itemN in iterableN:

                    if conditionN:

                        res.append(expression)

针对下述场景

egg_list=[]

for i in range(10):

    egg_list.append('鸡蛋%s' %i)

用列表生成式可以一行解决

egg_list=['鸡蛋%s'%iforiinrange(10)]

8.3 生成器表达式

创建一个生成器对象有两种方式,一种是调用带yield关键字的函数,另一种就是生成器表达式,与列表生成式的语法格式相同,只需要将[]换成(),即:

(expression for item in iterable if condition)

对比列表生成式返回的是一个列表,生成器表达式返回的是一个生成器对象

>>> [x*x for x in range(3)]

[0, 1, 4]

>>> g=(x*x for x in range(3))

>>> g

<generator object <genexpr> at 0x101be0ba0>

对比列表生成式,生成器表达式的优点自然是节省内存(一次只产生一个值在内存中)

>>> next(g)

0

>>> next(g)

1

>>> next(g)

4

>>> next(g) #抛出异常StopIteration

如果我们要读取一个大文件的字节数,应该基于生成器表达式的方式完成

with open('db.txt','rb') as f:

    nums=(len(line) for line in f)

    total_size=sum(nums) # 依次执行next(nums),然后累加到一起得到结果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,820评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,648评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,324评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,714评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,724评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,328评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,897评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,804评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,345评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,431评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,561评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,238评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,928评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,417评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,528评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,983评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,573评论 2 359