【博客大赛】论python中器的组合
python中有几种特殊的对象,如可迭代对象、生成器、迭代器、装饰器等等,特别是生成器这些可以说是python中的门面担当,应用好这些特性的话,可以给我们的项目带来本质上的提升,装逼不说,这构筑的是代码护城河,祖传代码别人再也不敢动。熟悉特性的概念在和面试官交流的过程中也是挺吃香的不是吗?现在这么卷了,面试官也很少会问到迭代啊、递归啊什么的,反过来说,在社招面试被问到了这种看起来挺浅薄的问题,可能就是挂的节奏了:)嘿嘿,真的,毕竟面试是要有相对应的面试时间的,总要有水题来刷时间啊┑( ̄Д  ̄)┍
三者关系
可迭代对象、迭代器和生成器这三个概念很容易混淆,前两者通常不会区分的很明显,只是用法上有区别。生成器在某种概念下可以看做是特殊的迭代器,它比迭代实现上更加简洁。三者关系如图:
可迭代对象
可迭代对象Iterable Object
,简单的来理解就是可以使用for
或者while
来循环遍历的对象。比如常见的 list
、set
、dict
等,可以用以下方法来测试对象是否是可迭代
>>> 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)
来创建一个迭代器。
>>> 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__
两个方法的对象,都是迭代器。
>>> 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
异常。
>>> next(y) 8 >>> next(y) 8 >>> next(y) 8 >>> next(y) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
for 循环的本质
我们经常会写出以下代码:
for item in obj:
实际上这行代码执行了以下4步:
- 判断
obj
是否为可迭代对象,即是否有__iter__
方法 - 在第一步成立前提下, 系统调用
iter()
函数. 得到obj
对象__iter__
方法的返回值,这个其实可以自己显式调用 __iter__
方法的返回值是一个迭代器,有__iter__
和__next__
方法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. 递增顺序搜索树这道题,按中序遍历将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。
我们用上迭代器与生成器的组合之后得到题解
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
也是一个装饰器,它将原函数的元信息拷贝到装饰器环境中,从而不会被所替换的新函数覆盖掉。
有了装饰器,我们就可以剥离出大量与函数功能本身无关的代码,增加了代码的复用性。
总结
- 容器是一系列元素的集合,如str、list、set、dict、file、sockets对象都可以看作是容器,容器都可以被迭代(用在for,while等语句中),因此他们被称为可迭代对象。
- 可迭代对象实现了
__iter__
方法,该方法返回一个迭代器对象。 - 迭代器持有一个内部状态的字段,用于记录下次迭代返回值,它实现了
__next__
和__iter__
方法,迭代器不会一次性把所有元素加载到内存,而是需要的时候才生成返回结果。 - 生成器是一种特殊的迭代器,它的返回值不是通过
return
而是用yield
。 - 装饰器是一种特殊的闭包,本质上是一个函数
参考资料

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
浅入Kubernetes(12):Deployment 的升级、回滚
目录 更新 上线 会滚 缩放 Deployment 直接设置 Pod 水平自动缩放 比例缩放 暂停 Deployment 上线 本篇内容讨论 Pod 的更新和回滚,内容不多。 更新 打开https://hub.docker.com/_/nginx可以查询 nginx 的镜像版本,我们可以先选择一个旧一点的版本。 首先,我们创建一个 Nginx 的 Deployment,副本数量为 3。 kubectlcreatedeploymentnginx--image=nginx:1.19.0--replicas=3 首次部署的时候,跟之前的操作一致,不需要什么特殊的命令。 注:我们也可以加上--record标志将所执行的命令写入资源注解kubernetes.io/change-cause中。 这对于以后的检查是有用的。例如,要查看针对每个 Deployment 修订版本所执行过的命令。 其实更新 pod 是非常简单的,我们不需要控制每个 pod 的更新,也不需要担心会不会对业务产生影响,k8s 会自动控制这些过程。 我们只需要触发镜像版本更新事件,k8s 会自动为我们更新 pod 的。 kube...
- 下一篇
2021超详细的HashMap原理分析,面试官看了都说好
前言 最近针对互联网公司面试问到的知识点,总结出了Java程序员面试涉及到的绝大部分面试题及答案分享给大家,希望能帮助到你面试前的复习且找到一个好的工作,也节省你在网上搜索资料的时间来学习。 内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、MySQL、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技术栈。 完整版Java面试题地址:JAVA后端面试题整合 一、散列表结构 散列表结构就是数组+链表的结构 二、什么是哈希? Hash也称散列、哈希,对应的英文单词Hash,基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出 这个映射的规则就是对应的哈希算法,而原始数据映射后的二进制就是哈希值 Java并发编程学习笔记,关注公众号:程序员追风,回复 013 领取422页PDF文档 不同的数据它对应的哈希码值是不一样的 哈希算法的效率非常高 三、HashMap原理讲解 3.1、继承体系图 3.2、Node数据结构分析 static class No...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS6,CentOS7官方镜像安装Oracle11G
- Red5直播服务器,属于Java语言的直播服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS关闭SELinux安全模块