Python中的迭代器、生成器、装饰器

版权声明:原创文章转载请注明出处~ https://blog.csdn.net/PecoHe/article/details/90173789

13.1 迭代

13.1.1 可迭代对象与迭代器

序列,字典与集合等类型都可以看做是一个容器,用来存放多个元素,并且每种类型都提供了相应的方法来操作容器中的元素。这些类型都可以用在for循环中进行遍历,依次获取容器中的每一个元素。这些可以用在for循环中进行遍历的对象称为可迭代对象
可迭代对象类型在collections.abc.Iterable类中定义,因此往往可以通过判断某对象是否为Iterable实例的方式来判断该对象是否为可迭代对象

Iterable类是一个抽象基类(父类),用来定义可迭代对象的规范,即可迭代对象应该具有的公共特征。该接口中定义了一个用来表示规范的方法(抽象方法):

def __iter__(self)

该方法用来返回一个迭代器,用来依次访问容器中的数据。所谓迭代器,就是一个数据流对象,可以连续返回流中数据。可以说,可迭代对象能够在for循环中遍历,底层靠的就是迭代器来实现的。
对于迭代器类型,是在collections.abc.Iterator中定义。该类型也是一个抽象基类,用来定义迭代器对象的规范,Iterator继承Iterable类型, 两个重要的方法如下:

def __next__(self)

返回下一个元素,当没有元素时,产生StopIteration异常。

def __iter__(self)

从父类Iterable继承的方法,意义与Iterable类中的__iter__方法相同,即返回一个迭代器。因为当前对象就是迭代器对象,所以在该方法中,只需要简单的返回当前对象即可:
return self
正规来说,作为可迭代对象,需要实现__iter__方法或者__getitem__方法。

def __iter__(self)

该方法用来返回一个迭代器,用来遍历容器中的元素。

def __getitem__(self, key)
该方法用来返回self[key]的结果。

说明:可迭代对象也可以不实现__iter__方法,而是实现__getitem__方法,这里大家可能会造成困扰。因为如果这样做,就与抽象基类Iterable中定义的行为不一致(Iterable中仅定义了__iter__方法)。实际上,这是由于历史原因造成的,保留__getitem__方法是为了做到兼容以前的实现。

for循环内部的工作方式
在使用for循环来遍历容器中的元素时,底层会调用iter函数,来返回容器的迭代器。iter函数首先检查类是否实现__iter__方法,如果实现,则调用该方法,返回迭代器。否则,会创建一个迭代器,然后调用__getitem__方法依次获取元素。如果以上两个方法都不存在,则表示当前对象并不是一个可迭代对象,因此也就不能放在for循环中使用(产生错误)。
在遍历容器中的元素时,会调用迭代器的__next__方法,返回下一个元素,如此反复执行。当没有可用的元素时,迭代器会产生StopIteration异常,而这个异常,会由for循环内部进行捕获,无需我们显式处理。

一次性的迭代器
对于迭代器,如果只想获得下一个元素而不是遍历,可以调用__next__方法而实现,不过,我们往往不会直接调用Python中的特殊方法,内建函数next可以帮助我们获取迭代器的下一个元素,next在内部会调用迭代器的__next__方法。同时,我们需要注意,迭代器只能迭代一轮,也就是说,如果容器中已经没有可用的元素,则迭代器就不能再次使用了(再次调用next函数获取下一个元素会产生异常),如果想要重新进行迭代,需要再次调用iter函数获取一个新的迭代器对象。

13.1.2 自定义迭代类型

除了Python语言提供的内建可迭代类型外,我们也可以自定义迭代类型。不过,自定义的迭代类型也可以不继承Iterable或者Iterator,只需要满足抽象基类定义的规范,这样的类型就会成为Iterable或者Iterator的子类型。即:如果自定义可迭代对象类型,需要实现__iter__方法,如果自定义迭代器,除了实现父类中的__iter__方法外,额外实现__next__方法。

13.1.3 迭代合体

为什么Python在实现序列等类型时,将其设计为可迭代对象,但是却不是迭代器呢?或者说,我们是否可以将二者何为一体,让序列等类型作为可迭代对象的同时,也是一个迭代器类型呢?如果二者可以合体,上例的程序就可以不用再定义一个新的迭代器类,直接在可迭代对象的__iter__方法中返回自身(self),然后同时实现__next__方法不就可以了吗?
尝试合体。

13.2. 生成器

13.2.1. 需求背景

