Python-迭代器、生成器、装饰器(1)

迭代器

迭代概念

概念

  • 通过for循环遍历对象的每一个元素的过程。
  • Python的for语法功能非常强大,可以遍历任何可迭代的对象。
  • 在Python中,list/tuple/string/dict/set/bytes都是可以迭代的数据类型。
  • 可以通过collections模块的Iterable类型来判断一个对象是否可迭代:
1
2
3
4
5
6
7
>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False

迭代器

概念

  • 迭代器是一种可以被遍历的对象,并且能作用于next()函数。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束

  • 迭代器只能往后遍历不能回溯,不像列表,你随时可以取后面的数据,也可以返回头取前面的数据。迭代器通常要实现两个基本的方法:iter() 和 next()。

  • 字符串,列表或元组对象,甚至自定义对象都可用于创建迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> lis=[1,2,3,4]
>>> it = iter(lis) # 使用Python内置的iter()方法创建迭代器对象
>>> next(it) # 使用next()方法获取迭代器的下一个元素
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
4
>>> next(it) # 当后面没有元素可以next的时候,弹出错误
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
next(it)
StopIteration
  • 或者使用for循环遍历迭代器:
1
2
3
4
lis = [1,2,3,4]
it = iter(lis) # 创建迭代器对象
for x in it: # 使用for循环遍历迭代对象
print (x, end=" ")
  • 很多时候,为了让我们自己写的类成为一个迭代器,需要在类里实现__iter__()和__next__()方法。

  • 总结:Python的迭代器表示的是一个元素流,可以被next()函数调用并不断返回下一个元素,直到没有元素时抛出StopIteration错误。可以把这个元素流看做是一个有序序列,但却不能提前知道序列的长度,只能不断通过next()函数得到下一个元素,所以迭代器可以节省内存和空间。

迭代器(Iterator)和可迭代(Iterable)的区别:

  • 凡是可作用于for循环的对象都是可迭代类型;

  • 凡是可作用于next()函数的对象都是迭代器类型;

  • list、dict、str等是可迭代的但不是迭代器,因为next()函数无法调用它们。可以通过iter()函数将它们转换成迭代器。

  • Python的for循环本质上就是通过不断调用next()函数实现的。

生成器

概念

  • 有时候,序列或集合内的元素的个数非常巨大,如果全制造出来并放入内存,对计算机的压力是非常大的。

  • 比如,假设需要获取一个10**20次方如此巨大的数据序列,把每一个数都生成出来,并放在一个内存的列表内,这是粗暴的方式,有如此大的内存么?

  • 如果元素可以按照某种算法推算出来,需要就计算到哪个,就可以在循环的过程中不断推算出后续的元素,而不必创建完整的元素集合,从而节省大量的空间。在Python中,这种一边循环一边计算出元素的机制,称为生成器:generator。

  • 前面我们说过,通过圆括号可以编写生成器推导式:

1
2
3
>>> g = (x * x for x in range(1, 4))
>>> g
<generator object <genexpr> at 0x1022ef630>
  • 可以通过next()函数获得generator的下一个返回值,这点和迭代器非常相似:
1
2
3
4
5
6
7
8
9
10
11
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
next(g)
StopIteration
  • 但更多情况下,我们使用for循环。

    1
    2
    for i in g:
    print(i)
  • 除了使用生成器推导式,我们还可以使用yield关键字。

  • 在 Python中,使用yield返回的函数会变成一个生成器(generator)。 在调用生成器的过程中,每次遇到yield时函数会暂停并保存当前所有的运行信息,返回yield的值。并在下一次执行**next()**方法时从当前位置继续运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 斐波那契函数
def fibonacci(n):
a, b, counter = 0, 1, 0
while True:
if counter > n:
return
yield a # yield让该函数变成一个生成器
a, b = b, a + b
counter += 1

fib = fibonacci(10) # fib是一个生成器
print(type(fib))
for i in fib:
print(i, end=" ")

装饰器

概念

  • 装饰器(Decorator):从字面上理解,就是装饰对象的器件。可以在不修改原有代码的情况下,为被装饰的对象增加新的功能或者附加限制条件或者帮助输出。

  • 装饰器有很多种,有函数的装饰器,也有类的装饰器。装饰器在很多语言中的名字也不尽相同,它体现的是设计模式中的装饰模式,强调的是开放封闭原则。装饰器的语法是将@装饰器名,放在被装饰对象上面。

    1
    2
    3
    @dec
    def func():
    pass
  • 在进行装饰器的介绍之前,我们必须先明确几个概念和原则:

  • 首先,Python程序是从上往下顺序执行的,而且碰到函数的定义代码块是不会立即执行的,只有等到该函数被调用时,才会执行其内部的代码块。

1
2
3
4
5
6
def foo():
print("foo函数被运行了!")

#如果就这么样,foo里的语句是不会被执行的。
#程序只是简单的将定义代码块读入内存中。
# foo() 只有调用了,才会执行
  • 其次,由于顺序执行的原因,如果你真的对同一个函数定义了两次,那么,后面的定义会覆盖前面的定义。因此,在Python中代码的放置位置是有区别的,不能随意摆放,通常函数体要放在调用的语句之前。
