理解Python的Iterable和Iterator

今天看到一篇很有意思的文章,说Pyhton3的range是返回的什么?

很多人都会不假思索的说,这还不简单,在Python2中range()会返回list,到了Python3range已经使用xrange替换,返回的是一个迭代器(Iterator)。

恭喜你,答错了。

range()返回的是一个Iterable,并不是一个Iterator.

原理很简单,先简单说一下Iterable和Iterator,不要试图比较二者有什么不同,因为二者根本就是不同的概念。二者字面意思都非常明确:Iterable就是一个可迭代的对象,对其调用iter(Iterable)将会得到一个迭代器;而Iterator就是一个迭代器,对其调用next(Iterator)将会得到下一个元素。

Python推崇协议,说白了就是鸭子类型。你如果实现了__iter__(),(即对你调用iter()可以得到一个Iterator)那你就是一个Iterable;如果实现了__next__()__iter__()就是一个Iterator。

等等,Iterator不就是调用next()得到下一个元素就可以了?为什么Iterator也要实现Iterable的__iter__()方法,这不纯粹啊!

为什么Python的Iterator要实现__iter__()呢(通常的实现都是return self)。官方文档中说的相当清楚。

Iterators are required to have an __iter__() method that returns the iterator object itself so every iterator is also iterable and may be used in most places where other iterables are accepted.

简单翻译一下,就是说Iterator也要求实现__iter__(),因为很多地方接收的参数是一个Iterable,如果所有的Iterator都是Iterable,那么这些用Iterable地方都可以无障碍地使用Iterator了。比如说for循环吧,关于for循环,我在Python的wiki中(已经比较老了)找到这样的描述:

 Basically, any object with an iterable method can be used in a for loop. Even strings, despite not having an iterable method – but we’ll not get on to that here.

即,for循环拿到一个Iterable的Iterator,然后使用这个Iterator进行迭代。如果Iterator实现了__iter__()方法,那么for循环就可以无障碍地对Iterator进行迭代了,Neat!想象一下,Python的生成器也是Iterator,如果for循环不能支持对Iterator迭代,生不如死啊。

所以对Iterator就有了这样一个“过分”的要求。我们可以认为,所有的Iterator都是Iterable。那么回到最初的问题,为什么range()反回的是一个Iterable而不是Iterator呢?

Iterator是有状态的,只能遍历一次,是“消费型”的,不可以“二次消费”。Iterable是没有状态的(这里不太严谨,这句话暂且不提是Iterator的Iterable),每一次对Iterable调用iter()都会得到一个新的迭代器。

考虑我们平时使用range(),我们认为这是一个表示范围的一个容器。可以使用这个容器去初始化成别的容器,这没有任何问题。

倘若range()返回的是迭代器,那么上面这个看起来在正常不过的代码就有麻烦了:

 

读到这里,你应该完全理解Iterable和Iterator了,再说点题外话。

有人可能觉得(我之前也这么觉得),这样的实现太不纯粹了,要是Iterator就是Iterator,Iterable就是Iterable不好吗?规定接口只接受Iterator,如果有Iterable的话必须手动获得一个新的Iterator然后使用。可以是可以,但是想象一下每次都初始化一个新的Iterator多费劲啊。并且肯定有地方会定义一个临时变量,万一忍不住忘记这已经迭代完了,被迭代第二次呢?

对Iterator进行迭代,是很符合人类思维的事情,因为迭代器本来就是可以迭代的。

对Iterable进行迭代,也是很符合人类思维的事情,”可迭代的对象“本来也是可迭代的,每次迭代的时候在内部初始化一个Iterator进行迭代。

所以现在想想,其实Iterator的这种行为就非常合理了。

Python是一门讲求实用主义的语言。随着越来越多地接触和使用这一门语言,我也越来越体会到这句话。另一个例子是Pyhton的元祖,这个数据结构的灵感来自ABC语言的compoundscompounds可以作为字典(在ABC中字典叫table)的合成键,也支持平行赋值。仅此而已。如果你想使用它,要么使用平行赋值取出全部的元素,要么作为一个整体来使用。这样compounds的作用变得非常纯粹——一个没有字段名字的记录的集合。而Python的tuple,却支持了迭代、下标、切片,给人的印象更像是frozenlist!概念上已经失去了compounds的纯粹。

