第17条:在参数上面迭代时,要多加小心
如果函数接受的参数是个对象列表,那么很有可能在这个列表上面进行多次迭代。
比如在一个文件中获取数字,获取当前每个数字占所有数字总和百分比的结果时,可能会这样写:
def normalize(numbers):
total = sum(numbers)
result = []
for num in numbers:
percentage = 100 * num / total
result.append(percentage)
return result
# 这里使用生成器来实现文件的读取
def read_visits(data_path):
with open(data_path) as f:
for line in f:
yield int(line)
然而在运行的时候,发现并没有输出预想的结果,而是输出了一个 None。
it = read_visits('D:\\numbers.txt')
percentages = normalize(it)
print(percentages)
>>>
None
对于出乎意料的结果,原因就在于给 normalize
函数的参数是一个生成器,在进行 sum()
运算的时候,就已经对这个参数进行了一直完全的迭代(直到抛出 StopIteration 异常),当再去使用 for
语句来对其进行迭代的时候,由于该生成器已经通过 sum()
遍历到结尾了,就无法继续进行遍历。
这里还要注意的是, for
语句对 StopIteration 异常认为是合法的,因为在 for
语句中正常遍历到最后都会出现这个异常,所以 for
语句中也没有任何报错。
所以在把生成器作为函数的参数时,如果函数中存在对生成器进行多次迭代,就不能正常运行返回结果。
为了函数的正确运行,我们可以在函数中将迭代器复制一份,在函数中多次使用,
def normalize(numbers):
number_list = list(numbers)
total = sum(number_list)
result = []
for num in number_list:
percentage = 100 * num / total
result.append(percentage)
return result
it = read_visits('D:\\numbers.txt')
percentages = normalize(it)
print(percentages)
>>>
[13.186813186813186, 31.16883116883117, 21.37862137862138, 34.26573426573427]
还有一种方法就是传入一个可以返回新的生成器的函数作为参数,保证函数中每次使用这个参数时都是一个新的生成器,就不会出现 StopIteration 异常。
def normalize_func(get_iter):
total = sum(get_iter())
result = []
for num in get_iter():
percentage = 100 * num / total
result.append(percentage)
return result
这里的 get_iter() 函数可以使用 lamdba 表达式来实现。
percentages = read_visits(lambda: read_visits(data_path))
这样把函数作为参数,确实使得结果正确,只是看起来 lambda 表达式略显生硬。还有一个更好的方法,就是新编一种实现迭代器协议的容器。
在执行 for x in foo
时, python 实际上会调用 iter(foo)
。内置的 iter
函数又会调用 foo.__iter__
这个特殊方法。该方法必须返回迭代器对象,而迭代器本身,则实现了 __next__
的方法。然后, for 循环会在迭代器上反复调用内置的 next
函数,直到 next 函数返回 StopIteration 为止。
按照这个思路,我们只要把自己类中的 __iter__
方法实现为生成器就可以满足这个要求。
class ReadVisits(data_path):
def __init__(self):
self.data_path = data_path
def __iter__(self):
with open(self.data_path) as f:
for line in f:
yield int(line)
visit = ReadVisits(data_path)
percentages = normalize(visit)
print(percentages)
>>>
[13.186813186813186, 31.16883116883117, 21.37862137862138, 34.26573426573427]
这时再把前面的 normalize 函数改写一下,保证传入的参数不是生成器对象,
def normalize_defensive(numbers):
if iter(numbers) is iter(numbers):
raise TypeError('Must supply a container')
total = sum(numbers())
result = []
for num in numbers():
percentage = 100 * num / total
result.append(percentage)
return result
visit = ReadVisits(data_path)
percentages = normalize_defensive(visit)
print(percentages)
>>>
[13.186813186813186, 31.16883116883117, 21.37862137862138, 34.26573426573427]
it = read_visits(data_path)
percentages = normalize_defensive(it)
print(percentages)
>>>
TypeError: Must supply a container
网友评论