【Python】中的类序列化,迭代器和生成器

附录A.1. Python中的类序列化、迭代器及生成器

本章的内容有实践价值,但稍稍有点深入,在简单的应用程序实现当中也不是非用不可,跟后续章节关联度也很低。心急的读者可以先略过不读。

本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者"海洋饼干叔
叔";本文不允许以纸质及电子出版为目的进行抄摘或改编。
1.《Python编程基础及应用》,陈波,刘慧君,高等教育出版社。免费授课视频 Python编程基础及应用
2.《Python编程基础及应用实验教程》, 陈波,熊心志,张全和,刘慧君,赵恒军,高等教育出版社Python编程基础及应用实验教程
3. 《简明C及C++语言教程》,陈波,待出版书稿。免费授课视频

对于list、tuple、str这样的类型,我们可以通过[]来访问其特定下标的元素(item);可以通过len()函数来询问其内部包含的元素个数;还可以通过del来删除指定位置的元素。

我们能否设计一个类,让它象序列一样工作呢?或者说,我们能否实现一个可以当序列用的自定义类型?通过实现一些特殊方法,可以办到。

A.1.1 斐波那契数列

斐波那契数列 - Fibonacci sequence也叫兔子序列,最早用于预测一定周期后野外的兔子的数量。该序列可用一个分段递归函数来描述。
在这里插入图片描述
简单计算可知,F(1) = 1, F(2) = 1, F(3) = 2, F(4) = 3, 5, 8,13,21… 简单地说,从第三项开始,每一项等于前两项的和。

A.1.2 斐波那契序列类

下述代码通过实现几个特殊方法实现了序列化的斐波那契类。

#fibseq1.py
class Fibonacci:
    def __init__(self):
        self.seq = [0,1,1]                  #序列第1项,第2项为1
        self.maxKey = 10000

    def computeTo(self,key):
        for idx in range(len(self.seq), key + 1):
            v = self.seq[idx - 1] + self.seq[idx - 2]
            self.seq.append(v)

    def __getitem__(self,key):
        if not isinstance(key,int):         #判断是否int类型
            raise TypeError
        if key <=0 or key > self.maxKey:    #数列不含第0项,最大10000项
            raise IndexError
        if key > len(self.seq):         
            self.computeTo(key)             #计算序列的前key项
        return self.seq[key]

    def __setitem__(self,key,value):        #key为下标,value为值
        if not isinstance(key,int):         #判断是否int类型
            raise TypeError 
        if key <=0 or key > self.maxKey:    #数列不含第0项,最大10000项
            raise IndexError
        if key > len(self.seq):
            self.computeTo(key)             #计算序列的前key项
        self.seq[key] = value

    def __len__(self):
        return self.maxKey      #返回最大项数10000作为长度

f = Fibonacci()                 #实例化Fibonacci类
print("f[20]=",f[20])           #取值,导致f.__getitem__(20)被执行
f[10] = "熊孩子"                 #赋值,导致f.__setitem__(10,"熊孩子")被执行  
for i in range(1,21):
    print(f[i],end=",")         #取值,导致f.__getitem__(i)被执行
print("")                       #换行
print("Length of f:",len(f))    #len(f)导致f.__len__()被执行

执行结果

f[20]= 6765
1, 1, 2, 3, 5, 8, 13, 21, 34, 熊孩子, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 
Length of f: 10000

斐波那契数列这么设计,并没有多大的实践意义。这样做,是希望以读者熟悉的方式向大家介绍序列化类型的方法。当我们以f对象为参数执行len()函数时,f对象的_ _ len _ _ ()函数会被执行。斐波那契数列是个无穷数列,这里,作者规定了一个极限,最多10000项。

当我们对f[20]进行取值时,f对象的_ _ getitem _ _()函数将被执行,key,即下标参数为20。该函数内,首先通过isinstance判断key是否为int,如果不是,引发TypeError异常。然后,判断key的取值,如果<=0或者>10000,引发IndexError异常。接下来,检查key所对应的斐波那契项是否已计算过,如果没有,执行computeTo()成员函数逐项计算。最后,返回self.seq[key]作为结果。

当我们对f[10]进行赋值时,f对象的_ _ setitem _ _()特殊方法被执行。该方法先对key进行类型和下标取值检查,并确保序列已计算至第key项。然后对self.seq[key]进行赋值。当然,真实的斐波那契数列是不需要进行赋值的,所以这里给了一个“熊孩子”的值以吸引读者的注意:请数一数,确认“熊孩子”在序列中的下标。

