Python函数之装饰器&列表生成式&迭代器&生成器

一 装饰器

1.1 什么是装饰器

装饰器本身可以是任意可调用对象,被装饰者也可以是任意可调用对象

装饰器的原则:

  • 不修改被装饰对象的源代码
  • 不修改被装饰对象的调用方式

装饰器的目标:在遵循前面两点的前提下,为被装饰对象添加上新功能

其实装饰器就是在闭包的基础上多进行了几步,如下:

def decorator(func):    # 装饰函数
    def wrapper():
        print('在传入函数执行前做一些操作')
        func()   # 执行函数
        print('在传入函数执行后做一些操作')
    return wrapper

def login():    # 被装饰函数
    print('登录成功')

login = decorator(login)   # 将被装饰函数传入装饰函数中,并覆盖了原函数的入口
login()   # 此时执行的就是被装饰后的函数

# 执行结果
在传入函数执行前做一些操作
登录成功
在传入函数执行后做一些操作

更多闭包和装饰器可参考: http://www.jb51.net/article/86383.htm

1.2 为何要使用装饰器

开放封闭原则:简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展,即:

  • 封闭:已实现的功能代码块不应该被修改
  • 开放:对现有功能的扩展开放

1.3 装饰器语法

# 被装饰函数的正上方,单独一行采用@+装饰函数
@deco1
@deco2
@deco3
def foo():
    pass

# 上面的结果等价于:
foo = deco1(deco2(deco3(foo)))

1.4 装饰器的应用

def timer(func):
    def wrapper(*args,**kwargs):
        start_time = time.time()
        res = func(*args,**kwargs)
        stop_time = time.time()
        print('foo函数执行时间: %.2f' %(stop_time - start_time))
        return res
    return wrapper

@timer          # 相当于执行timer(foo)
def foo(second):
    time.sleep(second)
    print('foo函数执行完成')
    return True

res = foo(1)
print(res)
无参装饰器
def data_source(data='file'):
    def auth(func):
        def wrapper(*args,**kwargs):
            name = input("Please input username:>> ")
            pwd = input("Please input password:>> ")
            if data == 'file':
                if name == 'joe' and pwd == '123':
                    print('login successful')
                    res = func(*args,**kwargs)
                    return res
            elif data == 'xxx':
                print('xxx')
        return wrapper
    return auth

@data_source(data='file')    # 实际执行效果:login = data_source(data='file')(login)
def login():
    print('执行完成')

login()
有参装饰器

有参装饰器说明:

上面的例子的中,有参装饰器的实际执行的效果为:

login = data_source(data='file')(login)

我们来剖析上面的语句,首先执行data_source(data='file'),返回的是auth函数,再调用返回的函数,参数是login函数,返回值最终是wrapper函数。

有参装饰器多用在:当一个函数不需要加装饰器的时候,如果直接把装饰器去掉,代码太多去掉显得麻烦而且防止后续会再次使用,这时我们可以利用有参装饰器去装饰它,具体代码如下:

def outer(flag):
    def decoreator(func):
        def wrapper(*args,**kwargs):
            if flag:
                print('装饰器函数生效')
                ret = func(*args,**kwargs)
            else:
                print('装饰器函数不使用')
                ret = func(*args, **kwargs)
            return ret
        return wrapper
    return decoreator

f = False       # 去掉装饰器
# f = True        # 加装装饰器
@outer(f)
def func():
    print('原函数执行')

func()
View Code
def decoreator1(func):
    def wrapper(*args,**kwargs):
        print('装饰器1执行开始')
        ret = func(*args, **kwargs)    # 此时的func变为了decoreator2中的wrapper()函数
        print('装饰器1执行结束')
        return ret
    return wrapper

def decoreator2(func):
    def wrapper(*args,**kwargs):
        print('装饰器2执行开始')
        ret = func(*args, **kwargs)    # 被装饰函数func()
        print('装饰器2执行结束')
        return ret
    return wrapper

@decoreator1
@decoreator2
def func():         # 等价于decoreator1(decoreator2(func))
    print('原函数执行')

func()

# 执行结果
装饰器1执行开始
装饰器2执行开始
原函数执行
装饰器2执行结束
装饰器1执行结束
多装饰器
time = []

def decoreator(func):
    time.append(func)           # 统计当前程序中有多少个函数被装饰了
    def warpper(*args,**kwargs):
        # time.append(func)       # 统计本次程序执行有多少个带装饰器的函数被调用
        ret = func(*args,**kwargs)
    return warpper

@decoreator
def func1():
    pass

@decoreator
def func2():
    pass

@decoreator
def func3():
    pass

func1()

print(time)
统计被装饰器函数及调用情况

1.5 wraps

先看下面一个例子:

def decorator(func):   
    def wrapper(*args,**kwargs):
        func(*args,**kwargs)
    return wrapper

