《编写高质量Python代码的59个有效方法》——第17条:在参数上面迭代时,要多加小心

第17条:在参数上面迭代时,要多加小心
如果函数接受的参数是个对象列表,那么很有可能要在这个列表上面多次迭代。例如,要分析来美国Texas旅游的人数。假设数据集是由每个城市的游客数量构成的(单位是每年百万人)。现在要统计来每个城市旅游的人数,占总游客数的百分比。
为此,需要编写标准化函数(normalization function)。它会把所有的输入值加总,以求出每年的游客总数。然后,用每个城市的游客数除以总数,以求出该城市所占的比例。

把各城市的游客数量放在一份列表里,传给该函数,可以得到正确结果。

为了扩大函数的应用范围,现在把Texas每个城市的游客数放在一份文件里面,然后从该文件中读取数据。由于这套流程还能够分析全世界的游客数量,所以笔者定义了生成器函数来实现此功能,以便稍后把该函数重用到更为庞大的数据集上面(参见本书第16条)。

奇怪的是,以生成器所返回的那个迭代器为参数,来调用normalize,却没有产生任何结果。

出现这种情况的原因在于,迭代器只能产生一轮结果。在抛出过StopIteration异常的迭代器或生成器上面继续迭代第二轮,是不会有结果的。

通过上面这段代码,我们还可以看出一个奇怪的地方,那就是:在已经用完的迭代器上面继续迭代时,居然不会报错。for循环、list构造器以及Python标准库里的其他许多函数,都认为在正常的操作过程中完全有可能出现StopIteration异常,这些函数没办法区分这个迭代器是本来就没有输出值,还是本来有输出值,但现在已经用完了。
为解决此问题,我们可以明确地使用该迭代器制作一份列表,将它的全部内容都遍历一次,并复制到这份列表里,然后,就可以在复制出来的数据列表上面多次迭代了。下面这个函数的功能,与刚才的normalize函数相同,只是它会把包含输入数据的那个迭代器,小心地复制一份:

这次再把调用生成器所返回的迭代器传给normalize_copy,就能产生正确结果了:

这种写法的问题在于,待复制的那个迭代器,可能含有大量输入数据,从而导致程序在复制迭代器的时候耗尽内存并崩溃。一种解决办法是通过参数来接受另外一个函数,那个函数每次调用后,都能返回新的迭代器。

使用normalize_func函数的时候,可以传入lambda表达式,该表达式会调用生成器,以便每次都能产生新的迭代器。

这种办法虽然没错,但是像上面这样传递lambda函数,毕竟显得生硬。还有个更好的办法,也能达成同样的效果,那就是新编一种实现迭代器协议(iterator protocol)的容器类。
Python在for循环及相关表达式中遍历某种容器的内容时,就要依靠这个迭代器协议。在执行类似for x in foo这样的语句时,Python实际上会调用iter(foo)。内置的iter函数又会调用foo.__iter__这个特殊方法。该方法必须返回迭代器对象,而那个迭代器本身,则实现了名为__next__的特殊方法。此后,for循环会在迭代器对象上面反复调用内置的next函数,直至其耗尽并产生StopIteration异常。
这听起来比较复杂,但实际上,只需要令自己的类把__iter__方法实现为生成器,就能满足上述要求。下面定义一个可以迭代的容器类,用来从文件中读取游客数据:

这种新型容器,可以直接传给原来那个normalize函数,无需再做修改,即可正常运行。

normalize函数中的sum方法会调用ReadVisits.__iter__,从而得到新的迭代器对象,而调整数值所用的那个for循环,也会调用__iter__,从而得到另外一个新的迭代器对象,由于这两个迭代器会各自前进并走完一整轮,所以它们都可以看到全部的输入数据。这种方式的唯一缺点在于,需要多次读取输入数据。
明白了ReadVisits这种容器的工作原理之后,我们可以修改原来编写的normalize函数,以确保调用者传进来的参数,并不是迭代器对象本身。迭代器协议有这样的约定:如果把迭代器对象传给内置的iter函数,那么此函数会把该迭代器返回,反之,如果传给iter函数的是个容器类型的对象,那么iter函数则每次都会返回新的迭代器对象。于是,我们可以根据iter函数的这种行为来判断输入值是不是迭代器对象本身,如果是,就抛出TypeError错误。

如果我们不愿意像原来的normalize_copy那样,把迭代器中的输入数据完整复制一份,但却想多次迭代这些数据,那么上面这种写法就比较理想。这个函数能够处理list和ReadVisits这样的输入参数,因为它们都是容器。凡是遵从迭代器协议的容器类型,都与这个函数相兼容。

如果输入的参数是迭代器而不是容器,那么此函数就会抛出异常。

要点
函数在输入的参数上面多次迭代时要当心:如果参数是迭代器,那么可能会导致奇怪的行为并错失某些值。
Python的迭代器协议,描述了容器和迭代器应该如何与iter和next内置函数、for循环及相关表达式相互配合。
把__iter__方法实现为生成器,即可定义自己的容器类型。
想判断某个值是迭代器还是容器,可以拿该值为参数,两次调用iter函数,若结果相同,则是迭代器,调用内置的next函数,即可令该迭代器前进一步。

时间: 2024-09-21 16:10:56

《编写高质量Python代码的59个有效方法》——第17条:在参数上面迭代时,要多加小心的相关文章

《编写高质量Python代码的59个有效方法》——导读

目 录 前 言 致 谢第1章 用Pythonic方式来思考 第1条:确认自己所用的Python版本 第2条:遵循PEP 8风格指南 第3条:了解bytes.str与unicode的区别第4条:用辅助函数来取代复杂的表达式第5条:了解切割序列的办法第6条:在单次切片操作内,不要同时指定start.end和stride第7条:用列表推导来取代map和f?ilter 第8条:不要使用含有两个以上表达式的列表推导第9条:用生成器表达式来改写数据量较大的列表推导第10条:尽量用enumerate取代ran