但是这种实用主义的哲学让Python比ABC更灵活,更好用也更成功。

Although practicality beats purity.

Python 之禅如是说。

 

Python3 str混入bytes的解码问题

问题:"abc\\xe2\\x86\\x92"是一个str,其中混入了一些被错误转义的字符,需要解码得到"abc→",其中,b'\xe2\x86\x92'.decode()会得到

TL;DR

解释

"abc\\xe2\\x86\\x92"是一个str,其中\\是一个字符,前面一个反斜杠将后面一个转义了。所以这一共是15个字符。而我想要的事后面12个字符其实是表示一个utf-8编码的unicode字符。

Python提供了unicode-escape编码/解码器。可以编码/解码字面意思的unicode。编码的时候将\xe2四个字节,转换成一个字节。解码的时候,将3个字节表示的一个unicode字符,先展开unicode的表示形式,例如\u4f60,然后包括\,一个字符占一个字节,一共6个字节。

Encoding suitable as the contents of a Unicode literal in ASCII-encoded Python source code, except that quotes are not escaped. Decodes from Latin-1 source code. Beware that Python source code actually uses UTF-8 by default.

unicode-escape实际上做了两件事:

  1. escape。encode的时候,将一个unicode字符转换成多个字符,字面形式表示。decode的时候,将多个字符转成一个unicode字节。
  2. 正常encode/decode做的事情,提供strbytes之间的转换,但是只使用latin-1编码。

"abc\\xe2\\x86\\x92"中后半部分是bytes,所以应该先转换成bytes的正确格式。

现在是15个字符,后面12个应该是一个字符,按照unicode解释。

根据上面提到的,现在decode变成了latin-1编码的str,但实际上这是utf-8编码的,所以应该用latin-1解码然后重新用utf-8编码。

参考:

  1. 转换\\
  2. v2ex讨论
 

Python的命令行界面库

最近想写个命令行的图形界面的小玩具,类似htop,在命令行运行,但是不是那种输入-输出的模式,而是一种基于文字的图形界面,对于终端用户来说,比较友好。

我记得之前看到过一个不错的库,还跑过它的demo,费了不少劲才找到(你用就找不到不用就天天看到定律)。神奇的是,在这个过程中又发现不少类似的库…… 这下可纠结了。下面是整理的搜索过程中的资料,希望能帮到一些人。

1. curses

这是Python内置的一个module。不同的操作系统中不同的终端模拟器的行为可能是不一样的,curses就为你屏蔽了底层的细节,让你依赖curses写出的程序可以运行在各种终端上。也是基于这个目的,Python内置的curses基本上就是原生C curses的一个封装,实现了一些公共的操作,比如移动光标(cursor),滚动屏幕,擦除部分区域等。

另外,Windows上面的Python不会自带这个库,官方推荐了the Console moduleUniCurses

另另外,ncurses是curses的一个free版本。

我们如果想快速开发用户界面,一般会用到一些组件(widget),例如TextArea,Button,Form等,写过前端或安卓app等应该都很熟悉这些。这些东西curses都是没有提供的,并且如何响应事件等好像也是没有的,看过几个例子都是通过While True循环来监听事件。所以如果想写的界面稍微有点点复杂,这个库就力不从心了。Python的文档中推荐使用Urwid

2. pyCDK

SourceForge的页面已经打不开了…… 我在pypi上看到上次更新是2003年…… 再见!

3. Urwid

这个就很厉害了,官方网站没有太多介绍,但是从requirements可以看出,是提供了事件循环的,并且是可选的:

  • python-gi for GlibEventLoop (optional)
  • Twisted for TwistedEventLoop (optional)
  • Tornado for TornadoEventLoop (optional)
  • asyncio or trollius for AsyncioEventLoop (optional)