由于不准备支持对对象内元素的删除,这里没有实现_ _ delitem _ _()特殊方法。

可以想象,对f[0]的访问将触发IndexError异常,对f[“ABC”]的访问将触发TypeError异常。此外,缺乏编程训练的读者可能对computeTo()函数的实现感到疑惑。设定一个比较小的key值,比如5,把自己当成计算机,拿一张纸,一支笔,人肉模拟一下代码的执行过程,是理解复杂程序的好办法。

A.1.3 从list继承

面向对象的方法论告诉我们,从一个类那里继承出一个子类,子类将自动获得父类的全部特性。那么,如果从一个序列类继承,继承类不就自动是一个序列类型了吗?这个思路可以有。

#userlist1.py
class UserList(list):
    pass

a = UserList()
a.extend([0,1,2,3,4,5,6,7,8,9])
a[3] = "熊孩子"
print("a[2]=",a[2],"len(a)=",len(a))
print("a[3]=",a[3])

执行结果

a[2]= 2 len(a)= 10
a[3]= 熊孩子

看上去,UserList用起来跟list一模一样。这没有意义,我们通过继承设计新的类,总是要跟父类有些区别。我们来给UserList加点功能。

#userlist2.py
class UserList(list):
    def __init__(self,*args):       #args吸收除self之外的全部参数
        super().__init__(*args)     #执行父类的构造函数
        self.iCounter = 0

    def __getitem__(self, idx):     #该函数在应用[]按下标取值时被执行
        self.iCounter += 1
        return super().__getitem__(idx)

a = UserList()
a.extend([0,1,2,3,4,5,6,7,8,9])
a[3] = "熊孩子"
print("a[2]=",a[2],"len(a)=",len(a))
print("a[3]=",a[3])
print("a.iCounter=",a.iCounter)

执行结果

a[2]= 2 len(a)= 10
a[3]= 熊孩子
a.iCounter= 2

容易看出,通过重载list父类的_ _ getitem _ _()函数,UserList可以对列表[]取值的次数进行计数。在本例中,a[2],a[3]两次取值,故iCounter值为2。同样地,tuple,str,bytearray这些序列类都可以被继承,实现类似目的。

A.1.4 可迭代Fibonacci数列

列表,元组,数值列表(range list),字典等都是可迭代的(iterable)。我们可以使用for x in X来列举可迭代对象X内部的全部元素。下面我们将把斐波那契数列类可迭代化。直接上代码:

#fibiter.py
class Fibonacci:
    def __init__(self, max):
        self.a = 1
        self.b = 1
        self.idx = 0
        self.maxIdx = max

    def __iter__(self):
        return self

    def __next__(self):
        self.idx += 1
        if self.idx == 1:
            return 1
        elif self.idx == 2:
            return 1
        elif self.idx > self.maxIdx:
            raise StopIteration
        else:
            c = self.a + self.b
            self.a, self.b = self.b, c
            return c
            
for x in Fibonacci(10):
    print(x, end=",")
    
print("")
it = Fibonacci(10)
for x in range(10):
    print(next(it), end=",")

print("")
print(list(Fibonacci(10)))

执行结果

1,1,2,3,5,8,13,21,34,55,
1,1,2,3,5,8,13,21,34,55,
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Python解释器内部迭代一个对象的过程大致可以描述成下述模样:当代码试图迭代一个对象时,解释器会调用该对象的_ _ iter _ ()特殊方法,试图获得一个可迭代对象 - iterable object。这个可迭代对象预期应该实现了 _ next _ ()方法,每执行一次该方法,就会返回一个“内部”元素,当可迭代对象的“内部”元素已经全部列举完毕后, _ next _ _()方法引发一个StopIteration异常,外部程序捕获该异常后,停止迭代。在这一过程中,StopIteration导常的捕获是由解释器自动进行的,编程者无法也不必进行干预。

解释器对上述_ _ iter _ _()及 _ _ next _ _ ()特殊方法的调用也是隐含的,程序员无法也不必要干预。

在上述代码中,Fibonacci类的_ _ iter _ ()函数返回对象自身做为可迭代对象。Fibonacci对象内部的idx属性记录了当前已经被列举的数列项数,外部程序每次执行 _ next _ _()函数时,该函数会生下一项并返回。提示,a,b属性记录了最近两项被列举的数列项的值,而根据Fibonacii的定义,下一项正好等于 a + b。

