我叫“Snake”,在我佛如来的指引下,我开始了一段取经之路。猴哥的称号是“斗战圣佛”,我的称号是“Python”。一条蛇的取经故事,我称之为《蛇经》。
在我的修行道路上,会有各种各样的怪。因为他们的形式不同,所以我一直在学习新的打怪技能,为的就是能控制他们,为我所用。我佛慈悲,先招将,不从再杀。
一天,我收到处理一些数据的传令,并被告知数据在某个文件里面。我是没能力上天入地的,有些办不到的事情,我就会求救于“天将”或者“土地”。喊土地(内置函数)很简单,他们就在我身边,每次需要他们帮忙的时候,我就直接呼唤他们。叫天将(标准库)就不一样了,不是随随便便就能让他们来帮你干活的。取经前,上天给了我块“请神令”——import,后面有机会我们再谈吧。
先说这次的问题,文件里的东西我是没办法直接拿到的。在需要从文件里拿数据的情况下,我都会找一位土地——open()。他很厉害,这是我们第一次见面时,他的自我介绍:
你好,很高兴您能召唤我,自我介绍下,我能去不毛之地——文件里把数据带回来。您可以指定我的四项功能。
第一:文件路径。告诉我去哪里,之后您就别管了,我会代步的。
第二:打开方式。告诉您要做什么,是往文件里送人(写数据),还是从文件里往外带人(读数据)。参数有‘r’(read读),‘w’(write),‘b’(binary以二进制形式打开文件,以字节对象的形式进行读取或写入),‘+’(r and w),‘a’(add,在文件末尾追加东西)等等。这些个参数您可以同时指定给我,比如 ‘rb’,我就会知道您是想让我以二进制形式读数据,其他的相同。
第三:编码方式。告诉我,文件的编码方式,我会按照那种方式来处理数据,如果您让我处理的方式和数据不匹配,那我处理完后的数据您可能不认得(乱码)。
第四:缓冲区个数。这个功能一般不需要您操作,但是我还是要说下。您知道我会把数据带到指定文件里,但是文件最终都会被存放到一个叫“磁盘”的地方。也就是说,我会一直往“磁盘”里带数据。如果您将我的这项参数设为1,那我每遇到一个换行符,我就会去磁盘走一次。如果您让我带的数据有10行,那我就要去10次。
如果您设置的是大于1的某个值n,每当够n个字节,我就会走一次。
如果是0的话,一有数据我就会带走。
如果是负值或者不指定,我就会默认系统值。所以这项功能关乎着我的劳动量,请慎重。
很简明又有趣的介绍。这里让我突然想到我们最开始合作时出现的那个很严重的问题。
open每次打开文件后,自己不会回来,而是派一个小信使(文件对象)回来完成任务,自己待在原处使文件一直处于打开状态。
oh!你好,我是open的信使,需要我做什么?随时效力~
很可爱的小信使,我给他随便起了个名字叫f(也就是file的意思),然后问他:“都能做什么?”
他说:“可读可写,什么都没问题。”
有趣,“你读数据的功能有什么特殊之处吗”
“我读数据的方法有很多,你可以直接让我read。我会把文件里的所有内容处理成一个字符串(一种数据的类型,这种属性的怪有很多处理方法,你先理解成一种怪的类别。)给你,read(10)的话,我会读10个字节给你。如果你想让我只读一行,我可以readline。如果......”
“等会~,先给我一行数据看看。”
“好嘞,稍等~”,小信使拿出他的信封开始查看,不久给了我一行数据。我很是欣喜,之后我们聊了很多,聊着聊着我突然想到,我忘了给小信使刚刚给我的字符串起名字(变量赋值)。比如开始的时候我给小信使起了名字叫f,所有我才能用他做f.readline()这件事。字符串名字没有起,我就找不到他了,他如果四处跑,最后会被“检察官”认定为无证子民,从这个世界处理掉。(垃圾回收机制,不再使用的数据进行清理)
我一拍脑门,对小信使说,“麻烦再去readline下吧,那个字符串我忘记起名字了,给弄丢了。”我一脸郁闷。
“没问题,稍等。”不一会后,我就又得到了一个字符串,我刚要感谢,突然发现问题不对。
“等等!这个不是刚才的字符串啊,和刚刚的不一样?”
“当然不一样啦~ 这是第二行的数据。”
“你怎么读了第二行的数据呢?”
“听我给你讲哈,每次我读完数据,我就会在最后做个标记。下次再读的时候,我会从做的标记开始。开始的时候我读了一行数据,我就会做个标记。后来我又读了一行数据,就从那个标记开始读一行,所以就读了第二行的数据。”
“哦~ 懂了,也就是说你会记住你上次的操作是吗?如果我让你再执行readline,你就会给我第三的数据?”
“对的,就是这样~”
“那下次我再让open来帮忙,你还会记得你读到哪里了吗?”
“哦,不会了,当你完成这个任务后,我就会消失。你下次再找open老大帮忙的时候,他会再派一个信使,那已经不是我了。他会重新开始的。”
一想到下次就见不到他了,突然有点悲伤。
他看出了我的顾虑,道:“别担心,虽然不是我了。但那个哥们还是信使,能和我做相同的事情。”
“嗯嗯,现在我懂了,也就是说,我每次请open打开文件,你们信使(文件对象)的标记(指针)是从0开始的,也就是文件开头。但是我现在不想重新打开文件,我想让你在帮我读下第一行的数据,还有办法做到吗?”
“当然啦~ 你可以修改我的标记啊~”
“修改标记?”
“对,因为我每次都会从标记处开始读取,你就可以修改我的标记,下次读取的时候,我就会从那里开始的。”
“也就是说我现在把你的标记改成 ‘0’ ,你下次读的时候就从头开始了是吗?”
“对的,给你笔~”。
我拿着这支叫seek的笔。然后执行了一段命令:f.seek(0)
“ok,修改成功了,我现在的标记在0处,也就是文件开头了。”
“那个,使用这支笔的时候,我还有一些其他需要注意的东西吗?”
“您还真是好学啊,有的。其实seek的参数有两个,一个就是您刚刚输入的offset,我们把它叫做‘偏移量’,单位是‘字节’。还有一个参数我叫它whence,如果您不指定,我就默认为是0了。这个参数表示从哪个位置开始偏移。0表示从文件开头开始偏移,1表示从当前标记开始算起,2表示从文件末尾开始算起。举个例子,比如您刚刚执行的命令是seek(0),其实完整地代码命令是——seek(0, 0),也就是从文件开始位置偏移0个字节的地方做标记,所以我的标记就做到开头了。如果您输入的命令是seek(0, 1),本质上我的标记是不用动的,因为您指定的标记是从当前位置开始,偏移0个字节,也就是没有改变。”
“那whence为2的时候呢?它已经在文件末尾了,后面没有数据,标记怎么做呢?”
“如果当whence为2,并且offset >= 0的时候,我就会把标记做到文件末尾。如果这个时候您还让我读数据,我就只能给您返回空字符串了。”
“offset >= 0? 你是说offset还能是负数?”
“哇塞,您还注意到这一点啦~,是的。我可以处理负数,正数既然是往后偏移,负数自然就是往左偏移了~,比如说offset是-1,我就会把标记做到whence指定位置的前一个字节处,如果是-2,我就会做当前两个字节处的~”
“哇~ 小信使真厉害”。我心里默默地想。
“不过这里是有个问题的,您还记得您派open老大去文件的时候有个打开方式吗?”
“我记得,我记得我设置成了‘r’,也就是read的意思。”
“对,就是这里。如果您在用seek笔的时候,whence指定了1或者2,offset只能是0。如果不是0的话,就会出现可怕的后果,那件事情“天机处”(后面会谈到)告诉我叫做‘io.UnsupportedOperation’——IO不支持操作,我是这么叫的。”
“那我怎样才能正常使用seek笔呢?”
“哦,您记得下次让open老大打开文件的方式是二进制读取就可以了,也就是把那个参数设成'rb'。‘b’就是binary的意思。”
“ok, 了解了~ 帮大忙了。”
“等下,还有件事情要告诉您~ 我还有个工具叫tell。”
“哦~,干什么用的?”
他邪魅的一笑,“告诉您我当前的标记在什么位置。(从文件开始到当前位置的比特数,不是字数)”
“想的还挺周到的~”,我笑着说。
“你还有什么能力向我展示吗?”
“很多呢,比如读数据的时候,我除了可以readline还可以readlines。如果让我执行readlines,我会把每行数据当成一个元素,放到list(列表,我取经路上四大保镖之一,每次我处理数据,都会放到保镖那里,让他们帮我做事情。比如把数据们排排序啊,把所有数据都加一啊,之类的。后面有机会,我们再来说说我的保镖们吧~)里面。”
“嗯,这个很好用,你把每行的数据给了list,那之后我就可以从list里面拿数据了。也就不用一直麻烦你了。”
“还有呢~,这些功能只是读数据,我还可以往文件里写数据,只需要执行write,当然了,你派open老大过去的时候,要告诉他文件的打开方式是‘w’(write写)才行。”
“还有没有更炫酷的功能!!!”期待脸。
“我还可以writelines()。你可以给我一个“序列”(我的四大保镖都是序列部门的),我可以把序列的内容一条条写进文件里去。”
“斯盖~~,还有吗?”
“我还有个next()功能,和readline()一样......”
bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla, bla......
那次我们聊了很久,让我差点忘记了正事:
我迅速地写下两条命令,拿到了第一行的数据(这次没有忘记起名字。汗~~~~):
f = open('test.txt', 'r')
f.readline()
#以上为历史信息
f.seek(0)
data = f.readline()
其实我只需要文件的第一行数据,当我拿到他后,就去忙别的了。没有再去让小信使做别的事,我是后来才知道的,原来他一直在等我。因为我不告诉他任务结束,他是不会走的!
我是怎么知道的呢。说起来,还是我犯的错误。那次我让open帮忙存储数据......。
那天我用了半天的时间处理完一堆数据,看着他们工工整整的样子,我很是欣慰。于是我想把他们存储起来,以备后面我再次用到他们,也省的我下次再次处理了。
我迅速的写下下面指令:
f = open('test.txt', 'w')
f.write('abc')
我往test.txt文件里写入了一个字符串‘abc’。
几天后,我想把test.txt文件里的数据拿出来。于是我又找来土地open帮忙,我告诉他:“麻烦去“test.txt”,‘r’方式打开。”
不久后,他派的小信使回来了,“请指示~ sir”
“麻烦帮我读出第一行数据。”
“收到,readline执行中,请稍等~。读取完毕,请接收......”,他递给我一个字符串。
“谢谢,......”我的话还未说完,就发现了问题,小信使给我的竟然是个空字符串。“你怎么不好好工作呢?我让你读取数据,你怎么给我个空串!”
“可是文件就是空的呀......”信使有些委屈。
“啥?! 空的! 不可能,上次我明明派另一个小信使write了的。你把你们open老大喊回来,我要和他聊聊!”
“如果open老的回来,我就会消失的。”
“嗯,你的工作已经做完了,去喊你们老大吧。”
“工作完了?您是想让我现在执行close()命令吗?”
“close()命令? 啥意思?”我一脸疑惑。
“啊?没人和您说过吗。如果您需要信使做的事情都做完了,您要告诉他,任务结束,可以关闭文件了。不然open老大会一直在那边打开着文件,我也一直会存在的。”
“呀呀呀~,这么回事啊。如果我让你write了,不管你,一直等到程序结束,这个世界不再存在了,会怎样。”
“那文件里的东西会先被清空,只有当我执行close之后,我才能把数据写进去,open老大也才能把他们保存到“磁盘”里。不然直到程序结束,我都没有close的话,文件就成空的了。”
“呀呀呀,这么回事啊,错怪你了,是我的问题,抱歉......”
“没关系,现在是否需要我执行close命令呢?”
“嗯,需要执行,不过在那之前,你先帮我把‘abc’写到文件里面去吧。”
“Got it~”
f.write('abc')
f.close()
不久后,open回来了。我手头也没有什么事,和他闲聊的时候说到了这件事。
“抱歉啊,每次打开文件,我都没有关,让你一直守在文件那里,直到程序结束。”
“没事没事,信使不给我报信,我一直以为你还有工作要做,所以我也就一直处于打开状态。”
“这是个问题啊,这次的数据比较小,丢了没关系。万一后面有些核心数据,我write完后忘了close可怎么办~”,我一阵担忧。
“的确是个问题......哎!”open大叫。
“怎么了,一惊一乍的。”
“观音不是给了你很多鳞片吗,让你看情况使用,我们可以约定个规则啊。”
话说,猴哥取经的时候,观音给了他三根救命毫毛,以备燃眉之需。给我的,是三十三片鳞片(关键字,又叫保留字)。和猴哥的不同,猴哥的毫毛用一次少一根,而我的可以重复使用。
“啊~,我都给忘了。”
“给你的鳞片都有什么,我们可以拿来用几片。”open问。
“呀,这个怎么说呢,我记不住所有的鳞片名字呀。太多了,常用的还行。”
“这是个问题......”open愁眉不展。
“嗳~,差点把这事给忘了。我还有请神令呢~”
“请神令?”
“嗯,因为你们土地在地下,我们直接喊来帮忙。但是如果喊天将来帮忙,就需要“请神令”了,这是开始如来给我的。”
“你要请谁过来呢?”
“keyword!”
“哦~这位天将是作甚的呢。”
“他记录着我所有的鳞片名字,观音特别封的职。”
“吼~ 快让我开开眼。”
于是我用import请神令,呼喊keyword星君。
import keyword
片刻时间,keyword星君站到了我的面前。
“哦,你好,需要我做什么?”
“那个,鳞片的名字我给忘记了,您帮我看看呗。”
“嗨,多大点事啊,稍等啊~”
星君执行了他的kwlist功能,然后把数据给了我的保镖list暂存起来。
“open土地也在啊,你看到print土地了吗?”星君问。
“没有啊,我们不经常在一起合作。你需要他来帮你把数据输出出来吗?”open回应到。
“是啊,没有他把数据展示出来,你们看不到啊。”星君解释。
“我可以找他来。”我一边说着,赶紧让print土地出来帮忙了。
星君看到print土地开心了,“看来我们又要合作了,小仙忘记了观音给的鳞片名称,你来帮我输出一下,让小仙看看吧。”
“没问题。”print土地回应。
于是二人合作起来。加上list保镖的帮衬~
import keyword
print(keyword.kwlist)
刷刷刷,33片鳞片全摆在了我的面前。
我大喜,对keyword和print说:“谢谢了,帮大忙了。”
“不客气,需要我们帮忙,随叫随到~”,二人同语。
留下open与我二人面对着33枚鳞片:
我们讨论了好久,选择了with和as这两枚鳞片加以利用。
“这样吧,你以后用with来标记我,用as给小信使起名字。之后你用一个 ‘:’ 另起一行,你需要小信使做的所有任务都进行缩进,我和小信使约好,等把你缩进里面的任务都做完,就去执行close指令。我们就默认你做完了,你也就不用操心close了,我们自己来。”open一边说着,一边给我画了张架构图:
with open('test.txt', 'r') as f:
data = f.readline()
data2 = f.readline()
print(data)
print(data2)
“好比这样,当小信使执行完你的data和data2的赋值命令后,他就close回来向我报告。我也就不用一直在文件那里等着你了。”open土地说到。
“真是个好办法,这样我就不用一直担心着close了~,谢谢你啦。”
那天我们把酒言欢,好不痛快。
宏图建好后,我命令下达方式的转变:
(with open结构,当成try finally的简写完全没问题。finally经常用于外部资源的释放,此处就是实际应用之一,对文件进行释放。
有些对象定义了标准的清理行为,无论对象操作是否成功,不再需要该对象的时候就会起作用。with open结构的close功能就是这种清理行为,有些地方叫做“预定义清理行为”。对象是否提供了预定义的清理行为要查看它们的文档。)
这之后的一段时间里,我和open配合的天衣无缝,我们一起做了很多工作。直到发生了那件事,现在开始,我才说到了重点。
那天天气不错,一切正常。我像往常一样喊open来帮忙,我给了open文件位置,告诉他‘r’(read)。土地领命后,告辞前往。不久后,派回来的小信使站到了我的面前。
“麻烦把数据全读出来。”我说。
“要执行read()吗?”信使确认性的问。
“嗯,read。”
“正在执行,稍等~”
过了一段不短的时间,小信使还在读数据,看来这次的数据量有点大啊。我耐心的等着~
不久后,我发现情况有些不对劲,天空变了颜色,黑红色的云,闷声乱叫的闪电。周围的环境变得乌烟瘴气的,静静感受,貌似连大地都有些颤抖起来。
“怎么回事!”我大喊。正当我惊叹的时候,整个世界崩溃掉了(程序抛出异常,中断执行)。所有正在干活的人都被清理掉了。
其实,每次我执行任务的时候,如来都会创造个世界,让我在这个世界工作。完成任务后,我们所有的人退出,如来再把创造的世界收回。下次我有任务的时候,如来还会做相同的事情。
可是这次的不同,因为世界的消失(程序的退出)不是如来做的(程序正常运行结束),而是不知哪里出了问题。果然没过多久,我被人接到了“天庭”,来探讨此次事故的原因。
我和如来老儿踱步来到“天机处”(控制台),这里记录了整个世界的运行状况。出现了问题,“天机处”也会有记录。
我们来到“天机处”的工作间,大屏幕上赫然的闪烁着几个大字: MemoryError
“啥意思?”我疑惑的问。
“我也不太清楚,不过应该不是天庭的问题。走吧,去看看《天地轮回录》,那里面记载了我们整个世界的运行规律。可不止我创造的世界哦~”
那天我没回去,和如来老儿一起读遍了《天地轮回录》,貌似找到了答案。
我们的世界是在一个叫“内存”的家伙肚子里的。平常我们不用的时候,会被放到一个叫“磁盘”的家伙肚子里。需要我们的时候,就把我们带到内存里来,只有在这里,我们才能工作。但是“内存”和“磁盘”相比,虽然速度快,但是小很多。
那,问题就找到答案了。我需要用的数据需要从磁盘带进内存里面, 数据量超过内存的大小后,内存就顶不住了。所以我们的世界抛出了异常,被迫中止。
“得啦,原因也找到了,剩下的工作就是你的了。遇到大数据的时候,不要一次把他们带进来,先带进来一部分。处理完后,再带下一部分,别又‘天崩地裂’了。”如来嘱咐道。
“行,我回去考虑下怎么办。”
“嗳,对了,我最近新封了个官职叫this,你可以用请神令叫他来哦~”
“哦?干什么的”我好奇的问。
“呵呵,你看到自会明白。”
刚出了天庭,我就等不急了,立马使用了请神令。
import this
长见识,this天兵带给我一段话:
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
“吼,有意思,以后下命令就向这个目标努力了”我心里想。一边想着,我脚下也没有停,赶紧回去睡觉。第二天还要和open探讨怎样读大数据的问题呢。
“我们的世界一下装不下那么多的数据,如来让我们每次不要读进来那么多,处理完一部分再读下一部分。”我没说过多废话,直奔主题。
“原来是这么回事......”open摸着他的花白胡须,细细思索。“哎,你前段时间不是用def鳞片搞了个叫‘封装’的东西吗。我们能不能用用它。”
的确,因为我在工作过程中有许多相似的命令,比如我对一个字符串可能做去掉第一个字节,然后去掉末尾字节,最后将字符串里面的‘a’全部去除这么一套操作。虽然有很多字符串,但是操作都是一样的,为了我不用一直发命令,我就把这一套命令封装了起来,我又用了观音给的一个鳞片def。封装的这段命令我叫做“函数”。
def doSomething(string):
blabla
blabla
blabla
我每次对一个字符串处理的时候,直接交给def封装的doSomething这个函数。像这样:
doSomething(‘abc’),我就不用再把命令说一遍了。
“你的消息还挺灵通,但是怎么利用函数呢?”我问。
“记得前一次,我看到你的鳞片里有个yield,我们用下他吧。”open应道。
“怎么用?”
“这样,你的函数一般在最后不是有返回值吗。我们规定在函数里如果有yield,当程序走到yield的时候,就返回yield后面的内容,并且让函数记住执行到哪了。下次再用这个函数的时候,从标记的位置继续运行。”
“嗯?这和解决读大数据问题有什么关系?”
“你脑子怎么回事,我们可以在函数里面写个循环啊。每次就yield一部分数据出来,等我们处理完,再调用那个函数。他就会从上次的位置再yield一部分数据,这不就解决啦?”
“哦!对啊,好办法呢。”
“这样吧,我们把这种使用的时候才生成数据的结构叫generator生成器,顾名思义,就是数据生成的地方。”
“好!”
于是,对于大数据读取问题,我常常会这么处理:
def read_file(fpath):
BLOCK_SIZE = 1024
with open(fpath, 'rb') as f:
while True:
block = f.read(BLOCK_SIZE)
if block:
yield block
else:
pass
我每次先调用这个函数,得到他的一个信使,也就是一个generator对象。然后让信使给我1024个字节。我处理完之后,再从他那里拿一次,一点点的,我就处理完毕了。(哦,对了,这里我又用了一枚鳞片pass,我把他叫做占位符,望文生义,就是占位的,什么也不做。while也是一枚鳞片,他的功能是循环,只要while后面的条件满足,就会一直做他缩进里面的命令。if和else也是,他们起判断的作用。)
后来我们给yield正了名,说明了他的地位:
yield的作用就是把一个函数变成一个generator,在调用函数的时候,函数不会执行,而是返回一个generator对象。这里要区分一个概念,拿我上面的read_file函数来说,read_file是一个generator function,而read_file(‘test.txt’)是调用read_file返回的一个generator。
生成器有个__next__()功能方便我的使用,每次我用他的时候,直接调用生成器对象的__next__()方法来得到他的下段数据。当生成器把所有的数据都给我后,如果我还调用__next__()方法,就会抛出StopIteration异常。
总结来说,为什么生成器能解决这个问题呢?是因为生成器在你需要数据的时候才开始计算生成,而不是一开始就把所有的数据都产出来,放在内存里,等着你来用。
为此,上面还特意授予了个官职,来判断一个函数是不是生成器函数。
from inspect import isgeneratorfunction
就是他,isgeneratorfunction(),人和他的名字一样,那么的直白。是就是,不是就不是。
不久,我在我们的世界宣布了generator的产生,说明了他的功能。就在我刚刚宣布完毕这件事情后,open的小信使就发话了:
“我本身就有这个功能啊!但是我们叫做Iterator,你们说的generator是Iterator的一种,只是更方便使用罢了。”
我看向身边的open,想要得到回应。他却向我摊摊手,看来他都不知道他的信使(返回对象)是Iterator。
“也就是说,我可以直接循环遍历你本身喽?”
“没问题啊。”信使显得很开心
“走,去试试。”
我先创建了个文件,然后写了点内容:
做起了实验:
这里我用到了我的四大保镖之一list,就是在这里叫content的那位,如果后面有机会,我们再来谈谈他的故事,这里只要知道他的append功能是把后面的数据存到他这里就行了。我们先来说open的小信使吧。
结果你猜怎么着?还真行:
小信使还告诉我,collections宫里有位Iterator星君,他就是一个Iterator。你可以让isinstance土地来帮忙,看看我是不是个Iterator。
我赶紧把人家请来,确认了一番。
人家isinstance土地告诉我了:
我从天上找到地下,没想到最开始的小信使就可以解决这个问题。真是——众里寻芳千百度,蓦然回首,那人却在灯火阑珊处。
最后我们总结说下,读取大数据问题的根本方法是,分批次读入。而实现的技术是generator(或者是Iterator),open()返回对象也可以使用,不过每次只返回一行。而用yield生成的generator,我们可以自定义每次读入的数据量。
取经路上,我和土地open的故事在这里就告一段落了。没准以后我们还会出现更多的问题,谁知道呢~