@ decorator
def login():
    pass

print(login.__name__)       # wrapper

经过装饰器装饰过后,函数对象的__name__已经从原来的'login'变成了'wrapper',想要其不改变,又能完成装饰器功能,可以这样做:

from functools import wraps     # 导入wraps,模块导入后面会详讲

def decorator(func):
    @wraps(func)    # 加在最内层函数正上方
    def wrapper(*args,**kwargs):
        func(*args,**kwargs)
    return wrapper

@ decorator
def login():
    pass

print(login.__name__)       # login

login() 

二 列表生成式

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。

举个例子,要生成[1,2,3,4,5,6,7,8,9,10],这样一个列表,我们可以这样做:

>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

如果要生成[1x1, 2x2, 3x3, ..., 10x10],我们可以通过循环这样做:

l = []
for i in range(1,11):
    l.append(i*i)
print(l)

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

[x * x for x in range(1, 11)]

for循环后面还可以加上if判断,比如:我们在上面的例子中筛选出仅偶数的平方:

[x * x for x in range(1, 11) if x % 2 == 0]

还可以使用两层循环:

l = [int(i)*int(j) for i in '123' for j in '456']
print(l)

等价于:

l=[]
for i in '123':
    for j in '456':
        l.append(int(i)*int(j))
print(l)

运用列表生成式,可以写出非常简洁的代码。

补充说明:也存在字典生成式(推导式)和集合生成式(推导式),具体如下:

# 将一个字典的key和value对调
dic = {'a':1, 'b':2, 'c':3}
ret = {dic[i]:i for i in dic}
print(ret)      # {'1': 'a', '2': 'b', 3: 'c'}

# 将一个字典的value加值
dic = dic = {'a':1, 'b':2, 'c':3}
ret = {i:dic[i]+1 for i in dic}
print(ret)      # {'a': 2, 'c': 4, 'b': 3}

dic = dic = {'a':'1', 'b':'2', 'c':'3'}
ret = {i:dic[i]+'1' for i in dic}
print(ret)      # {'a': '11', 'c': '31', 'b': '21'}
字典推导式
# 计算列表中每个值的平方,自带去重功能
L = [1,2,3,4,5,-2,-4]
print({i**2 for i in L})
集合推导式

三 迭代器

3.1 为什么有迭代器

对于序列类型:字符串、列表、元组,我们可以使用索引的方式迭代取出其包含的元素。但对于字典、集合、文件等类型是没有索引的,如果想取出其内部包含的元素,则必须找出一种不依赖于索引的迭代方式,这就是迭代器

3.2 什么是可迭代对象

可迭代对象指的是内置有__iter__方法的对象,即obj.__iter__()。可迭代对象都可以使用for循环来遍历其包含的元素,比如:列表、元组、字符串等。

L = [1, 2, 3, 4, 5]

L.__iter__()      # 存在__iter__()方法

for i in L:
    print(i)

怎么判断一个对象是不是可迭代对象?

方法一:查看该对象是否存在__iter__()方法

方法二:通过collections模块的Iterable类型判断

from collections import Iterable

L = [1, 2, 3, 4, 5]

print(isinstance(L,Iterable))   # True

3.3 什么是迭代器对象

迭代器对象指的是内置即有__iter__()方法又有__next__()方法的对象,比如:文件对象

这里需要说明的是:迭代器一定是可迭代对象,但可迭代对象不一定是迭代器对象,我们可以用collections模块的Iterator类型进行判断一个对象是否为迭代器,同时可以使用iter()方法将可迭代对象装换为迭代器。

from collections import Iterable,Iterator

L = [1, 2, 3, 4, 5]

print(isinstance(L,Iterable))   # True
print(isinstance(L,Iterator))   # False

L = iter(L)    # 将可迭代对象转换为迭代器,等价于:L.__iter__()

print(isinstance(L,Iterable))   # True
print(isinstance(L,Iterator))   # True

3.4 迭代器的使用

Python的迭代器对象表示的是一个数据流,它可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。我们可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以迭代器的计算是惰性的,只有在需要返回下一个数据时它才会计算

L = [1, 2, 3, 4, 5]
L = iter(L)          # 将可迭代对象转换为迭代器,等价于:L.__iter__()

print(next(L))       # 等价于:L.__next__()
print(next(L))
print(next(L))
print(next(L))
print(next(L))
print(next(L))       # 报错:StopIteration

我们可以加入异常处理:

L = [1, 2, 3, 4, 5]
L = iter(L)          # 将可迭代对象转换为迭代器,等价于:L.__iter__()

while True:
    try:
        print(next(L))       # 等价于:L.__next__()
    except StopIteration:
        break

这样我们就可以不依赖索引迭代取值了。

