Sentence类第1版:单词序列我们要实现一个 Sentence 类,以此打开探索可迭代对象的旅程。我们向这个类的构造方法传入包含一些文本的字符串,然后可以逐个单词迭代。第 1 版要实现序列协议,这个类的对象可以迭代,因为所有序列都可以迭代——这一点前面已经说过,不过现在要说明真正的原因。
示例 14-1 定义了一个 Sentence 类,通过索引从文本中提取单词。
示例 14-1 sentence.py:把句子划分为单词序列
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text) ➊
def __getitem__(self, index):
return self.words[index] ➋
def __len__(self): ➌
return len(self.words)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text) ➍
❶ re.findall 函数返回一个字符串列表,里面的元素是正则表达式的全部非重叠匹配。
❷ self.words 中保存的是 .findall 函数返回的结果,因此直接返回指定索引位上的单词。
❸ 为了完善序列协议,我们实现了 __len__ 方法;不过,为了让对象可以迭代,没必要实现这个方法。
❹ reprlib.repr 这个实用函数用于生成大型数据结构的简略字符串表示形式。4
4首次使用 reprlib 模块是在 10.2 节。
默认情况下,reprlib.repr 函数生成的字符串最多有 30 个字符。Sentence 类的用法参见示例 14-2 中的控制台会话。
示例 14-2 测试
Sentence实例能否迭代
>>> s = Sentence('"The time has come," the Walrus said,') # ➊
>>> s
Sentence('"The time ha... Walrus said,') # ➋
>>> for word in s: # ➌
... print(word)
The
time
has
come
the
Walrus
said
>>> list(s) # ➍
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
❶ 传入一个字符串,创建一个 Sentence 实例。
❷ 注意,__repr__ 方法的输出中包含 reprlib.repr 方法生成的 ...。
❸ Sentence 实例可以迭代,稍后说明原因。
❹ 因为可以迭代,所以 Sentence 对象可以用于构建列表和其他可迭代的类型。
在接下来的几页中,我们还要开发其他 Sentence 类,而且都能通过示例 14-2 中的测试。不过,示例 14-1 中的实现与其他实现都不同,因为这一版 Sentence 类也是序列,可以按索引获取单词:
>>> s[0] 'The' >>> s[5] 'Walrus' >>> s[-1] 'said'
所有 Python 程序员都知道,序列可以迭代。下面说明具体的原因。
iter函数解释器需要迭代对象 x 时,会自动调用 iter(x)。
内置的 iter 函数有以下作用。
(1) 检查对象是否实现了 __iter__ 方法,如果实现了就调用它,获取一个迭代器。
(2) 如果没有实现 __iter__ 方法,但是实现了 __getitem__ 方法,Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。
(3) 如果尝试失败,Python 抛出 TypeError 异常,通常会提示“C object is not iterable”(C 对象不可迭代),其中 C 是目标对象所属的类。
任何 Python 序列都可迭代的原因是,它们都实现了 __getitem__ 方法。其实,标准的序列也都实现了 __iter__ 方法,因此你也应该这么做。之所以对 __getitem__ 方法做特殊处理,是为了向后兼容,而未来可能不会再这么做(不过,写作本书时还未弃用)。
11.2 节提到过,这是鸭子类型(duck typing)的极端形式:不仅要实现特殊的 __iter__ 方法,还要实现 __getitem__ 方法,而且 __getitem__ 方法的参数是从 0 开始的整数(int),这样才认为对象是可迭代的。
在白鹅类型(goose-typing)理论中,可迭代对象的定义简单一些,不过没那么灵活:如果实现了 __iter__ 方法,那么就认为对象是可迭代的。此时,不需要创建子类,也不用注册,因为 abc.Iterable 类实现了 __subclasshook__ 方法,如 11.10 节所述。下面举个例子:
>>> class Foo: ... def __iter__(self): ... pass ... >>> from collections import abc >>> issubclass(Foo, abc.Iterable) True >>> f = Foo() >>> isinstance(f, abc.Iterable) True
不过要注意,虽然前面定义的 Sentence 类是可以迭代的,但却无法通过 issubclass (Sentence, abc.Iterable) 测试。
从 Python 3.4 开始,检查对象
x能否迭代,最准确的方法是:调用iter(x)函数,如果不可迭代,再处理TypeError异常。这比使用isinstance(x, abc.Iterable)更准确,因为iter(x)函数会考虑到遗留的__getitem__方法,而abc.Iterable类则不考虑。
迭代对象之前显式检查对象是否可迭代或许没必要,毕竟尝试迭代不可迭代的对象时,Python 抛出的异常信息很明确:TypeError: 'C' object is not iterable。如果除了抛出 TypeError 异常之外还要做进一步的处理,可以使用 try/except 块,而无需显式检查。如果要保存对象,等以后再迭代,或许可以显式检查,因为这种情况可能需要尽早捕获错误。
下一节详述可迭代的对象和迭代器之间的关系。