如果函数接受的参数是个对象列表,那么很有可能要在这个列表上多次迭代。例如,要分析美国texas旅游的人数。 假设数据集是由每个城市的游客数量构成的。现在要统计每个城市旅游的人数,占总游客数的百分比。
为此,可以编写一个函数,它会把所有的输入值加总,以求出每年的游客总数。然后,用每个城市的游客数除以总数,以求出该城市所占的比例。
def normalize(numbers):
total = sum(numbers)
result = []
for value in numbers:
percent = 100*value/total
result.append(percent)
return result
visits = [15,38,80]
percentages = normalize(visits)
print(percentages)
输出结果:
[11.278195488721805, 28.571428571428573, 60.150375939849624]
为了扩大函数的应用范围,现在把texas每个城市的游客数放在一份文件里面,然后从该文件中取数。由于这套流程还能够分析全世界的游客数量,所以可以定义一个生成器函数来实现,以便以后能把该函数重用到更为庞大的数据集上面。
def read_visits(data_path):
with open(data_path) as f:
for line in f:
yield int(line)
it = read_visits('my_numbers.txt')
percentages = normalize(it)
print(percentages)
输出结果:
[]
出现这种情况,是因为迭代器只能产生一轮结果,normalize函数里使用sum时,就算把迭代器使用了一次,第二次for value in numbers时,迭代器已经为空了。如果 改下代码 :
it = read_visits('my_numbers.txt')
print(list(it))
print(list(it))
输出结果:
[99, 23, 45, 23, 45, 90, 102, 33]
[]
通过上面的代码还能发现一个问题,就是:在已经用完的迭代器上继续迭代时,不会报错。所以这就很容易产生一些不被发现的问题。为了解决这个问题,我们可以先把迭代器转换成列表,然后在复制的列表上多次迭代。
def normalize_copy(numbers):
numbers = list(numbers)
total = sum(numbers)
result = []
for value in numbers:
percent = 100*value/total
result.append(percent)
return result
it = read_visits('my_numbers.txt')
percentages = normalize_copy(it)
print(percentages)
>>>
[21.52173913043478, 5.0, 9.782608695652174, 5.0, 9.782608695652174, 19.565217391304348, 22.17391304347826, 7.173913043478261]
这种写法也有问题,就是待复制的迭代器,可能有大量数据,复制迭代器时可能会耗尽内存。一种解决办法 是通过参数来接受另一个函数 ,那个函数每次调用后,都返回新的迭代器。
def normalize_func(get_iter):
total = sum(get_iter())
result = []
for value in get_iter():
percent = 100*value/total
result.append(percent)
return result
def read_visits(data_path):
with open(data_path) as f:
for line in f:
yield int(line)
# 注意这个lambda的用法,lambad 相当于套用了read_visits函数,每次调用都返回迭代器
percentages = normalize_func(lambda:read_visits('my_numbers.txt'))
print(percentages)
这种办法虽然 没错,但是像上面那样传递lambda函数,显得生硬,还有些难懂。还有个更好的办法 ,也能达到 同样 的效果,那就是新编一种实现迭代器协议的容器类。
python在for循环及相关表达式中遍历某种容器内容时,就要依靠这个迭代器协议。在执行类似for x in foo这样的语句时,python实际上会调用iter(foo).内置的iter函数又会调用foo.__iter__这个特殊方法。该方法必须返回迭代器对象,而那个迭代器本身 ,则实现了名为__next__的特殊方法。此后,for循环会在迭代器对象上反复调用内置的next函数,直至其耗尽并产生stopiteration异常。
这听起来比较复杂,但实际上,只要令自己的类把__iter__方法实现为生成器,就能满足上述要求。下面定义一个可以迭代的容器类,用来从文件中读取游客数据:
class ReadVisits(object):
def __init__(self, data_path):
self.data_path = data_path
def __iter__(self):
with open(self.data_path) as f:
for line in f:
yield int(line)
def normalize(numbers):
total = sum(numbers)
result = []
for value in numbers:
percent = 100*value/total
result.append(percent)
return result
path = 'my_numbers.txt'
visits = ReadVisits(path)
percentages = normalize(visits)
print(percentages)
输出结果:
[21.52173913043478, 5.0, 9.782608695652174, 5.0, 9.782608695652174, 19.565217391304348, 22.17391304347826, 7.173913043478261]
normalize函数中的sum方法会调用ReadVists.iter,从而得到新的迭代器对象,而调整数值所用的那个for循环,也会调用__iter__,从而得到另外一个新的迭代器对象,由于这两个迭代器会各自前进并走完一整轮,所以它们都可以看到全部的输入数据。这种方式的唯一缺点在于,需要多次读取输入数据。
接下来,我们需要改写下normalize函数,以确保调用者传进来的参数,并不是迭代器对象本身。
def normalize(numbers):
if iter(numbers) is iter(numbers):
raise TypeError('Must supply a container')
total = sum(numbers)
result = []
for value in numbers:
percent = 100*value/total
result.append(percent)
return result
注意函数的第2行,他的原理在于:如果把迭代器对象传给内置iter函数,那么此函数会把迭代器返回,反之,如果传给iter函数的是个容器类型的对象,那么iter函数则每次都会返回新的迭代器对象。于是,我们可以根据iter函数的这种行为来判断输入值是不是迭代器对象本身,如果是,就抛出TypeError错误。
如果我们不愿意像原来的normalize_copy那样,把迭代器中的输入数据完整的复制 一份,却想多次迭代这些数据,那么上面这种写法就比较理想。这个函数 能够处理list和ReadVisits这样的输入参数,因为它们都是容器。凡是遵从迭代器协议的容器类型都与这个函数兼容。
path = 'my_numbers.txt'
visits = [15,34,80]
normalize_defensive(visits) # no error
visits = ReadVisits(path)
normalize_defensive(visits) # no error
但是如果输入的参数是迭代器而不是容器,那么此函数就会抛出异常。
it = iter(visits)
normalize_defensive(it)
>>>
TypeError: Must supply a container
要点:
- 函数在输入的参数上多次迭代时要小心:如果参数是迭代器,因为迭代器只能迭代一次,那么可能会导致奇怪的行为并错失某些值。
- python的迭代器协议,描述了容器和迭代器应该如何与iter和next内置函数、for循环及相关表达式相互配合。
- 把__iter__方法实现为生成器,即可定义自己的容器类型。
- 想判断某个值 是迭代器还是容器,可以拿该值为参数,两次调用iter函数,若结果相同,则是迭代器,调用内置的next函数,即可令迭代器前进一点。
个人点评:这一节的内容比较多,核心内容是对于可迭代的参数要注意:一方面给函数提供的可迭代内容最好是一个容器而不是一个迭代器,另一方面编写函数时要考虑可能传进迭代器的问题。文章中那个类的用法我还没完全理解透彻,还需要再想想.