假设我们有如下的需求:计算并能够返回1 ~ 100内所有自然数的平方。根据以前所学习的知识,我们可以轻松实现:

def compute():
    result = []
    for i in range(1, 101):
        result.append(i * i)
    return result

再简单一点,我们可以通过列表推导式来实现:

def compute():
    return [i * i for i in range(1, 101)]

这样做是没有问题的,但是,我们现在将问题升级,如果要计算的数值不是100个,而是一个海量甚至无限的数据集(例如,全体正整数的平方值),我们将大量的计算结果都存储一个列表当中,这就会占用大量的内存空间,导致程序运行非常缓慢。
Python语言中提供的生成器就类似于上述的处理方式,顾名思义,生成器类似于生产数据的工厂。在工作方式上,生成器不会预先准备好所有的数据,而是在需要时,每次仅生成一个数据。这样,在处理大量数据时,也不会占用大量的内容空间。我们可以使用两种方式来创建生成器:

  • 生成器表达式
  • 生成器函数

13.2.2 生成器表达式

生成器表达式的语法非常简单,只需要将列表推导式的中括号改成小括号就可以了。我们还是以之前求平方值的程序为例。
生成器与迭代器有什么关系?

13.2.3 生成器函数

当我们需要的数据集计算比较简单时,使用生成器表达式是一个不错的选择。但如果数据集的计算方式较为复杂,我们也可以使用生成器函数来实现。在生成器函数中,我们使用yield关键字来生成一个值,并将该值返回给生成器的调用端,格式为:

yield [生成的值]

这种语法,我们称为yield表达式。其中,生成的值是可选的。
程序:使用生成器函数,来产生斐波那契数列。
生成器函数与普通的函数非常相似,从形式上,只是使用yield代替了return而已。不过,我们不要小看yield关键字,如果函数中出现该关键字,则表示该函数为生成器函数,与普通的函数还是有很大差异的,说明如下:

  • 生成器函数更像是一个类的定义,而不是函数的定义。因此,在第11行语句处,我们是创建了一个生成器的对象,而不是调用函数,所以,此时生成器函数体不会执行,如果是普通函数,则会执行函数体。
  • 当调用生成器的方法时,生成器函数体才会执行。方法调用可以是显式调用:
    假设gen为调用生成器函数而返回的生成器对象。
    gen.next()
    当然,也可以是隐式调用。例如,在for循环,调用内建函数next等场合时,也会隐式调用生成器函数的__next__方法。在上例中,for循环内会隐式调用生成器的__next__方法(第12行)。当遇到yield表达式时,会暂停执行,并将yield生成的值返回给生成器的调用端。同时,生成器函数会保存当前的运行时环境,局部变量等信息都会保留,不会丢失。如果是普通函数,则函数内定义的局部变量值会丢失,每次调用函数,局部变量都会具有新的初始值。
  • 当再次调用生成器方法时,会在上次yield暂停的位置处,继续执行,直到遇到下一个yield表达式,产生下一个值,或者遇到return语句,产生异常而结束。如果生成器函数中没有return语句,则函数体执行结束,也会隐式执行return返回。
    yield表达式
  • 当调用生成器方法,生成器首次执行时,会在yield处暂停,然后将yield生成的值返回至生成器调用端。当再次调用生成器的方法,就会从之前处于暂停的位置处(即yield所在的位置)恢复执行,此时,yield表达式就会获得值,具体的值由生成器调用端传递。如果调用的是send方法,则yield表达式的值为send方法调用时所传递的实际参数值,如果调用的是__next__方法,则yield表达式的值为None。

如果在生成器函数第一次执行前,调用send方法,则send方法的参数必须为None。因为生成器函数体在第一次时执行时,会暂停在yield位置,yield表达式尚未获得任何值,直到第二次执行生成器函数,yield表达式才会获得值,因此,第一次执行时,send方法传递任何数值都没有意义。
通过send方法,我们就可以在生成器调用端向生成器传递值,同时,生成器调用端又可以获取生成器中使用yield所生成的值,从而实现二者之间的数据关联。

13.3 装饰器

13.3.1 闭包