《编写高质量Python代码的59个有效方法》——第3条:了解bytes、str与unicode的区别

第3条:了解bytes.str与unicode的区别 Python 3有两种表示字符序列的类型:bytes和str.前者的实例包含原始的8位值:后者的实例包含Unicode字符. Python 2也有两种表示字符序列的类型,分别叫做str和unicode.与Python 3不同的是,str的实例包含原始的8位值:而unicode的实例,则包含Unicode字符. 把Unicode字符表示为二进制数据(也就是原始8位值)有许多种办法.最常见的编码方式就是UTF-8.但是大家要记住,Python 3

《编写高质量Python代码的59个有效方法》——第2条:遵循PEP 8风格指南

第2条:遵循PEP 8风格指南 <Python Enhancement Proposal #8>(8号Python增强提案)又叫PEP 8,它是针对Python代码格式而编订的风格指南.尽管可以在保证语法正确的前提下随意编写Python代码,但是,采用一致的风格来书写可以令代码更加易懂.更加易读.采用和其他Python程序员相同的风格来写代码,也可以使项目更利于多人协作.即便代码只会由你自己阅读,遵循这套风格也依然可以令后续的修改变得容易一些. PEP 8列出了许多细节,以描述如何撰写清晰的P

《编写高质量Python代码的59个有效方法》——第20条:用None和文档字符串来描述具有动态默认值的参数

第20条:用None和文档字符串来描述具有动态默认值的参数有时我们想采用一种非静态的类型,来做关键字参数的默认值.例如,在打印日志消息的时候,要把相关事件的记录时间也标注在这条消息中.默认情况下,消息里面所包含的时间,应该是调用log函数那一刻的时间.如果我们以为参数的默认值会在每次执行函数时得到评估,那可能就会写出下面这种代码. 两条消息的时间戳(timestamp)是一样的,这是因为datetime.now只执行了一次,也就是它只在函数定义的时候执行了一次.参数的默认值,会在每个模块加载进来

《编写高质量Python代码的59个有效方法》——第10条:尽量用enumerate取代range

第10条:尽量用enumerate取代range 在一系列整数上面迭代时,内置的range函数很有用. 对于字符串列表这样的序列式数据结构,可以直接在上面迭代. 当迭代列表的时候,通常还想知道当前元素在列表中的索引.例如,要按照喜好程度打印出自己爱吃的冰淇淋口味.一种办法是用range来做. 与单纯迭代f?lavor_list或是单纯使用range的代码相比,上面这段代码有些生硬.我们必须获取列表长度,并且通过下标来访问数组.这种代码不便于理解. Python提供了内置的enumerate函数,

《编写高质量Python代码的59个有效方法》——第9条:用生成器表达式来改写数据量较大的列表推导

第9条:用生成器表达式来改写数据量较大的列表推导 列表推导(参见本书第7条)的缺点是:在推导过程中,对于输入序列中的每个值来说,可能都要创建仅含一项元素的全新列表.当输入的数据比较少时,不会出问题,但如果输入的数据非常多,那么可能会消耗大量内存,并导致程序崩溃. 例如,要读取一份文件并返回每行的字符数.若采用列表推导来做,则需把文件每一行的长度都保存在内存中.如果这个文件特别大,或是通过无休止的network socket(网络套接字)来读取,那么这种列表推导就会出问题.下面的这段列表推导代码,

《编写高质量Python代码的59个有效方法》——第8条:不要使用含有两个以上表达式的列表推导

第8条:不要使用含有两个以上表达式的列表推导除了基本的用法(参见本书第7条)之外,列表推导也支持多重循环.例如,要把矩阵(也就是包含列表的列表,即二维列表)简化成一维列表,使原来的每个单元格都成为新列表中的普通元素.这个功能采用包含两个for表达式的列表推导即可实现,这些for表达式会按照从左至右的顺序来评估. 上面这个例子简单易懂,这就是多重循环的合理用法.还有一种包含多重循环的合理用法,那就是根据输入列表来创建有两层深度的新列表.例如,我们要对二维矩阵中的每个单元格取平方,然后用这些平方值构

《编写高质量Python代码的59个有效方法》——第16条:考虑用生成器来改写直接返回列表的函数

第16条:考虑用生成器来改写直接返回列表的函数 如果函数要产生一系列结果,那么最简单的做法就是把这些结果都放在一份列表里,并将其返回给调用者.例如,我们要查出字符串中每个词的首字母,在整个字符串里的位置.下面这段代码,用append方法将这些词的首字母索引添加到result列表中,并在函数结束时将其返回给调用者. 输入一些范例值,以验证该函数能够正常运作: index_words函数有两个问题. 第一个问题是,这段代码写得有点拥挤.每次找到新的结果,都要调用append方法.但我们真正应该强调的

《编写高质量Python代码的59个有效方法》——第15条:了解如何在闭包里使用外围作用域中的变量

第15条:了解如何在闭包里使用外围作用域中的变量 假如有一份列表,其中的元素都是数字,现在要对其排序,但排序时,要把出现在某个群组内的数字,放在群组外的那些数字之前.这种用法在绘制用户界面时候可能会遇到,我们可以用这个办法把重要的消息或意外的事件优先显示在其他内容前面. 实现该功能的一种常见做法,是在调用列表的sort方法时,把辅助函数传给key参数.这个辅助函数的返回值,将会用来确定列表中各元素的顺序.辅助函数可以判断受测元素是否处在重要群组中,并据此返回相应的排序关键字(sort key).