上面这种不断调用next(L)实在是坑了,常用的方法是使用for循环,这时我们根本不需要关心StopIteration的错误:

L = [1, 2, 3, 4, 5]
L = iter(L)          # 将可迭代对象转换为迭代器,等价于:L.__iter__()

for i in L:
    print(i)

3.5 迭代器的优缺点

优点:

1. 迭代器提供一种不依赖于索引的取值方式,这样就可以遍历那些没有索引的可迭代对象,比如:字典、集合、文件等

2. 迭代器与列表比较,迭代器是惰性计算的,更省内存

缺点:

1. 永远无法获取迭代器的长度使用不如列表索引取值灵活

2. 迭代器不能向后移动,不能回到开始,一次性的

3.6 总结

凡是可作用于for循环的对象都是Iterable类型

凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列

集合数据类型如:listdictstr等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象

Python的for循环本质上就是通过不断调用next()函数实现的,针对可迭代对象for循环内部事实上是先调用iter()把Iterable变成Iterator再进行循环迭代的。例如:

L = [1, 2, 3, 4, 5]
for i in L:
    print(i)

# 完全等价于
L = [1, 2, 3, 4, 5]
it = iter(L)      # 首先获得Iterator对象
# 循环:
while True:
    try:
        x = next(it)       # 获得下一个值:
    except StopIteration:
        break              # 遇到StopIteration就退出循环

四 生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。它的本质就是一个迭代器

4.1 生成器的创建

要创建一个generator,有很多种方法,下面依次进行介绍。

方式一:将列表生成式的[]改成()

该方式我们也成为:生成器表达式

g = (x * x for x in range(1,11))
print(g)        # <generator object <genexpr> at 0

此时的g就是一个生成器对象,我们怎么打印出生成器的每一个元素呢?因为生成器本质上就是一个迭代器,所以,可以通过next()函数获得生成器的下一个返回值:

g = (x * x for x in range(1,11))
print(g)        # <generator object <genexpr> at 0x00000200ED65E4C0>
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))  # 值取完后,报错:StopIteration

当然我们也可以使用for循环,而且是最常用的使用方式:

g = (x * x for x in range(1,11))
for i in g:
    print(i)

方式二:yeild

先看一个例子:斐波拉契数列(Fibonacci)

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1

fib(10)  # 依次打印1,1,2,3,5,8,13,21,34,55

从这个例子可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。

也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator,只需要把print(b)改为yield b就可以了:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1

这就是定义生成器的另外一种方式--采用关键字yield。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator,函数名()得到的结果就是生成器:

print(fib(10))   # <generator object fib at 0x000001BFF7C6E468>

同样的,我们可以通过for循环依次取值:

for i in fib(10):
    print(i)

需要注意的是:generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

这里还有一个问题,如果我们定义的生成器函数存在返回值,利用for循环调用时,无法接收返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIterationvalue中:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'Done'

g = fib(10)

while True:
    try:
        print(next(g))
    except StopIteration as e:
        print('生成器返回值为:%s' %e.value)
        break

4.2 yield from

def func():
    # for i in [1,2,3,4,5]:
    #     yield i
    yield from [1,2,3,4,5]  # 等价于上面的for循环

g = func()
print(next(g))
print(next(g))
print(next(g))

yield与return的区别:

return只能返回一次函数就彻底结束了,而yield能返回多次值

yield到底干了什么事?

1. 把函数变成生成器-->迭代器。相当于把__iter__和__next__方法封装到函数内部

2. 用return只能返回一次,而yield返回多次

3. 函数在暂停以及继续下一次运行时的状态是由yield保存

yield总结:

1. 把函数做成迭代器

2. 对比return,可以返回多次值,可以挂起/保存函数的运行状态

4.3 协程函数

先来看一个例子:

# yield关键字的另外一种使用形式:表达式形式的yield
from urllib.request import urlopen

def get():
    while True:
        url = yield     # 依次接收send过来的值
        ret = urlopen(url).read()
        print(ret)

g = get()
g.send(None)    # 对于表达式形式的yield,在使用时,第一次必须传None,g.send(None)等同于next(g)
g.send('https://www.hao123.com/')
g.send('http://sports.qq.com/nba/')

g.send和next()的区别:

1. 如果函数内yield是表达式形式,那么必须先next(e)或e.send(None)

2. 二者共同之处是都可以让函数在上次暂定的位置继续运行,不一样的地方在于send在触发下一次代码的执行时,会顺便给yield传一个值

4.4 实例应用

模仿range()函数的实现:

def my_range(start, end, step=1):
    while start < end:
        yield start
        start += step

obj = my_range(0,10,2)      # <generator object my_range at 0x000001D87BAFE468>

for i in obj:
    print(i)
View Code
posted @ 2018-06-17 18:33  Joe1991  阅读(228)  评论(0)    收藏  举报