附录A.1. Python中的类序列化、迭代器及生成器
本章的内容有实践价值,但稍稍有点深入,在简单的应用程序实现当中也不是非用不可,跟后续章节关联度也很低。心急的读者可以先略过不读。
对于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.pyclass 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]= 67651, 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.pyclass UserList(list): passa = 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)= 10a[3]= 熊孩子
看上去,UserList用起来跟list一模一样。这没有意义,我们通过继承设计新的类,总是要跟父类有些区别。我们来给UserList加点功能。
#userlist2.pyclass 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)= 10a[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.pyclass 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 cfor 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.pydef 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.pydef 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 cprint(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:
我们看到了执行结果和相关用法,跟上节所描述的可迭代对象十分相似。上述代码中的FibonacciGenerator(10)函数可以认为是一个生成器。所谓生成器,就是一段伪装成“序列”的“程序”,它是可迭代的。外部程序直接或间接地通过next()函数列举生成器的元素时,这段“程序”就会被执行,它通过yield语句向外部程序提供一个“内部”元素。每次执行yield,都会导致生成器“程序”被挂起暂停,直到外部程序试图列举生成器的下一个元素时,生成器“程序”从断点处继续执行。生成器程序全部执行完成会执行return语句,这意味着它已列举完全部“内部”元素,迭代将停止。这个函数的最后作者没有写出那个return语句,但可以认为解释器会自动加上一个。
FibonacciGenerator就是一个函数对象。由于这个函数对象内部包括yield关键字,所以该函数被“执行”时并不会立即执行,而是返回一个生成器对象,上面的执行结果证明了这一点。print(g)的输出结果为。当这个生成器对象被外部程序迭代时,其中的代码才会真正运行。
上述的生成器是一个函数对象,还有更简单的:
#simplegrr.pyg1 = [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))
执行结果
[1, 9, 25, 49, 81] [1, 9, 25, 49, 81]
g1的格式我们已经见过多次,这是所谓的列表推导,它生成一个结果列表。而生成全部的列表,将花费大量的CPU时间和内存空间。g2跟g1的区别在于没有使用列表推导的方括号,而是使用圆括号。所以,g2的类型是,它的本质是生成器,也就是伪装成“序列”的程序,只有外部程序试图迭代g2内的元素时,该程序才会真正执行。list(g2)完成了对生成器g2的迭代,生成一个包含g2内全部元素的列表。
练习
A.1-1 设计一个生成器函数,该生成器函数生成字母'a', 'b','c','d'的全排列,其生成顺序按照字母表排序。用一个for循环迭代这个生成器函数,打印全部元素。
A.1-2 设计一个序列化类。实例化该类并使用下标访问其全部元素。其中,第i个元素的值为斐波那契序列前i项的和,i的最大值限定为30。