Fibonacci是无穷数列,但迭代必须有尽头。所以上述代码设定了一个self.maxIdx属性,当已迭代项数达到self.maxIdx时,触发StopIteration异常,终止迭代。

上述代码首先用for循环来迭代Fibonacci(10)对象。然后,代码又连续 10次执行next(it),而每次next(it)的执行,都间接调用执行it._ _ next _ _()特殊方法,这算是一种手工迭代方法。最后,代码将Fibonacci(10)转换成列表,这也是一种间接迭代可迭代对象的方法。

A.1.5 生成器

还是Fibonacci,毕竞我们这里要讨论是Python,而不是数学,所以我们尽量使用大家熟悉的数学工具。

#fibgrr.py
def FibonacciGenerator(n):
    assert n > 2   #为代码简单,作者要求n>2
    print(1,end=",")
    print(1,end=",")
    a = b = 1
    for i in range(3,n+1):
        c = a + b
        a,b = b,c
        print(c,end=",")

FibonacciGenerator(10)

执行结果

1,1,2,3,5,8,13,21,34,55,

这段代码很简单,生成并打印Fibonacci数列的前十项,n代表要打印的项数。在之前,我们已经学会了将一个Fibonacci类对象变成可迭代对象的方法。那么这里,负责生成Fibonacci数列的是一个函数,能否也将这个函数变成可迭代的呢?我们要一项,它就给一项?解决方法很简单,把print()变成yield即可。

#fibfuncitr.py
def FibonacciGenerator(n):
    assert n > 2  # 为代码简单,作者要求n>2
    yield 1
    yield 1
    a = b = 1
    for i in range(3, n + 1):
        c = a + b
        a, b = b, c
        yield c

print(list(FibonacciGenerator(10)))

for x in FibonacciGenerator(10):
    print(x, end=",")

print("")  # 换行
g = FibonacciGenerator(10)
for i in range(10):
    print(next(g),end=":")
    
print("")
print(FibonacciGenerator)
print(g)

执行结果

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
1,1,2,3,5,8,13,21,34,55,
1:1:2:3:5:8:13:21:34:55:
<function FibonacciGenerator at 0x000002665A0EC1E0>
<generator object FibonacciGenerator at 0x000002665BD1B750>

我们看到了执行结果和相关用法,跟上节所描述的可迭代对象十分相似。上述代码中的FibonacciGenerator(10)函数可以认为是一个生成器。所谓生成器,就是一段伪装成“序列”的“程序”,它是可迭代的。外部程序直接或间接地通过next()函数列举生成器的元素时,这段“程序”就会被执行,它通过yield语句向外部程序提供一个“内部”元素。每次执行yield,都会导致生成器“程序”被挂起暂停,直到外部程序试图列举生成器的下一个元素时,生成器“程序”从断点处继续执行。生成器程序全部执行完成会执行return语句,这意味着它已列举完全部“内部”元素,迭代将停止。这个函数的最后作者没有写出那个return语句,但可以认为解释器会自动加上一个。

FibonacciGenerator就是一个函数对象。由于这个函数对象内部包括yield关键字,所以该函数被“执行”时并不会立即执行,而是返回一个生成器对象,上面的执行结果证明了这一点。print(g)的输出结果为<generator object…>。当这个生成器对象被外部程序迭代时,其中的代码才会真正运行。

上述的生成器是一个函数对象,还有更简单的:

#simplegrr.py
g1 = [x**2 for x in range(1,10,2)]
g2 = (x**2 for x in range(1,10,2))
print(type(g1),g1)
print(type(g2),list(g2))

执行结果

<class 'list'> [1, 9, 25, 49, 81]
<class 'generator'> [1, 9, 25, 49, 81]

g1的格式我们已经见过多次,这是所谓的列表推导,它生成一个结果列表。而生成全部的列表,将花费大量的CPU时间和内存空间。g2跟g1的区别在于没有使用列表推导的方括号,而是使用圆括号。所以,g2的类型是,它的本质是生成器,也就是伪装成“序列”的程序,只有外部程序试图迭代g2内的元素时,该程序才会真正执行。list(g2)完成了对生成器g2的迭代,生成一个包含g2内全部元素的列表。

为了帮助更多的年轻朋友们学好编程,作者在B站上开了两门免费的网课,一门零基础讲Python,一门零基础C和C++一起学,拿走不谢!

简洁的C及C++
由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造
Python编程基础及应用
由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造

如果你觉得纸质书看起来更顺手,目前Python有两本,C和C++在出版过程中。

Python编程基础及应用

Python编程基础及应用实验教程
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值