美文网首页
函数(4)-- 参数迭代

函数(4)-- 参数迭代

作者: 小懒额 | 来源:发表于2018-05-10 08:02 被阅读0次
    第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
    

    相关文章

      网友评论

          本文标题:函数(4)-- 参数迭代

          本文链接:https://www.haomeiwen.com/subject/gzxirftx.html