闭包是指在函数体内部,访问到其外围函数中定义的变量。则对于内部函数,我们就称为闭包。从定义可知,闭包是发生在函数嵌套的上下文环境中。从代码实现的角度,我们通常会定义嵌套函数,然后将内部函数作为外围函数的返回值,返回给函数的调用端,供调用端多次调用执行。
闭包的优点就在于:

  • 其访问外围函数中定义的变量,不会随着内部函数的执行结束而销毁,当我们下次再次执行内部函数时,所引用的外围函数中定义的变量依然会保留上次的值。这样,函数执行时的状态就得以保存,以便于调用端可以多次使用。
  • 内部函数定义在外围函数中,作为外围函数的返回值。因此,内部函数名称就是一个局部名称(定义在局部命名空间中),不会被外界直接访问,这样就不会对外围函数的外部带来命名冲突等影响,当内部函数名称发生改动时,也不会带来任何问题,从而具有封装的特征。
    现在我们来实现上班的函数,函数的功能是能够记录员工是第几次上班。如果使用以前函数的形式,则无法实现我们的需求,因为函数内定义的变量(状态)无法在函数调用后得到保存。
    其实,我们也可以使用定义类的形式,来实现闭包的效果。

13.3.2 需求背景

我们以员工上班与下班为例。
需求变更,要求在上下班增加签到日期与时间。
进行修改。

13.3.3 使用装饰器

装饰器,用来处理被其所装饰的函数,然后将该函数返回。从实现的角度讲,装饰器本身也是一个函数,其参数用来接收另外一个函数,然后返回一个函数,返回的函数与参数接收的函数可以相同,也可以不同。

def decorator(函数1):
    return 函数2

其实装饰器使用的,就是闭包的思想。当定义好一个装饰器(函数)后,就可以使用如下的语法来修饰另外一个函数:

# decorator为装饰器函数名。
@decorator
def fun():
    pass

经过这样修饰后,在任意调用fun函数的位置:
fun()
就相当于执行:
fun = decorator(fun)
这与我们之前使用闭包的形式是一样的。我们将这种使用@修饰函数的形式称为装饰器的语法糖(syntactic sugar)。所谓语法糖,就是某种特殊的语法,用来方便程序员使用。

说明:装饰器本身除了是函数以外,也可以是一个类。
装饰器的优势在于:我们可以在不修改现有函数的基础上,对其进行的扩展,增加额外的功能。一个装饰器可以用来修饰多个函数,这样,我们就可以避免代码的重复,有利于程序的维护。

装饰器优化
我们还可以对之前实现的装饰器进行如下的优化:

  • 装饰器使用参数接收的函数,可能是具有返回值的,因此,我们在内部函数中,不应该仅仅只是调用参数接收的函数,还应该将参数接收函数的返回值作为内部函数的返回值而返回。
  • 装饰器可能会用来修饰很多函数(对很多函数进行功能扩展),例如,对上班与下班,开会与散会等很多函数记录时间。但是,每个函数定义的参数数量可能不尽相同,例如,我们当前的上下班函数定义了一个参数,但是,以后新增的开会与散会函数可能会定义两个或更多参数,也可能没有参数。

叠加装饰器
在开发项目时,因为需求的不确定性,业务的不断发展,功能的不断扩充等诸多原因,我们很难做到一步到位,因此,我们可能对现有功能不止一次的进行扩展。此时,我们可以对装饰器进行叠加,以便于对功能进行多次扩展。格式如下:

@decorator1
@decorator2
def fun():
    pass

叠加修饰后,就相当于执行:

fun = decorator2(fun)
fun = decorator1(fun)

13.3.4 含有参数的装饰器

我们使用装饰器完美的实现了需求。但是,输出的结果含有微秒,这可能并不是所有人都想要的。对于签到时间,我们只需要精确到分钟就够用了。因此,我们提供一种功能:让客户端指定输出格式。
如果让客户端指定输出格式,我们应当通过参数传递到装饰器中,然而,装饰器的参数已经预定好了,是用来接收我们使用@修饰的函数。因此,我们只能另辟蹊径。
方法就是,我们再定义一层函数,用来接收装饰器的参数,然后返回装饰器。这样,返回的装饰器就会停留在我们需要修饰的函数上,继续修饰对应的函数。

13.3.5 保留函数信息

我们可以使用functools.wraps来解决,wraps接收一个函数,可以将接收函数的元信息复制到其所修饰的函数中。

13.3.6 类装饰器

我们在使用闭包时,也介绍了使用定义类来实现同样功能的方式。既然装饰器使用的是闭包的思想,那装饰器是否也可以使用类的方式来实现呢?
实际上,装饰器不仅可以是函数,只要是可调用的对象,就能够成为装饰器。在Python中,类也是对象(一切皆为对象)。而调用类时,返回的其实就是类所创建的对象。因此,类也可以成为装饰器。

猜你喜欢

转载自blog.csdn.net/PecoHe/article/details/90173789