这就很牛逼了。从文档中,可以看到有一些Widget Classes,常用的一些Widget都有的,很赞,还有一些例子,上手应该也挺简单。我觉得首选就是它了。它更像是一个framework,而不是一个widget的库。某些情况下还是有不方便的地方。我觉得应该可以再搞一个库,弄一些常用、方便用的widget。但是想来在命令行写UI的需求实在太小吧,估计没人去做了。

4. Blessings

这个库只是curses的一个pitch和封装,解决了部分curses不好的地方,比如说函数调用啰嗦等。可以让你用string的format来控制显示,然后封装了一个Terminal,用户只用这一个对象就可以。可以说只是修修补补,但是主要的问题没有解决(UI组件和事件驱动)。

作者有另一个东西挺有意思,一个终端的游戏conway,就是用自己的这个库写的。

curses这种库用来当做一个终端的pygame来用应该没啥问题的,每一帧都重新绘制(终端本来也没几个字符),并不需要什么widget,因为基本都是自己画的游戏图形,但是并不适合来制作UI。

参考:

  1. Python ncurses, CDK, urwid difference
  2. google搜索找到不少有意思的相关项目
 

看牙医

上个月发现一个牙上面有黑黑的东西,得去看牙医了。在上海看得请假,而且估计去大医院得排队,于是就拖到了过年之后会家在一个私人诊所看的。顺便还洗了牙,价格是260人民币,不知道在大医院是多少。洗完牙之后医生给涂了一些东西,比起上次洗牙,没有不磨合、需要适应的时间了,洗完之后就很舒服。

牙医比较友好,一边洗牙一边跟我说哪些地方牙结石比较多,让我平时刷牙注意:

  1. 牙结石是肯定会有的,但是如果平时刷牙仔细的话,就不太用洗,比如可以四五年洗一次。洗牙的时候可以问一下医生哪里结石比较多,自己以后注意。
  2. 刷牙的方法,说白了就是每个牙齿露在外面的地方都要刷到,不要漏下。例如牙齿内侧,最里面的几个牙齿都不容易刷到。博主建议买个电动牙刷。可以看下Fview的这篇评测
  3. 牙齿疼或者自己发现有不好的地方,尽早看牙医。
  4. 吃东西要两侧都用,不要只用或者多用某一侧,对牙齿,肌肉等都不太好。

过年在家里十天,没干什么正经事,家里太冷了,用电脑都不太方便。闲暇的时候自己想到了几个问题,然后又自己冥想解决方法,明天回上海,可以好好研究、验证一下。在家还读了一些TCP/IP的东西,觉得网络协议很多有意思。协议不光要实现基本的功能,还要考虑到各种各样意外的情况、恶意的情况、节省资源等,虽然现在TCP/IP协议有一些缺陷,比如SYN洪水攻击等,但是我觉得已经很棒了,想想现在整个世界都是基于这个协议的,每天都有那么多包在穿来穿去,来满足35亿网民的数据请求,很神奇。

 

HTTP连接池(基于Python的requests和urllib3)

HTTP是建立在TCP上面的,一次HTTP请求要经历TCP三次握手阶段,然后发送请求,得到相应,最后TCP断开连接。如果我们要发出多个HTTP请求,每次都这么搞,那每次要握手、请求、断开,就太浪费了,如果是HTTPS请求,就更加浪费了,每次HTTPS请求之前的连接多好几个包(不包括ACK的话会多4个)。所以如果我们在TCP或HTTP连接建立之后,可以传输、传输、传输,就能省很多资源。于是就有了“HTTP(S)连接池”的概念。和线程池非常像是不是。本文介绍连接池,连接池管理器,主要基于Python和 requests, urllib3 两个库。主要讲HTTP连接池,HTTPS连接池原理一样,只不过不光缓存TCP连接,还有发起请求之前对证书认证等过程。

HTTP连接池 urllib3.HTTPConnectionPool

