遍历参数的时候保守一点
常规遍历
当一个函数需要一个集合作为其参数的时候,多次遍历整个集合时很重要的。例如:你想分析一下美国德克萨斯州的旅游人数,想象一下每个城市的旅游人数的数据集该有多大!要求得每个城市中旅游人数在德克萨斯州旅游总人数的百分比,这将会是多么繁重的一项任务。
为了完成这个目标,你需要一个常规性的函数,貌似其接收的输入就是每年的旅游总人数。然后按旅游地分配到不同的成熟,最后计算出每个城市对德克萨斯州旅游业的贡献。
def normalize(numbers):
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
# 测试
visits = [15, 35, 80]
percentages = normalize(visits)
print(percentages)
>>>
[11.53846, 26.92307, 61.53846]
添加拓展
为了拓展这个函数,我还需要读取一个包含了德克萨斯州所有城市的文件。我定义了一个生成器来做这件事,因为如果我以后想计算全世界的旅游业状况的时候我可以很好的重用这段代码(详见第16
项:使用生成器而不是返回列表)。
def read_visits(data_path):
with open(data_path,'r') as f:
for line in f:
yield int(line)
#让人惊讶的是,调用了read_visits后,返回的结果为空([])。
it = read_visits('/tmp/my_numbers.txt')
percentages = normalize(it)
print(percentages)
>>>
[]
造成上述结果的原因是 一个迭代器每次只处理它本身的数据。如果你遍历一个迭代器或者生成器本身已经引发了一个StopIteration
的异常,你就不可能获得任何数据了。
it = read_visits('tmp/my_numbers.txt')
print(list(it))
print(list(it)) # 这里其实已经执行到头了
>>>
[15, 35, 80]
[]
当你感到疑惑的可能就是你无法在迭代器已经迭代结束的时候发现这个错误,而list
够咱函数以及很多其他的Python
标准库函数却在普通操作的过程中得到StopIteration
异常。这些函数不能分辨到底是没有输入值还是迭代器已经到头了,所以才容易出错。明白了这点,就可以搞懂为什么上面的代码返回的是一个空列表了吧。为了解决这个问题,就需要从迭代器本身下手了,既然是在迭代的过程中导致的问题,那我们也就从这点下手。在未迭代之前先复制一份完整的副本,并保存到一个集合里面。然后就可以通过迭代这个集合来规避上面的那个问题了。代码如下:
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('/tmp/my_numbers.txt')
percentages = normalize_copy(it)
print(percentages)
>>>
[11.53846, 26.92307, 61.53846]
看到list
构造函数,聪明的你可能也会觉得,要是iterator
非常的大怎么办,那样岂不是又会出现一开始的内存危机了吗?是的,确实是这样。解决这个问题的一个办法就是接受一个函数,这样每次调用这个函数的时候都会返回一个新的迭代器,这样也可以是问题得到解决。
def normalize_func(numbers):
total = sum(get_iter()) # 一个新的迭代器
result = []
for value in get_iter(): # 又一个新的迭代器
percent = 100 * value / total
result.append(percent)
return result
为了使用normalize_func函数,你可以将一个lambda表达式作为参数,对每次调用都返回一个迭代器。
percentages = normalize_func(lambda: read_visits(path))
自定义容器类
虽然代码可以正常的工作了,但是每次都要传递一个lambda
表达式又显得很笨拙。一个更好的方式就是提供一个实现了iterator
协议的container
类。iterator
协议是Python
中对于循环和相关表达式内容类型的查找的解释。当Python
遇到像for x in foo
这样的表达式的时候,他就会调用iter(foo)
。内置的iter
函数然后会立刻调用foo.__iter__
方法。而__iter__
方法一定会返回一个迭代器对象(这个方法本身实现了__next__
方法)。然后循环语句就可以重复的调用next
这个内置于迭代器中的方法,直到结果集完全被遍历或者引发了一个StopIteration
异常。
听起来貌似很复杂,但是事实上只要生成器实现了__iter__
方法就足够了。这里,我定义了一个可迭代的容器类来读取旅游业数据。
class ReadVisitors(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 ine(line)
# 现在使用原来的测试函数也可以使得代码正确的运行了。
visits = ReadVisitors(path)
percentages = normalize(visits)
print(percentages)
>>>
[11.53846, 26.92307, 61.53846]
现在这段代码可以正常的工作的原因就在于noamalize
调用了ReadVisitors
的__iter__
方法来重新分配了一个迭代器,从而避免了第二次迭代的时候迭代器已失效(因为在计算total
的时候,sum
函数会一下子迭代完,整个迭代器,等下面的for
循环再遍历的时候就会出现内容已经耗尽的情况发生)的状况。而现在不会了,现在我们会重新生成一个迭代器,在进行循环的时候也可以使得代码正常的工作。现在唯一的缺点就在于 这个方法会多次读取输入数据。
经过了上面的例子的洗礼,想必你已经明白了像ReadVisitors
这样的容器是如何工作了的吧。当你的函数中某些参数不只是迭代器的时候你也可以仿照本例来完善一下。核心就在于:当一个迭代器被iter
函数接收的时候会重新返回这个迭代器本身。相反,每次当一个动器类型被传给iter
函数的时候,一个新的迭代器就会返回一个新的迭代器对象(注意是一个全新的迭代器,待会会用这个条件来判断的)。因此,你可以测试一下这个行为,并触发一个TypeError
来拒绝迭代器。
def normalize_defensive(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
如果你不想像normalize_copy
函数那样复制整个输入迭代器,使用这个方式就变的很理想了,但是你仍然需要多次的迭代输入数据。这个函数可以符合预期的运行,因为ReadVisitors
是一个容器类。当然了,符合iterator
协议的容器类也都是可行的,这里就不再过多地叙述了。
visits = [15, 35, 80]
normalize_defensive(visits)
visits = ReadVIsitors(path)
normalize_defensive(visits)
# 但是如果输入值不是一个容器类的话,就会引发异常了
it = iter(visits)
normalize_defensive(it)
>>>
TypeError: Must supply a container
备忘录
- 多次遍历输入参数的时候应该多加小心。如果参数是迭代器的话你可能看到奇怪的现象或者缺少值现象的发生。
Python
的iterator
协议定义额容器和迭代器在iter
和next
下对于循环和相关表达式的关系。- 只要实现了
__iter__
方法,你就可以很容易的定义一个可迭代的容器类。 - 通过连续调用两次
iter
方法,你就可以预先检测一个值是不是迭代器而不是容器。两次结果一致那就是迭代器,否则就是容器了。