1
2
3
4
5
6
7
8
9
10
11
12
13
def foo():
print("我是上面的函数定义!")

foo()

def foo():
print("我是下面的函数定义!")

foo()
#----------------
执行结果:
我是上面的函数定义!
我是下面的函数定义!
  • 然后,我们还要先搞清楚几样东西:函数名、函数体、返回值,函数的内存地址、函数名加括号、函数名被当作参数、函数名加括号被当作参数、返回函数名、返回函数名加括号。
1
2
3
4
5
6
7
8
9
10
def outer(func):
def inner():
print("我是内层函数!")
return inner

def foo():
print("我是原始函数!")

outer(foo)
outer(foo())
  1. 函数名: foo、outer、inner
  2. 函数体:函数的整个代码结构
  3. 返回值: return后面的表达式
  4. 函数的内存地址:id(foo)、id(outer)等等
  5. 函数名加括号:对函数进行调用,比如foo()、outer(foo)
  6. 函数名作为参数: outer(foo)中的foo本身是个函数,但作为参数被传递给了outer函数
  7. 函数名加括号被当做参数:其实就是先调用函数,再将它的返回值当做别的函数的参数,例如outer(foo())
  8. 返回函数名:return inner
  9. 返回函数名加括号:return inner(),其实就是先执行inner函数,再将其返回值作为别的函数的返回值。
  • 函数也是一个对象,通过一个实例来讲解Python中装饰器的作用了。下例是针对函数的装饰器。

虚拟场景

  • 有一个大公司,下属的基础平台部负责内部应用程序及API的开发。另外还有上百个业务部门负责不同的业务
  • 这些业务部门各自调用基础平台部提供的不同函数,也就是API处理自己的业务,情况如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 基础平台部门开发了上百个函数API
def f1():
print("业务部门1的数据接口......")
def f2():
print("业务部门2的数据接口......")
def f3():
print("业务部门3的数据接口......")
def f100():
print("业务部门100的数据接口......")

#各部门分别调用自己需要的API
f1()
f2()
f3()
f100()
  • 公司还在创业初期时,基础平台部就开发了这些函数。由于各种原因,比如时间紧,比如人手不足,比如架构缺陷,比如考虑不周等等,没有为函数的调用进行安全认证。现在,公司发展壮大了,不能再像初创时期的“草台班子”一样将就下去了,基础平台部主管决定弥补这个缺陷,于是(以下场景纯属虚构,调侃之言,切勿对号入座):

  • 第一天:主管叫来了一个运维工程师,工程师跑上跑下逐个部门进行通知,让他们在代码里加上认证功能,然后,当天他被开除了。

  • 第二天:主管又叫来了一个运维工程师,工程师用shell写了个复杂的脚本,勉强实现了功能。但他很快就回去接着做运维了,不会开发的运维不是好运维….

  • 第三天:主管叫来了一个python自动化开发工程师。哥们是这么干的,只对基础平台的代码进行重构,让N个业务部门无需做任何修改。这哥们很快也被开了,连运维也没得做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def f1():
#加入认证程序代码
print("业务部门1数据接口......")
def f2():
# 加入认证程序代码
print("业务部门2数据接口......")
def f3():
# 加入认证程序代码
print("业务部门3数据接口......")
def f100():
#加入认证程序代码
print("业务部门100数据接口......")

#各部门分别调用
f1()
f2()
f3()
f100()
  • 第四天:主管又换了个开发工程师。他是这么干的:定义个认证函数,在原来其他的函数中调用它,代码如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def login():
print("认证成功!")

def f1():
login()
print("业务部门1数据接口......")
def f2():
login()
print("业务部门2数据接口......")
def f3():
login()
print("业务部门3数据接口......")
def f100():
login()
print("业务部门100数据接口......")

#各部门分别调用
f1()
f2()
f3()
f100()
  • 但是主管依然不满意,不过这一次他解释了为什么。主管说:写代码要遵循开放封闭原则,简单来说,已经实现的功能代码内部不允许被修改,但外部可以被扩展。如果将开放封闭原则应用在上面的需求中,那么就是不允许在函数f1 、f2、f3……f100的内部进行代码修改,但是可以在外部对它们进行扩展。

  • 第五天:已经没有时间让主管找别人来干这活了,他决定亲自上阵,使用装饰器完成这一任务,并且打算在函数执行后再增加个日志功能。主管的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def outer(func):
def inner():
print("认证成功!")
result = func()
print("日志添加成功")
return result
return inner

@outer
def f1():
print("业务部门1数据接口......")

@outer
def f2():
print("业务部门2数据接口......")
@outer
def f3():
print("业务部门3数据接口......")

@outer
def f100():
print("业务部门100数据接口......")

#各部门分别调用
f1()
f2()
f3()
f100()
  • 使用装饰器@outer,也是仅需对基础平台的代码进行拓展,就可以实现在其他部门调用函数API之前都进行认证操作,在操作结束后保存日志,并且其他业务部门无需对他们自己的代码做任何修改,调用方式也不用变。
  • 下章接着说装饰器分析