首先需要明确的是,HTTP连接池缓存的是TCP连接,这个链接是相对于客户端和服务器的,说简单点,就是针对一个url(ip)目标的,所以连接池建立的时候要指定对哪一个主机缓存连接。比如发送给 domain.com/a 的请求和发送给 domain.com/b 的请求是可以使用一个TCP连接的,但是发送给 a-domain.com 的请求和 b-domain.com/b 的请求就不可能用一个连接完成的。

尝试使用一下:

这里我们用一个连接池发送了5次请求,运行结果如下:

同时,用Wireshark抓包,用 ip.src==47.95.47.253 or ip.dst==47.95.47.253 and (tcp.flags==0x12)过滤出来TCP握手的包,可以看到只抓到1个。证明我们5次请求只建立了一个TCP连接。

有个需要注意的参数是maxsize,这个参数指定了缓存连接的数量,默认是1.如果在多线程的情况下,可能两个线程用到了同一个pool,只有一个连接被缓存的话,另一个线程就需要新开一个连接。这时候会有两种情况:

  1. 如果block参数是True,那么第二个线程被阻塞,直到这唯一一个可用的连接被释放。
  2. 如果blcok参数是False(默认),那么第二个线程会新建一个连接,但是使用完成之后连接被销毁。连接池只会保存一个连接。

测试一下第一种情况,线程1和2同时发送请求,结束之后新的两个线程又发送请求。通过输出结果和Wireshark抓包发现自始至终只有1个TCP连接,没有新的建立。

输出结果,连接数始终是1:

Wireshark抓包,只有1次连接:

再试一下第二种情况,下面的代码和上面的唯一的区别是block参数是False

输出结果:

Wireshark抓包,前两个线程会创建两个连接,一个连接使用之后被缓存,另一个使用之后就断开。在后面线程3和4的时候,一个线程会使用缓存的连接,另一个又会新开一个连接。所以一共有三次握手的包。

综上,在多线程的环境中,多缓存一些连接可能带来性能上的提升,一般连接数等于线程数,这样保证所有的线程都有缓存的连接可用。当然,也要结合实际的情况考虑timeout 和 block等参数。

连接池管理器 urllib3.PoolManager

上面介绍的连接池是面向对方主机管理的,如果我要向不同的域名发请求,希望缓存多个域名的连接,就要有多个连接池。好在urllib3将这一层也抽象了。

PoolManager做的事情并不多,基本上就是一个MRU原则(Least Recently Used )维护自己的Pool。比如初始化的大小设置为10,那么需要建立第11个连接池的时候,最最旧的一个连接池就被销毁。

它的函数原型是class urllib3.poolmanager.PoolManager(num_pools=10, **connection_pool_kw),只有一个参数num_pools表示池的数量,其余参数将会传给Pool初始化。

requests中的接口

HTTP请求相当dirty,好在优秀的库requests帮我们搞定了各种复杂的情况。建议涉及HTTP操作的都是用requests这个封装好的库。

requests中有Adapter的概念,事实上,所有的请求都是通过默认的一个HTTPAdapter发出去的。如果我们想给一个域名加代理,都可以amount一个自定义的Adapter。

参数很明确,pool_connections会传到HTTPConnectionPool控制缓存连接的数量,pool_maxsize会传到PoolManager控制Pool的数量。

关于“连接池”和“连接池管理器”我有一个很困惑的地方, 为什么要分开这两个概念呢?这样的话要控制连接池连接的数量和连接池的数量,就要权衡我的应用是都连接向同一个网站的,还是连接向不同的网站的。然后根据线程权衡设置这两个数量。如果只有一个概念,连接池里面可以有各种域名的连接的缓存,我就可以直接考虑线程的数量来设置缓存连接的数量了。反正同一连接池的两个连接是两个连接,两个连接池的连接也是两个连接。如果去掉连接池管理器,直接将概念压扁成一层,那么对连接数量的管理就更方便了不是吗?可能urllib这么做也有它的原因吧,如果读者知道其中的原因或者我的想法的漏洞可以指点一下。