如果函数接受的参数是个对象列表,那么很有可能要在这个列表上多次迭代。例如,要分析美国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

要点:

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

个人点评:这一节的内容比较多,核心内容是对于可迭代的参数要注意:一方面给函数提供的可迭代内容最好是一个容器而不是一个迭代器,另一方面编写函数时要考虑可能传进迭代器的问题。文章中那个类的用法我还没完全理解透彻,还需要再想想.