【博客大赛】论python中器的组合

python中有几种特殊的对象,如可迭代对象、生成器、迭代器、装饰器等等,特别是生成器这些能够说是python中的门面担当,应用好这些特性的话,能够给咱们的项目带来本质上的提高,装逼不说,这构筑的是代码护城河,祖传代码别人不再敢动。熟悉特性的概念在和面试官交流的过程当中也是挺吃香的不是吗?如今这么卷了,面试官也不多会问到迭代啊、递归啊什么的,反过来讲,在社招面试被问到了这种看起来挺浅薄的问题,可能就是挂的节奏了:)嘿嘿,真的,毕竟面试是要有相对应的面试时间的,总要有水题来刷时间啊┑( ̄Д  ̄)┍node

三者关系

可迭代对象、迭代器和生成器这三个概念很容易混淆,前二者一般不会区分的很明显,只是用法上有区别。生成器在某种概念下能够看作是特殊的迭代器,它比迭代实现上更加简洁。三者关系如图:python

【博客大赛】论python中器的组合

可迭代对象

可迭代对象Iterable Object,简单的来理解就是可使用for或者while来循环遍历的对象。好比常见的 listsetdict等,能够用如下方法来测试对象是不是可迭代面试

>>> from collections import Iterable
>>> isinstance('yerik', Iterable)     # str是否可迭代
True
>>> isinstance([5, 2, 0], Iterable)   # list是否可迭代
True
>>> isinstance(520, Iterable)       # 整数是否可迭代
False

本质

可迭代对象的本质就是能够向咱们提供一个迭代器帮助咱们对其进行迭代遍历使用。 可迭代对象经过 __iteration__提供一个迭代器,在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的迭代器,而后经过这个迭代器来以此获取对象中的每个数据,这也是一个具有__iter__方法的对象,就是一个可迭代对象的缘由。算法

from collections import Iterable

class ListIter(object):
    def __init__(self):
        self.container = list()

    def add(self, item):
        self.container.append(item)

    # 能够经过注释如下两行代码来感觉可迭代对象检测的原理
    def __iter__(self):
        pass

if __name__ == '__main__':
    listiter = ListIter()
    print(isinstance(listiter, Iterable))

经过对可迭代对象使用iter() 函数获取此可迭代对象的迭代器,而后对取到的迭代器不断使用next() 函数来获取下一条数据。iter() 函数实际上就是调用了可迭代对象的__iter__方法。编程

迭代器

迭代器是用来记录每次迭代访问到的位置,当对迭代器使用next() 函数的时候,迭代器会返回他所记录位置的下一个位置的数据。实际上,在使用next() 函数的时候,调用的就是迭代器对象的__next__方法。python3 要求迭代器自己也是可迭代对象,因此还要为迭代器对象实现__iter__方法,而__iter__方法要返回一个迭代器,迭代器自己正是一个迭代器,因此迭代器的__iter__方法返回自身便可.闭包

对全部的可迭代对象调用dir() 方法时,会发现他们都默认实现了__iter__ 方法。咱们能够经过iter(object) 来建立一个迭代器。app

>>> x = [8 ,8 ,8]
>>> dir(x)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>> y = iter(x)
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>

调用iter() 以后,建立一个list_iterator对象,会发现增长了__next__ 方法。咱们不妨断言全部实现了__iter____next__ 两个方法的对象,都是迭代器。ssh

>>> dir(y)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']

迭代器是带状态的对象,它会记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。__iter__返回迭代器自身,__next__返回容器中的下一个值,若是容器中没有更多元素了,则抛出StopIteration异常。socket

>>> next(y)
8
>>> next(y)
8
>>> next(y)
8
>>> next(y)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

for 循环的本质

咱们常常会写出如下代码:ide

for item in obj:

实际上这行代码执行了如下4步:

  1. 判断obj 是否为可迭代对象,便是否有__iter__方法
  2. 在第一步成立前提下, 系统调用iter()函数. 获得obj对象__iter__方法的返回值,这个其实能够本身显式调用
  3. __iter__方法的返回值是一个迭代器,有__iter____next__方法
  4. for 不断的调用迭代器中__next__方法并将值赋给item, 当遇到Stopiteration 的异常后循环结束.

生成器

利用迭代器,能够在每次迭代获取数据,经过next() 方法时按照特定的规律进行生成,可是在实现一个迭代器时,关于当前迭代到的状态须要本身记录,进而才能根据但前状态生成下一个数据。为了达到记录当前状态,并配合next() 函数进行迭代使用,能够采用更简便的语法,即生成器,其本质上是一类特殊的迭代器。

