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

简介:

本节书摘来自华章社区《编写高质量Python代码的59个有效方法》一书中的第17条:在参数上面迭代时,要多加小心,作者[美]布雷特·斯拉特金(Brett Slatkin),更多章节内容可以访问云栖社区“华章社区”公众号查看

第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函数,即可令该迭代器前进一步。

相关文章
|
8天前
|
Python
python函数的参数学习
学习Python函数参数涉及五个方面:1) 位置参数按顺序传递,如`func(1, 2, 3)`;2) 关键字参数通过名称传值,如`func(a=1, b=2, c=3)`;3) 默认参数设定默认值,如`func(a, b, c=0)`;4) 可变参数用*和**接收任意数量的位置和关键字参数,如`func(1, 2, 3, a=4, b=5, c=6)`;5) 参数组合结合不同类型的参数,如`func(1, 2, 3, a=4, b=5, c=6)`。
13 1
|
9天前
|
监控 Python
Python中的装饰器:提升代码灵活性与可读性
在Python编程中,装饰器是一种强大的工具,能够提升代码的灵活性和可读性。本文将介绍装饰器的基本概念、使用方法以及实际应用场景,帮助读者更好地理解和利用这一功能。
|
11天前
|
人工智能 数据可视化 数据挖掘
【python】Python航空公司客户价值数据分析(代码+论文)【独一无二】
【python】Python航空公司客户价值数据分析(代码+论文)【独一无二】
|
15天前
|
机器学习/深度学习 算法 搜索推荐
Machine Learning机器学习之决策树算法 Decision Tree(附Python代码)
Machine Learning机器学习之决策树算法 Decision Tree(附Python代码)
|
10天前
|
缓存 监控 算法
优化Python代码性能的10个技巧
提高Python代码性能是每个开发者都需要关注的重要问题。本文将介绍10个实用的技巧,帮助你优化Python代码,提升程序的运行效率和性能表现。无论是避免内存泄漏、减少函数调用次数,还是使用适当的数据结构,都能在不同场景下发挥作用,使你的Python应用更加高效稳定。
|
2天前
|
数据安全/隐私保护 Python
Python中的装饰器:提升代码可读性与灵活性
Python中的装饰器是一种强大的工具,可以在不改变函数原有逻辑的情况下,为函数添加额外的功能。本文将介绍装饰器的基本概念和用法,并通过实例演示如何利用装饰器提升代码的可读性和灵活性,使代码更加简洁、易于维护。
|
2天前
|
BI 开发者 数据格式
Python代码填充数据到word模板中
【4月更文挑战第16天】
|
2天前
|
Python
python面型对象编程进阶(继承、多态、私有化、异常捕获、类属性和类方法)(上)
python面型对象编程进阶(继承、多态、私有化、异常捕获、类属性和类方法)(上)
25 0
|
2天前
05-python之函数-函数的定义/函数的参数/函数返回值/函数说明文档/函数的嵌套使用/函数变量的作用域
05-python之函数-函数的定义/函数的参数/函数返回值/函数说明文档/函数的嵌套使用/函数变量的作用域
|
4天前
|
缓存 算法 Python
优化Python代码的十大技巧
本文介绍了十种优化Python代码的技巧,涵盖了从代码结构到性能调优的方方面面。通过学习和应用这些技巧,你可以提高Python程序的执行效率,提升代码质量,以及更好地应对复杂的编程任务。

热门文章

最新文章