生成器和装饰器都是python中最吸引人的两个黑科技,生成器虽没有装饰器那么经常使用,但在某些针对的情境下十分有效。

好比咱们在建立列表的时候,可能会受到内存限制(特别是在刷题的时候),容量确定是有限的,并且不可能所有给他一次枚举出来。这里可使用列表生成式,可是它有一个致命的缺点就是定义即生成,很是的浪费空间和效率。若是列表元素能够按照某种算法推算出来,那咱们能够在循环的过程当中不断推算出后续的元素,这样就没必要建立完整的list,从而节省大量的空间。这种一边循环一边计算的机制,称为生成器:generator

要建立一个generator ,最简单的方法是改造列表生成式

>>> [x*x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> (x*x for x in range(10))
<generator object <genexpr> at 0x7f8fcc3b5e60>

还有一个方法是生成器函数,一样是经过def 定义,以后经过yield来支持迭代器协议,因此比迭代器写起来更简单,咱们甚至能够下断言道,只要在一个函数中有yield 关键字那么这个函数就不是一个函数,而是生成器

>>> def spam():
...  yield "first"
...  yield "second"
...  yield "3"
...  yield 123
... 
>>> spam
<function spam at 0x7f8fd0391c80>
>>> gen = spam()
>>> gen
<generator object spam at 0x7f8fcc3b5f68>
>>> gen.__next__()
'first'
>>> gen.__next__()
'second'
>>> gen.__next__()
'3'
>>> gen.__next__()
123
>>> gen.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

固然通常都是经过for来使用的,这样不用关心StopIteration的异常

>>> for it in spam():
...  print(it)
... 
first
second
3
123

更进一步的是将生成器和迭代器进行组合,这里是经过iter()来实现

>>> for it in iter(spam()):
...  print(it)
... 
first
second
3
123

本质上就是在进行函数调用的时候,返回一个生成器对象。使用next() 调用的时候,遇到yield就返回,记录此时的函数调用位置,下次调用next() 时,从断点处开始。

说实话有的时候,迭代器和生成器很难区分,毕竟generator 是比Iterator更加简单的实现方式。官方文档写到

Python’s generators provide a convenient way to implement the iterator protocol.

所以彻底能够像使用iterator 同样使用generator ,固然除了定义。毕竟定义一个iterator,须要分别实现__iter__() 方法和__next__() 方法,但generator 只须要一个小小的yield

此外generator 还有send()close() 方法,都是只能在next()调用以后,生成器出去挂起状态时才能使用的。

总的来讲生成器在Python中是一个很是强大的编程结构,能够用更少地中间变量写流式代码,相比其它容器对象它更能节省内存和CPU,固然它能够用更少的代码来实现类似的功能。如今就能够动手重构你的代码了,但凡看到相似:

def something():
    res = list()
    for ... in iter(...):
        res.append(x)
    return res

均可以用生成器函数来替换:

def iter_something():
    for ... in iter(...):
        yield x

python 是支持协程的,也就是微线程,就是经过generator 来实现的。配合generator 咱们能够自定义函数的调用层次关系从而本身来调度线程。

实战

经过两个经典例子来真实感觉一下迭代器与生成器的妙用

斐波那契数列

用 普通函数,迭代器和生成器来实现斐波那契数列,区分三种

输出数列的前N个数

普通函数

这个其实就是内循环,没啥好说的,通过max次循环完成输出

def fab(max):
    n,a,b = 0,0,1
    L = []
    while n < max:
        L.append(b)
        a,b = b,a+b
        n += 1
    return L

Iterator方法

为了节省内存,和处于未知输出的考虑,使用迭代器来改善代码。

class fab(object):
    '''
    Iterator to produce Fibonacci
    '''
    def __init__(self,max):
        self.max = max
        self.n = 0
        self.a = 0
        self.b = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.n < self.max:
            r = self.b
            self.a,self.b = self.b,self.a + self.b
            self.n += 1
            return r
        raise StopIteration('Done')

迭代器什么都好,就是写起来不简洁。因此用 yield 来改写第三版。

Generator

def fab(max):
    n,a,b = 0,0,1
    while n < max:
        yield b
        a,b = b,a+b
        n += 1

使用下面来输出

for a in fab(8):
    print(a)

看起来很简洁,并且有了迭代器的特性。

更进一步

这个是将迭代器和生成器结合起来使用,这个只须要进行一个小小的改动就有的成效

for a in iter(fab(8)):
    print(a)

树上的应用

斐波那契数列能够说是全部程序猿入门必经的练习,那么对于树的遍历也是很是实用的小窍门,不妨看下leetcode的897. 递增顺序搜索树这道题,按中序遍历将其从新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,而且每一个节点没有左子节点,只有一个右子节点。

【博客大赛】论python中器的组合

咱们用上迭代器与生成器的组合以后获得题解

def increasingBST(self, root: TreeNode) -> TreeNode:
        def dfs(node: TreeNode):
            if node:
                yield from dfs(node.left)
                yield node.val
                yield from dfs(node.right)

        ans = cur = TreeNode()
        for v in iter(dfs(root)):
            cur.right = TreeNode(v)
            cur = cur.right
        return ans.right

装饰器

装饰器Decorator是python中最吸引人的特性,其本质上仍是一个函数,它可让已有的函数不作任何改动的状况下增长功能。

很是适合有切面需求的场景,好比权限校验,日志记录和性能测试等等。好比想要执行某个函数前记录日志或者记录时间来统计性能,又不想改动这个函数,就能够经过装饰器来实现。

不用装饰器,咱们会这样来实如今函数执行前插入日志

def test():
    print('i am tester')

def test():
    print('tester is running')
    print('i am test')

虽然这样写是知足了需求,可是改动了原有的代码,若是有其余的函数也须要插入日志的话,就须要改写全部的函数,不能复用代码,为了实现代码复用的需求,能够这么改进

def use_logg(func):
    logging.warn("%s is running" % func.__name__)
    func()

def test():
    print('i am tester')

use_log(teat)    #将函数做为参数传入

这样写的确能够复用插入的日志,缺点就是显示的封装原来的函数,其实咱们更加但愿透明的作这件事。用装饰器来写

bar = use_log(bar)def use_log(func):
    def wrapper(*args,**kwargs):
        logging.warn('%s is running' % func.__name___)
        return func(*args,**kwargs)
    return wrapper

def test():
    print('I am tester')

tester = use_log(test)
tester()

use_log() 就是装饰器,它把真正咱们想要执行的函数test()封装在里面,返回一个封装了加入代码的新函数,看起来就像是test() 被装饰了同样。这个例子中的切面就是函数进入的时候,在这个时候,咱们插入了一句记录日志的代码。这样写仍是不够透明,经过@语法糖来起到tester = use_log(test)的做用。

bar = use_log(bar)def use_log(func):
    def wrapper(*args,**kwargs):
        logging.warn('%s is running' % func.__name___)
        return func(*args,**kwargs)
    return wrapper

@use_log
def test():
    print('I am tester')

@use_log
def haha():
    print('I am haha')

test()
haha()

这样看起来就很简洁,并且代码很容易复用。能够当作是一种智能的高级封装。

装饰器也是能够带参数的,这位装饰器提供了更大的灵活性。

def use_log(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            return func(*args)
        return wrapper

    return decorator

@use_log(level="warn")
def test(name='tester'):
    print("i am %s" % name)

test()

其实是对装饰器的一个函数封装,并返回一个装饰器。这里涉及到做用域的概念,能够把它当作一个带参数的闭包。当使用@use_log(level='warn')时,会将level 的值传给装饰器的环境中。它的效果至关于use_log(level='warn')(test),也就是一个三层的调用。

这里有一个美中不足,decorator 不会改变装饰的函数的功能,但会悄悄的改变一个__name__ 的属性(还有其余一些元信息),由于__name__ 是跟着函数命名走的。能够用@functools.wraps(func) 来让装饰器仍然使用func 的名字。好比

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

functools.wraps 也是一个装饰器,它将原函数的元信息拷贝到装饰器环境中,从而不会被所替换的新函数覆盖掉。

有了装饰器,咱们就能够剥离出大量与函数功能自己无关的代码,增长了代码的复用性。

总结

  1. 容器是一系列元素的集合,如str、list、set、dict、file、sockets对象均可以看做是容器,容器均可以被迭代(用在for,while等语句中),所以他们被称为可迭代对象。
  2. 可迭代对象实现了__iter__方法,该方法返回一个迭代器对象。
  3. 迭代器持有一个内部状态的字段,用于记录下次迭代返回值,它实现了__next____iter__方法,迭代器不会一次性把全部元素加载到内存,而是须要的时候才生成返回结果。
  4. 生成器是一种特殊的迭代器,它的返回值不是经过return而是用yield
  5. 装饰器是一种特殊的闭包,本质上是一个函数

参考资料

  1. https://wiki.python.org/moin/Generators
  2. https://www.jianshu.com/p/efaa19594cf4
  3. http://www.javashuo.com/article/p-vegjchto-do.html
相关文章
相关标签/搜索