使用复读机来进行听写跟读

0

Posted by conan | Posted in Uncategorized | Posted on 20-07-2011

前一段趁着限免的时候下载了个名叫复读机的ios软件,发现用来学英语真的很方便。在这里把方法分享一下。
复读机的软件介绍在此:http://www.iapps.im/archives/18626
下载地址为http://itunes.apple.com/cn/app/id417332218?mt=8

使用复读机之前首先需要明确的不是软件使用方法,而是英语学习套路。

这个套路中跟读部分取自李笑来,听写部分取自张晓楠,最后我自己进行综合整理而成。

我使用的资料是老托福听力Part C,下载地址如下:

http://www.lixiaolai.com/index.php/downloads

因为其时间控制内容不是lrc的,自己写了个python脚本把里面的txt转换成了lrc文件,这样在复读机里面就可以使用了。

对待每一篇的动作是这样的:

首先是听写,具体要求如下。首先总听两三遍,把握全篇大意;然后每次听一句话,根据听到的信息可以用自己的话来重写这个句子,也可以直接用听到的词来写,遇到听不懂的就用声音标记来记录这个词,每句话大概听6,7遍就到极限了。遇到不认识的单词就记下来,一篇文章听写3,4遍基本上就全熟了。
个人说法比较简略,没看懂的见听力方法原文如下:

http://blog.sina.com.cn/s/blog_4fe4f4760100adg9.html

使用复读机进行听写,在设置中关掉连续播放,然后按一下上面的放大镜图标隐藏掉里面的英语原文就可以开始听写了。软件升级之后进行单句播放非常的方便,只要从第一句开始听,如果还需要再听就点下面的“重复上一句”按钮,如果这一句听的足够了就点“播放”按钮来播放下一句,这样就完全可以很方便的使用上述方法进行听写了。
reciter

听写进行完之后是跟读模仿。
和听写类似的方法,开始是跟读,即听一句,跟着读一句。读熟练之后可以听一句跟着背一句,彻底熟练之后就可以整篇背诵了。
跟读的细节见跟读方法原文:

http://www.lixiaolai.com/index.php/archives/4054.html

http://www.lixiaolai.com/index.php/archives/4055.html

这个过程中复读机软件的使用方法和听写的时候一样,以句子为单位的重复阅读。

整个过程可以练习听力(因为有精听,而且是整句理解能力),口语(跟读和背诵会让你习惯说英语),部分写作能力(听写的时候会去熟悉句子,如果你用自己的话来重复的话还有复述能力),阅读(也可以这么说把,因为这个过程中你会接触到很多的新单词,新句子,只不过不会去阅读太过复杂的句子)。整体来说是个全部能力综合提升的过程。

动态数组

4

Posted by conan | Posted in 读书笔记 | Posted on 10-04-2011

自己感触,有错的还请大家指出

动态数组:
动态数组只写了三遍,发现一些问题总结如下。

设计的动态数组和之前的双向链表一样,也是浅拷贝的数据结构。

  • 第一遍用malloc,free,memmove等函数来完成的(注:memcpy和memmove的区别是memcpy没有考虑内存重叠的情况)。
  • 第二遍发现memmove的操作并不怎么直观,所移动的size每次都要写的比较长(sizeof(void *) * n),于是实现了一个elem_move的函数,使用index进行移动。同时替换malloc和free所进行的内存扩展为realloc,在使用realloc的过程中没有仔细阅读realloc的函数原型,第一次使用时居然认为肯定原地扩展了,虽然运行起来没发现错误,还好被valgrind查出来了。在此严重推荐一下valgrind,检查内存泄漏的一个很不错的工具,帮我查出很多次错误了;
  • 写第三遍是因为发现了浅拷贝的一个问题。如果get了其中一个elem然后set到不同的index下,那么原来在此index下的elem会发生内存泄漏,而被set过来的elem则会在销毁的时候被free两遍。为了改善这个这个问题,在数组内引入了reference count的概念。我在数组初始化的时候引入了ref_ch和ref_eq函数,调用者自己完成这些函数,并且在自己的数据结构里面加上reference count,根据自己的elem结构对reference count进行操作,并且在insert和set_by_index的时候进行reference count加一,elem_destroy的时候先减一,判断为0了再free。

最终结构是这样的:
数据部分:

struct _DArray{
	void **data;//用来存放动态数组的首指针
	size_t length;//数组当前长度
	size_t size;//数组目前的可用大小

	void *ctx;//进行free的上下文
	DArrayFreeFunc free;//自己的free函数
	DArrayVisitFunc ref_ch;//修改reference count的函数
	DArrayCmpFunc ref_eq;//判断reference count是否等于一个数的函数
};

内部方法:

//销毁一个元素,data为所要销毁的elem的指针。具体操作为调用
//ref_ch((void *)-1, data)减少其ref count,然后检查是否为0,
//是则free掉,否则只是减一
static void darray_elem_destroy(DArray *thiz, void *data)
static size_t darray_get_size(DArray *thiz)
static Ret darray_set_size(DArray *thiz, size_t size)
static Ret darray_set_length(DArray *thiz, size_t length)
//扩展当前数组,函数判断若inc+length>size了就扩展
static Ret darray_expand(DArray *thiz, int inc)
//收缩,若数组长度已减少到一半又少了rand(),则缩减数组尺寸为一半
static Ret darray_shrink(DArray *thiz)
//元素移动,用于中间的插入和删除。n为需要移动的元素个数。
static Ret darray_elem_move(DArray *thiz, size_t des_index, size_t src_index, size_t n)

外部方法:

//创建动态数组实例,返回此实例的指针,参数为该数组用的元素free函数,
DArray *darray_create(DArrayFreeFunc free,
		DArrayVisitFunc ref_ch,
		DArrayCmpFunc ref_eq,
		void *ctx);
//把data插入对应index,具体操作为调用expand(thiz, 1),elem_move, 设置值,
//ref_count++, set_length
Ret darray_insert(DArray *thiz, size_t index, void *data);
Ret darray_append(DArray *thiz, void *data);
Ret darray_prepend(DArray *thiz, void *data);
//删除某个元素,具体操作为elem_destroy, elem_move, set_length, shrink
Ret darray_delete(DArray *thiz, size_t index);
//把某个元素的内容设置为data
Ret darray_set_by_index(DArray *thiz, size_t index, void *data);
//获取某个index对应的元素,将其指针赋到data上
Ret darray_get_by_index(DArray *thiz, size_t index, void **data);

size_t darray_length(DArray *thiz);
Ret darray_foreach(DArray *thiz, DArrayVisitFunc visit, void *ctx);

//顺序调用elem_destroy, free掉所有元素,并把指针全部置为NULL
void darray_destroy(DArray *thiz);

在这个过程中同时折腾了好多的东西,感觉也很有意思。

  • 关于malloc来的内存没有初始化,原因是这样的:我原来认为malloc和free是c用来提供内存和释放内存的函数,其实更准确的说它们是管理内存的函数,它所做的事就是管理内存。malloc是找到一块没有其他人使用的内存,free是把这块内存标记为没有使用,至于内存中的内容?抱歉,这两个函数是不管里面的内容的,free掉的内存中甚至还可能有内存控制块的信息。这样的话所谓的“未初始化”的内存含义就是可能有人用过,可能没有人用过,至于里面的内容,谁知道是什么,可能刚刚有人拿它来存整数,也可能存指针了,也可能真就是空的。(此处提一嘴,强烈推荐K&R的C程序设计语言,这本书中居然还有malloc和free的实现!)
  • 关于struct,猜测内存对齐的原因及结构体访问的方式。新建一个struct实例的操作很简单,malloc一块空间,地址赋给struct类型的指针。就这么简单,这是怎么回事呢?从刚才的说法可以了解malloc来的内存其实是未初始化的,里面什么东西都有可能,那么我们调用其成员的时候系统是如何知道什么是它的成员呢?猜测:其实系统不知道,系统做的只是访问某块内存,对其读取或者写入,系统所了解到的“成员”只不过是这个“成员”相对于结构体指针地址的偏移量,为了方便管理和计算偏移,在编译的时候把它们的偏移量都统一确定了。(暂时不好查阅资料,内存对齐的话题看来要留到以后学习了)。

排序算法
这章中重新写了三个排序算法,冒泡,快速,归并。事实证明,我对这三个算法的熟悉程度并没有到“技能”的程度,平均一个算法写了一个半小时。这里将三个排序算法的关键点记录一下。

  • 冒泡排序:我采取了改进的冒泡法,即每次冒泡都进行到上次交换过的最后一个。此方法主要有三个变量,一个遍历用的变量,一个记录单轮冒泡过程中做交换的元素的位置,一个用来记录已经有序有序的元素的位置(它等于上一轮最后那做交换的元素的位置)。
  • 快速排序:主要思想是每轮都把一个元素放到最终位置上,并且保证比它大的和比它小的元素分开两边。这个算法有两个特殊情况需要处理,选取的标准元素是这一堆中的最小值,或者是其中的最大值,写条件的时候主意这两种情况外加不要越过左右边界,快速排序就不会有什么问题了。
  • 归并排序:它主要是两个动作,首先是递归,然后是合并有序数据。递归出口是函数参数包含的数据段合并完成,初始递归出口是数据段内只有一个元素。每个数据段都是闭区间,所以不要把某个元素包含到两个数据段中去。用来遍历的变量i超过边界是没有问题的,只要超过之后不会再次被使用。

走近专业程序员

0

Posted by conan | Posted in 读书笔记 | Posted on 19-03-2011

最近开始看李先静的《系统程序员成长计划》,算是对长久以来缺乏编码的一种补偿把。使用这本书顺便还为了养成编码的习惯,免得见了代码就头疼。养成一个习惯有三个阶段,第一个阶段是需要一直提醒自己,而且觉得很别扭,第二个阶段是需要一直提醒自己,但是已经比较习惯了,第三个阶段是不再需要提醒自己,而且习惯了。纵观以往的努力,中断于第二阶段的情况太多了,因为已经比较习惯了,所以总会去放松,不小心就中断了。这个时候中断太可惜了,既浪费了前面的努力,也浪费了来之不易的良好状态。这次引以为戒,坚决不能再次中断。
目前进度,两周,第一章结束,通用双向链表写了五遍,虽说还不能将双向链表烂熟于心,也算有了一定的经验了。

专业程序员最需要的是什么?是专业态度。

专业态度一:风格态度。良好的代码风格是严谨的一种表现,“傻瓜都能写出机器能读懂的代码,但只有专业程序员才能写出人能读懂的代码。”

稳定的代码风格:
文件名:单词小写,多个单词用下划线分隔。如:dlist.c
函数名:单词小写,多个单词用下划线分隔。如:node_find。一个函数只完成单一功能。一个函数最好在80*24的范围内完成,函数内的临时变量不要超过10个(此句来自linux kernel coding style)。
结构/枚举/联合名:下划线开头,首字母大写,多个单词连写。如:struct _DListNode;
宏名:单词大写,多个单词下划线分隔。如:#define MAX_PATH 260
变量名:单词小写,多个单词下划线分隔。如:DList *node = NULL(此处习惯*与node相连沿用于K&R C,表明*node是DList,这种写法有利于同时定义多个变量。)

面向对象的命名方式:
1.以对象为中心,采用主谓形式(对象+动作)来命名,取代传统的动宾形式,如:dlist_append
2.第一个参数为对象,并用thiz命名,如:dlist_append(DList *thiz, void *value)
3.对象有自己的声明周期,所以有对应的创建和销毁函数。

排版:
使用空行:1.函数体之间。2.结构/联合/枚举声明。3.不同功能的代码块之间。4.功能类似的代码放在一起,和其他部分用可能空行分隔。5.用一行就够了。
使用空格:1.等号两边。2.运算符两边。3.参数之间(for,函数等)
使用括号:
1.分隔子表达式,让人更容易看明白。如:((a && b) || (c && d))。
2.条件判断和循环,即使是一句也要加括号。如:

1
2
3
4
5
if (a > b) {
return c;
} else if (a == b) {
...
}

缩进:使用tab。缩进超过三层的话说明设计有问题

保护隐私:封装。可以隔离变化,降低复杂度。
隐藏数据结构:若为内部数据结构,则直接放在C文件中,不要放在头文件里。若为外部也要使用,则对外暴露名字而封装实现细节。做法为:
在头文件中声明该数据结构。
在C文件中定义该数据结构。
提供操作改数据结构的函数,避免直接访问其成员。
提供创建和销毁函数。
不过是否封装还是要看具体情况。
隐藏内部函数:在头文件中只放最少的接口函数声明,在C文件中所有内部函数都加上static关键字(将函数private化)。
禁用全局变量。

通用化
通用链表的实现方式有两种:
深拷贝,存值,以void *来保存数据首地址,size_t length来保存数据长度。进行深拷贝的时候使用memcpy。C语言没有构造函数,实现深拷贝比较复杂,且复制数据带来性能开销,不符合C语言的风格,较少见。
浅拷贝,存指针,只用void *保存对象的指针。存放整数时,可以把void *强制转换成整数使用。
(注:是否要进行深拷贝要根据实际情况来决定,在需要的时候,比如防火墙处理数据包,还是需要做深拷贝的。另外,据说公司前辈说自己实现的拷贝函数性能比memcpy好。)
让C++可以调用:在头文件中防止编译器对函数名重新编码,加入如下内容:

1
2
3
4
5
6
7
#ifdef __cplusplus
extern "C" {
#endif
/* add your code here */
#ifdef __cplusplus
}
#endif

通用链表面向特定数据的操作
面向特定数据的操作会很多,但是若将其一一实现的话会产生大量重复的代码。将遍历和对数据的操作分离开,使用函数指针将对数据的操作传入就是一个良好的实现框架,称为回调函数法。使用回调函数来进行数据操作的时候会产生一些中间数据,对此引入一个上下文context(缩写为ctx)的概念,用它来做参数负责传入传出数据和保存中间变量。框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 //定义函数指针别名
typedef STAT (*DListVisitFunc)(void *ctx, void *data);
//实现遍历
STAT dlist_foreach(DList *thiz, DListVisitFunc visit, void *ctx)
{
/* some code */
stat = visit(ctx, some_data);
/* some code */
}
//实际回调函数
static STAT sum_cb(void *ctx, void *data)
{
/* some code */
}
//调用方法
long long sum = 0;
stat = dlist_foreach(pdlist, sum_cb, &sum);

面向特定数据的操作不应该杂糅到通用链表中,否则会造成不必要的耦合度和复杂度。

九寨游记

0

Posted by conan | Posted in 旅游, 生活小趣 | Posted on 01-07-2010

想来一趟成都,最终还是来了。
只有一周,飞机来回,被人说实在太奢侈了。

自己拿主意,反正只有一周,景点还是一个玩够了吧。九寨沟,青城,峨眉,还是九寨沟吧~虽然挺费时间的,毕业旅行,还是找个比较好玩的地方。

自助?跟团?熟人帮我联系了一个团,然后某人极力推荐我自助。反正没有自助过,好歹疯狂一下,联系一下自助吧~跟着攻略一路查找,结束之后发现也不难么。开始喜欢这种自助游了,玩的实在太high了~

首先预处理工作,同学推荐了天堂青年旅社(http://www.ttjzg.com/),这个青年旅社是九寨沟脚下满有名的一个。打电话询问过床位情况之后去成都的新南门客运站买票,买完车票后再给旅社打电话确认床位预定。

第一天,大早晨的跑去新南门客运站坐车,路太绕了,建议不停车的时候不要吃东西,太容易勾引出来想吐的感觉了。晚上7点左右到九寨沟,找到旅社入住,然后在旅社办理入住手续,沟内住宿手续,车票代购手续。下车的地方在九通宾馆,从绵阳过去的车好像并不路过天堂青年旅社,下车之后往上走吧,大约两公里,具体走法在他们网站上都有。懒得走的话打车也行,估计8块左右。值得一提的是下车的时候外面围了一圈各种各样的青年旅社来拉人入住的,挺住啊~

第二天,进沟。旅社跟我说8点多正好是旅行团大批进入的时候,既然我要在里面玩两天,就9点前进去就行。结果我8点半出发,打车5元到了景区门口,正好是旅行团高峰期。在服务中心拿了个宣传资料,然后拿学生证去买了学生票。进门的时候跟着大队旅行团进去的,结果连学生证都没查验(检票系统语音提示要检查学生证了)。由于根本没来过,路也不熟,反正有两天时间呢,先跟着旅行团溜达一下吧。于是进门之后跟着旅行团大队坐车到了树正。(从门口往上走的车有两种,一种是到树正,一种是到山顶,车前面写着到树正的是前一种,没写的是后一种。)在车上路过芦苇海,玉带河,双龙海,卧龙海。坐在车上看玉带河还是很有意思的,矮矮的芦苇之中一条宝蓝色的河道从中穿过,很是漂亮。到了树正之后下车,树正寨就在旁边,晚上就住这里了,进来之前还怕找不到地方。往左走是树正群海,下了石阶就是水磨坊。到了这里我才知道九寨沟原来是藏族的村寨。水磨坊全是人,往上走树正瀑布也全是人,继续走。老虎海和犀牛海还是不错的,毕竟人只能分布在一条线上,哈哈。在沿岸拍点风景,继续走到犀牛海坐车。这次坐车直接送到了顶部的长海,路上的上季节海和下季节海现在都处于干旱期,基本没有水。从长海往下走看过五彩池后坐车到中间的诺日朗服务中心,开始进食。值得一提的是这个服务中心有餐厅,里面的餐食价格基本都在50以上,门口还有卖方便面的,¥15一桶送一根香肠而且给泡好。我在吃东西的时候闻到各种各样的方便面味道,真是太诱惑人了。吃过东西之后继续坐车,这次直接到了另外一条沟的最高处原始森林。原始森林还是看过比较多的,这边的也没有太大特点,从原始森林的栈道上走一圈,然后下来走到芳草海和天鹅海。再往下走需要走的距离比较远,于是坐车到箭竹海,一路步行下来到五花海。走在栈道上突然下雨了,于是打着伞在栈道上继续向前,栈道有点湿滑,走的小心翼翼的。走到五花海的时候很幸运,由于下雨那里都没什么人,连租服装照相的都收摊了,于是看到了很漂亮的雨后初晴的五花海,哈哈~~看完之后觉得下雨走的很不爽,于是从五花海出去坐车,然后直达珍珠滩。下车后遇到两个导游,听说我一路走下来的,觉得我很厉害,哈哈哈哈。珍珠滩是西游记的外景地之一,是个斜坡式的瀑布,白色的水花确实很像珍珠。从珍珠滩一路走到镜海停车场,顺便看到了雨后的镜海。(后来我倒是觉得雨后的镜海是一点都不比早晨的镜海差。)继续坐车到诺日朗瀑布拍照后上车回树正休息。

一年前刚去过贵州,风景秀美,水也是清澈怡人。不过和九寨沟相比的话那里的水一般只是蓝和绿,而九寨沟的水看起来则是色彩斑斓,辅以水中生长的植物,非常的赏心悦目。车上的导游说九寨风景是晴看水,雾看山,半晴半雨看云烟,而来到九寨沟第一反应一般都是这里的水如何如何,这样就很容易忽略了山和云。绿树和青山未必独立成景,然而辅以云烟或者配上海子确是秀美绝伦。

第三天,继续。大早晨起来跑出去看静海。从树正出来沿着第一天的路过去,磨房,树正瀑布,老虎海犀牛海全部都没有人,而且早晨景色超好,太爽了,哈哈。不过路实在好远啊,大早晨的没吃饭实在懒得走,找到巡逻的补了个车票,然后坐车到了镜海。镜海确实应该是没有风的时候看才会比较好,一旦有风起了水波,倒影就完全混乱了。早晨镜海的倒影不错,但是因为没有阳光不如昨天看到的雨后镜海那么透亮。不过有一点晨雾还是挺有感觉的。坐车回树正吃早餐,吃完之后有了力气,打算用和昨天不一样的方式来玩九寨沟。于是。。。我选择了走,哈哈,反正路也熟悉了。于是坐车到珍珠滩,开始一路往上走了。从珍珠滩一路步行到箭竹海,开始的时候人还很少,偶尔碰到几个人也还是外国人比较多,走到后面是越走人越多,估计旅行团是上来了。这段基本以看为主,反正照片昨天都拍过了~有一点心得就是,往上走比往下走累好多啊。本来想坐车到天鹅海然后继续步行上去,但是发现车到那里并不停,于是只好在车上多看两眼了。坐车到了原始森林,毕竟昨天都看过了,这一段也不怎么出彩,于是到休息站进餐,所以我就在九寨沟最高点吃了一顿饭,哈哈。吃完东西之后想到这条沟我两遍都是走的,坐车的观景点都没看,反正下去也要坐车,顺便再看一遍吧。继续坐车把这条沟的下车点的观景点看了一遍。值得一提的是可以俯瞰五花海全貌的老虎嘴观景点由于修缮工作不能停车,于是错失了拍照的大好地点,只好在下车点的公路上拍了两张五花海的半景。到了珍珠滩之后觉得镜海到诺日朗那段栈道没有走过,于是从珍珠滩走到了诺日朗的停车点,中间栈道上又看到不少老外。坐车坐到犀牛海,这次改往下走,路上的芦苇海很漂亮,但是好像没有看到玉带河。走到一大半的时候觉得好累啊,但是怎么都走不到头。上车点明明就在公路另一边,就是没有可以横穿的栈道。走到盆景滩的时候终于可以过去坐车了,这时我是实在不想再走了。于是在盆景滩坐车到景区门口,结束了两天的沟内旅游。回到旅店,吃点东西跑去上网,遇到一个沈阳过来开会的美女,一边上网一边聊天,最后回去住的时候发现居然住同一个房间。。。当然同寝室还有4个新加坡人(一个男生三个女生)。

第四天,一大早起来打车到九通宾馆,坐车走人~路上的山很漂亮,山顶的云看着很壮观!不过比较煞风景的就是水坝了,路边的河好多干枯的地方,一到有水多的地方基本上就是有水坝在拦水了。

游记写的和流水账似的,将就看吧~

总结:现在的九寨沟其实地方不是很大,就算想用走的也能一天玩的不错。一个人旅游就是拍照麻烦。可以借鉴的关键事项:去之前头一天打电话订好旅店,去新南门客运站买好车票,找好怎么到旅社的路。到了旅社之后就要计划好主要事项了,玩几天,怎么住,怎么返程之类的。九通车站是头一天放票,旅社的人可以给代买票,收¥10手续费。我在里面玩两天,肯定不能出来买票,让他们代买还是很方便的。关于在里面玩,其实一天足够了,不过对我来说多一天是多一份安心,第一天怎么都不怕玩漏了,第二天路都熟了,就是放心的四处乱跑了。一天的行程可以大早7,8点钟买票进去,坐到山顶的车,然后镜海下车,看早晨的镜海,如果你有体力的话建议你从镜海步行上去,到箭竹海后坐车上行到顶端的原始森林,然后走下来到天鹅海,接着坐车看这条沟的停车观景点。这条沟是重点,不玩就白来了。然后如果有时间可以坐车去长海和五彩池,然后坐车下来到犀牛海往下走到盆景滩结束旅程。不过这样这条沟的停车观景点又会忽略掉,如果知道怎么停车的话就先用与停车反向的方式走栈道,然后坐车停观景点就完美了。虽然坐车的时候看到了,不过我还是觉得忽略了火花海,卧龙海这块地方的停车观景点,唯一的缺憾吧。

对于摄影最大的收获就是我会用光圈优先了,哈哈~~上次去集安学会的用曝光补偿,继续慢慢学习摄影知识中。

python+pygtk+glade3编程中的白痴问题

0

Posted by conan | Posted in Uncategorized | Posted on 15-05-2010

近期使用这三个工具构建一个gui程序,遇到了若干问题。之所以称之白痴问题,是因为你知道这个问题的答案时一定会觉得这个问题问的好白痴

1.用glade3生成xml文件,然后在python中载入。运行之后没有窗口出现,按ctrl-c结束,停在gtk.main()那里。这是个什么问题呢?百思不得其解。。。直到我看到了这篇文章:http://tristram.squarespace.com/home/2008/5/26/glade-3-pygtk-and-beginning-python.html
原来是mainwindow的公共(common)选项卡下的可见(Visible)属性默认居然是否!!!这个问题真是太白痴了。。。

2.由于有个算法要一直运行,需要多线程,使用后发现一个问题。我点运行了,它不动,我点退出了,算法开始运行了。我晕死。期间使用了无数的测试方法,均没有发现问题所在。吴锡同学到最后说了一句,大概这个也是个初级问题。然后我google一下,发现了这篇文章:http://hi.baidu.com/jzinfo/blog/item/3f7dcc1b149088fbae51337e.html
原来需要在gtk.main()之前调用gtk.gdk.threads_init()这个函数否则gtk独占python解释器!!!果然还是个白痴问题。。。

python之处理文件编码

0

Posted by conan | Posted in 总结 | Posted on 27-04-2010

做了手机天气预报和rss generator,有必要把处理字符串信息过程中遇到的问题总结一下了。

在做这两个东西的时候对python有了一些初步的理解,python用起来真的很舒服。数据结构,函数方法等都能够让你写出简短易读的代码。做天气预报的时候出现的问题比较简单,因为信息处理都是自己做的,只有发送的时候使用飞信库,所有抓取的信息甚至都不用解码,在程序里处理后直接发送出去就行,需要注意的就只有程序里写的字符要使用unicode,发送前进行编码就行了。做RSS生成的时候使用了PyRSS2Gen库,最后写入文件是使用此类库的函数进行的,在认知上出了点偏差,发生了不少的错误,饱受折磨好几天。

简而言之,把握住以下几点,在python中进行信息处理的时候应该不会再遇到这么多的信息编码问题。
1.在取得信息的时候(从文件,网络中)尽快进行解码,在程序中使用被解码的信息进行处理,在信息被写出去的时候(写到文件,网络等)再进行编码。
2.关于编码和解码,概念一度被我理解的比较混乱。首先是这个:

>>> “a”.decode(‘ascii’)
u’a’
>>> u’a’.encode(‘ascii’)
‘a’

这个到底怎么理解呢?我一直认为unicode和ascii是一个类别的,类似raw data的那一种,现在发现不对。因为unicode只是规定了字符集,但是没有规定存储方式,所以只有unicode是类似于raw数据的,它离开存储方式无法生存,而ascii则字符集和存储方式都有,所以说unicode可以作为一种内部数据处理方式,但是不能用来存储。所以,u’a'是unicode字符串,它被ascii进行编码之后出现ascii编码下的a,而’a'也可以被解码为unicode的字符串u’a'。于是在处理数据的时候,其实ascii是应该和utf-8,gb2312这种编码处于一个类别里。由于历史原因,ascii不管在什么地方都会获得良好的支持,所以ascii的信息不经过任何编码解码处理都不会有什么问题,而网络上编码千变万化,比如在中文互联网中gb2312和utf-8就经常相互打架。总结一下,编码和解码的过程应该这样处理,如果是ascii编码,完全不用理会编码解码的问题,而如果是utf-8或者gb2312这种,在取到的时候应当对其进行解码,这样数据就变成了unicode字符串,它适合在程序内部进行处理,在需要对其进行输出的时候,把它进行编码,需要utf-8的就编码为utf-8,需要gb2312的就编码为gb2312。

在rss生成的时候遇到的UnicodeDecodeError的问题,最后确认是因为对某些数据提前进行了encode操作,而写操作的时候依然需要进行encode操作。在python中,已编码的字符串也有encode方法,其操作是按照默认的ascii编码进行解码操作,然后再进行encode[2]。已编码为utf-8的字符是无法被ascii编码进行解码操作的,于是这个过程中就是把编码过的数据当作未编码的数据,试图对其进行解码导致了错误。参考如下,取自第二篇:

Why do Python byte strings have an encode() method?

The sharp-eyed amongst you will have noticed that byte strings have an encode() method as well as a decode() method. What does this do? Quite simply, it does a decode-then-encode. The byte string is decoded to Unicode using the default (ascii) encoding, and is then encoded to the target encoding specified in the call to encode() using the appropriate encoding. As you’d expect, fun and games ensue if the original byte string isn’t actually encoded in ASCII at all.

最终总结:对于python2.5而言,字符类型就两种,str和unicode[3],下图中左边是str,右边是unicode。
ascii—————–decode(‘ascii’)——-┐
utf-8—————–decode(‘utf-8′)——–┼—unicode
gb2312————decode(‘gb2312′)—┘

反之为encode操作。

参考:
1.python核心编程第二版
2.Python, Unicode and UnicodeDecodeError,地址在这里(英文,讲的很好)
3.Re: u’吴’和’吴’有什么区别? ,地址在这里

孩子的教育之国内教育靠不住(初稿)

7

Posted by conan | Posted in 思考 | Posted on 22-03-2010

题注:本文是一个计算机硕士所写,他没有学习教育学知识,写的不够专业或者写错了的地方麻烦指出

首先,有很多人跟我说,你连女朋友都没有呢,想孩子的教育是不是太早了点阿。我觉得一点都不早,思考孩子的教育同时就是在总结自己的为人处事和反思自己的成长经历。

我是极其不放心把孩子扔给中国中小学教育的,这一点我几个好友基本达成共识了。总结一下的话,从传统上来说,教育是教书育人的过程,然而现在大部分的中国教 育只管教书,不管育人,学校里为了容易管理,老是想出来各种稀奇古怪而且磨灭孩子天性的方法。通俗一点说,就是学校和老师根本不管孩子的心灵成长。

如果不考虑行为的对错,人是不是能够坚定地执行某个行为取决于他自己是否认为自己的行为合理,而这一点很大程度上取决于他自己能不能找到一种自己认为合理的逻辑来支撑自己的行为。我家有一个我相当欣赏的小学生,他有 自己的思 维方式,能够自己独立思考,然后以相当高的分数考上了市里有名的重点初中。但是今年回去的时候,他也说,在他们中学里面,教育方式还是那一套。他们老师有 了孩子,才两岁,然后老师就拿管理自己孩子的方法来管理他们,对他们说课间时间不能越过楼梯,否则监视器里面我会看到,跟我小时候 相比除了多了个监视器以外没什么进步。在班里安插卧底这件事他的老师也在干。其实这件事要不是某一部电影我也不会反应这么强烈。上个学期看了闻香识女人,最后在礼堂上那段慷慨激昂的演讲让我一身冷 汗。我小时候虽然不是个标准的好学生,但是除了去打电动游戏以外没有什么出格的事,于是小时候我是个坚强的告密者,并且活在告密的“白色恐怖”中。当时的 思维很红很专,只要你做好自己的事,而且问心无愧,谁还去怕别人告密啊。但是看完这部电影,我才明白我错得有多么离谱,我们根本就是混淆了是非,然后用错误的逻辑来支撑错误的行为。坚守原则,不出卖同伴,这是非常难得的优良品质,看完后我为我小时候的行为感觉到深深的羞愧。

从这一点来反思中国的教育,我们 从小就是鼓励告密的。我没有告密告到现在,纯粹是因为上了初中之后我“学坏”了。也就是说,我小时候是用告密的逻辑来支撑自己的行为并且心安理得,但是我 直到研究生快毕业了才发现自己的逻辑是错的多么的离谱。另外这些纠正都是建立在我愿意思考人生的基础上的,不会思考的人很多,他们或许就会成为一辈子的告密者。小时候的是非其实记忆很深刻,如果我没有看到这部电影,并且我不愿意思考,不愿意去怀疑小时候的教育,那么我还是会告密下去。所以说如果一个人真的顺 从中国教育走到了最后,那么得到的很有可能是一个智商很高,但是情商很低的人,他不能独立思考,没有自己的人格。确实,这样的人很容易被政府管理,但是他 没有了人的灵魂,所有情况的处理都 是父母学校教的,离开了这些就不会思考,同时没有自己形成的原则。

这个例子只是管中窥豹,在中国的教育中这种为了方便管理而想出来的磨灭孩子未来可能性的方法还多的是。所以,关于这些原则家长必须早教育,因为你不教育,就会有别人或者别的事去教育。我的家长也不是这种观念,我的老师们也没有持这种观念 的,我为什么能形成这些呢?就是因为我看的东西。所以从家长的角度来说,他们掌控不了我的发展,同时由于没有足够的心灵上的沟通,他们也掌握不了我的心里 到底在想什么。同时,我看的东西也许是我决定的,也许是随机的,不过你能放心让孩子随机的发展么?显然没有一个家长会愿意,但是绝大多数的家长意识不到这样会 导致孩子的发展是随机的,例如用我妈的话说,这些东西你“慢慢就会知道了”。是的,还好我慢慢的随机的学到了这些,没有随机的跟着小时候的一些不好的习惯越走越远。

记得曾经有个电视节目里面出现过一个台湾的老师,她教育出来的孩 子在和别人对话的时候说了这么一句。别人说,咱们下午去玩吧,她的孩子说,不要,我和我爸爸有个约会,然后别人说,你真可怜,居然和你爸爸有个约会,她的孩子说,你才可怜呢,你和你爸爸没有约会。这段对话我非常的喜欢,它反映出不仅这个家庭沟通良好,而且孩子有自己的思考,他可以用一个极其温馨的逻辑来支 撑自己的行为。

国内不是没有好老师,但是完全靠运气,我有个同学就遇到了一个好老师,我十分的羡慕他。我认为一个老师能被评价为好老师,首先它要从心里去爱护 自己的学生,注重孩子心灵的成长,而不是纠缠于丢个钢笔的事儿去冤枉别人。从小就不公正,而是人云亦云,被小孩子看在眼里,所造成的后果最少会一直影响直 到有独立思维能力。然而什么时候会有独立思维能力呢?其实我看到很多成年人一样没有。。。
协助论证文章一篇:<中国孩子>http://www.dapenti.com/blog/more.asp?name=xilei&id=27864
这是个逆境成才的例子,如果让我选择的话我不愿意选择让孩子这样成才。其实是不是成才也没有关系,重要的是孩子要生活得幸福快乐。

尾注:对于无理取闹的,赠送一句话。你有孩子吗?如果你有孩子,或今后将要有孩子,我想你能够理解一个未来总会成为家长的人对因学校的错误教育而对孩子产生终生影响的担忧和关切。

Python的变量名

0

Posted by conan | Posted in 总结 | Posted on 25-09-2009

最近用python重写了天气预报,对python的变量名有了一些认识。

python的变量名和以前接触过的c/c++等完全不一样。c/c++的变量名的思想应该是这样,分配之后变量名就永远的关联到了编译器分配到的内存,任何对此变量名进行的操作都最终落实在对这段内存的操作上。

python中的变量名更像lisp的感觉,内存和变量名的关系并不是永远“绑定”的,这个变量名更像是一个指针,它指向了那个“变量”。

拿最简单的a=1来做例子。在c/c++中,=的意义是赋值,它对应的操作是把1赋值到a所指向的内存中。1这个表示在c/c++中是一个int型整数,存到a对应内存的是int型的整数1。为什么不同类型的赋值会出错?因为那个变量所指向的是一段可存储被赋值类型的内存空间,无法保证这个不同的类型在那段内存不出现问题。(由此引出的类型转换暂不考虑)如果再用b=a,则是将a的内容复制到b中去。在python中,=的意义虽然也是赋值,但是它做的完全不是这样的操作。根据python的声明,一切都是对象(变量名是不是还没有体会到),那么这个1也是对象,a=1就是将a指向1这个对象,而再用b=a则是将b指向a所指向的内容。那既然a和b都指向同一个地方,为什么修改了a之后b不变呢?举例,执行a=2,再看b的话你会发现还是1。其实这个还是惯性思维的原因。a=2实际上执行的操作是将a指向2这个对象,而不是将a所指向的内存改为2,所以a指向了2,而b还是指向1。可以体现这一点的比较容易的方法是用字符串。如果你用下标改了字符串的内容,两个“变量”都会变。

所以在python中,变量名不如叫名字(name)更为贴切,而在这种机制下,名字空间就显得尤为重要。

注:此文没有经过c/c++编译器及python解释器的证明,如有错误请指出,多谢

[编程珠玑第二章]啊哈,算法

3

Posted by conan | Posted in 读书笔记 | Posted on 16-09-2009

看完编程珠玑第二章,发现确实很好玩。第二章用三道题目引出三个基本操作,然后再将这三个操作推广到更一般的情形。

  1. 二分搜索:将线性搜索的O(n)时间降到O(log2n) ,要求单调
    1. 看这句话似乎很平凡无奇,但是这句话确实是二分搜索适用范围的精确表达,关键在于线性搜索四个字。你明确知道什么样的是线性搜索么?我不敢说我很明确,但是在这里我把它总结为在线性解空间里寻找解。曾经有人给我讲过二分枚举法,一个奇形怪状的容器,问水面多高的时候水是总容积的一半。这就是个线性有序的解空间,可以进行二分搜索。如果你可以求得高与体积的关系式,那么直接根据一半体积求高的做法就更像是hash查找。
    2. 二分搜索的核心问题:1.有序2.有个比较“大小”的方法来将解空间每次减半(定义一个范围,在该范围内表示元素的方式,确定哪一半范围存在缺失整数的方法)
    3. A问题,有一个连续的文件里面有40亿个32位整数,找出至少一个不在这个文件里的32位整数。朴素的二分思想,每次找出存在缺失整数的那一堆。
  2. 向量旋转:问题B:用几十个字节的额外空间将一个n元向量x在正比于n的时间内左旋i个位置。对应于实际问题中交换相邻的不同大小的内存块。
    可用方法:

    1. “杂技”算法。以“元素”为单位(元素不仅可以为数组元素,还可以为一切旋转单位),循环移动该向量中的元素,若元素个数为n,移动距离为d,那么需要移动n与d的最大公约数趟。
    2. 等大小内存块移动法。目的:ab->ba,假设b比a长,那么取b后面的与a等长的一段br,与a交换。于是效果就是ablbr->brbla,a已经到了最终位置,剩下的目标就是交换bl和br。递归操作。
    3. 求逆法。(a’b')’=ba
      reverse(0, i-1);     //abcdefgh->cbadefgh
      reverse(i-1, n);     //cbadefgh->cbahgfed
      reverse(0, n);       //cbahgfed->defghabc

    直观上比较,杂技算法对每个元素仅存取一次,而求逆要两次,那么杂技算法算法的速度要两倍于求逆。实际上当n比较大而旋转距离变长的时候,由于杂技算法的高速缓存性能比较差,而求逆大部分操作为顺序读取,由于缓存局部性的影响,杂技算法的实际表现并不如求逆算法。

  3. 标识:当使用等价关系来定义类别时,定义一种标识,使得类中每一项都具有相同的标识,而该类以外的其他项没有,这样就可以将一个感觉无从下手的问题变成一个用已知数据结构和算法解决的问题。(ps:后续还可以根据标识进行排序,二分等等操作)
      问题C,挑选出一个词典中所有的变位词,就是如deposit和topside这样关系的词。如果要考虑单词字母的所有排列,那是注定要失败的方法。因为算法复杂度实在太大了。注意描述,deposit和topside是一类,他们是变位词,也就是说他们存在着某种等价关系。只要给所有变位词定义上同样的标识,然后将其按照标识排序,这个问题就变成了一个很容易下手的问题。

后记:本章主题是寻找合适的算法,绝对不要有了想法就急不可耐的去实现,就是要找到自己的灵机一动,用巧妙的方法来实现,要“建设性的懒惰”。

c语言宏处理器哈希表 & Python字典类

163

Posted by conan | Posted in 读书笔记 | Posted on 10-09-2009

哈希算法,用一个算式result=hash(key)就可以大致确定所需数据的位置,大大简化了数据查找的复杂度。此算法的关键在于建立一种两类数据间的映射,就是那个hash函数的选定。另外有个很重要的问题就是hash冲突,就是不同的key得到了同样的result。常用冲突解决法有拉链法(我认为叫做链表法更容易理解)和平面地址法两种,后面有描述。

c语言宏处理器(出自C程序设计语言)

“这段代码很典型,可以在宏处理器或编译器的符号表管理例程中找到。”

例如考虑define语句。
#define IN 1
需要把名字IN和替换文本1存入到某个表中,此后当名字IN出现在某些语句中时,就必须用1来替换IN。
以下两个函数用来处理名字和替换文本。install(s, t)函数将名字s和替换文本t记录到某个表中,其中s和他仅仅是字符串。lookup(s)函数在表中查找s,若找到,则返回指向该处的指针;若没找到,返回NULL。
该算法采用的是散列查找方法,将输入的名字转换为一个小的非负整数,该整数随后将作为一个指针数组的下标。数组的每个元素指向某个链表的表头,链表中的各个块用于描述具有该散列值的名字。如果没有名字散列到该值,则数组元素值为NULL。
链表块结构:
struct nlist {                /*链表项*/
struct nlist *next;    /*链表中下一表项*/
char *name;            /*定义的名字*/
char *defn;            /*替换文本*/
}
指针数组:
#define HASHSIZE 101
static struct nlist *hashtab[HASHSIZE];    /*指针表*/
散列函数hash,通过for循环将上次计算的结果值变换(乘以31)得到的值加上当前字符值(*s + 31 * hashval),然后对数组长度取模。这并不是最好的散列函数,但比较简短有效。
unsigned hash(char *s)
{
unsigned hashval;
for (hashval = 0; *s  != ”; s++)
hashval = *s + 31 * hashval;
return hashval % HASHSIZE;
}
strdup函数将通过参数传入的字符串复制到某个安全的位置
char *strdup(char *s)
{
char *p;
p = (char *) malloc(strlen(s) + 1);
if (p != NULL)
strcpy(p, s);
return p;
}
lookup函数发现表项存在,则返回指向该表项的指针,否则返回NULL。散列过程生成了在数组hashtab中执行查找的起始下标,若该字符串可以找到,则它一定位于该起始下标指向的链表的某个块中。

struct nlist *lookup(char *s)

{
struct nlist *np;
for (np = hashtab[hash(s)]; np != NULL; np = np-> next)
if (strcmp(s, np->name == 0))
return np;    /*found*/
return NULL;        /*not found*/
}

install函数借助lookup判断待加入的名字是否已经存在。若存在则用新的定义取而代之;否则创建新表项。若无足够空间创建新表项,则install函数返回NULL

struct nlist *lookup(char *);
char *strdup(char *);
struct nlist *install(char *name, char *defn)
{
struct nlist *np;
unsigned hashval;
if ((np = lookup(name)) == NULL){    /*not found*/
np = (struct nlist *) malloc(sizeof(*np));
if (np == NULL || (np->name = strdup(name)) == NULL)
return NULL;
hashval = hash(name);
np->next = hashtab[hashval];
hashtab[hashval] = np;
} else                                 /*already exist*/
free((void *) np->defn);    /*free previous defn*/
if ((np->defn = strdup(defn)) == NULL)
return NULL;
return np;
}
三点疑问:
1.hash函数如何选的?
2.hashsize如何选定?
3.作为宏处理器,又用了宏定义?

Python字典类(代码之美 第18章)

“字典是python语言的基本数据类型,它的作用就像awk里的关联数组或者perl里的哈希表”
python的字典类实现中一般有以下内容:
int ma_fill        13
int ma_used    13
int ma_mask    31
PyDictEntry ma_table[]:
[0]: aa, 1        hash(aa) == -1549758592, -1549758592 & 31 = 0
.
.
.
域成员的名字前缀ma_的意思是映射,在Python里它专指那些提供关键字/数据元素匹配功能的数据结构。域成员:
ma_used    有关键字的存储单元数量。加入关键字增1,删除减一。
ma_fill        有关键字存储单元和哑存储单元的总数。删除关键字的时候不变化
ma_mask    对应哈希表大小的比特掩码。哈希表有ma_mask+1个存储单元。规定存储单元数永远是2的幂,那么ma_mask就是2**n – 1
ma_table    指向PyDictEntry结构的数组。这个结构包含几个指针,分别指向关键字对象,数据元素对象,保存关键字哈希值的缓存。缓存哈希值是为了提高速度,如字典大小变化的时候。
为小哈希表而做的特别优化
PyDictObject还包括一个拥有8个存储单元 的哈希表。不多于5个元素的小字典可以直接放在这张表内,从而免去了调用malloc()的额外开销。同时这样做还提高了缓存的局部性,拿x86 GCC编译的PyDictObject来说,它的大小是124字节,刚好可以放进两个大小为64字节的缓存行。因为一般参数关键字用到的字典大部分都只包 含1到3个关键字,所以小哈希表的这种优化措施也提高了函数调用的性能。
为字符串关键字优化
一个字典里可以包括多种数据类型的关键字。不过在大多数Python程序的类实例和模块中,它们内部字典只用字符串做关键字。
首先,字符串比较时不会抛出异常,所以可以掠过一些不必要的错误检查。其次,比较两个字符串是比较简单的事,不像一般Python对象可能会有<, >, <=, >=, ==和!=这样的比较操作。
Java版优化:
Python的Java版本,Jython有一个专用字符串的字典类型:org.python.org.PyStringMap。而用 户编程时所用的字典类实际上是另外一个叫做org.python.core.PyDictionary的类型,内部使用 java.util.Hashtable类来保存内容,并加了一个简介层来提供子类化能力。Python不允许用户用其它数据类型替换内建的 __dict__字典类,从而免去了子类化的麻烦(有什么麻烦?不明白)。对Jython来说,有一个专门的字符串类型字典还是有意义的。
C版优化:动态选择存储方法
一开始,字典调用之处理字符串的方法,当用户需要检索非字符串数据类型时,就调用能够处理通用数据类型的方法。 PyDictObject只有一个域成员ma_lookup,他是一个函数指针
struct PyDictObject{
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
}
字典类通过(dict->ma_lookup)(dict, key, hash)来查找关键字,里面的参数key是一个指针,它指向代表关键字的PyObject,参数hash是根据关键字算出来的哈希值。刚开始 时,ma_lookup指向函数lookdict_string,这个函数假定字典里所包含的关键字和待检索的关键字都是字符串类型的,用结构 PyStringObject表示。如果字典里有非字符串类型的关键字,或是用户想查找这样的关键字时,ma_lookup就会改成指向更加通用的查表函 数。当lookdict_string函数发现其参数的实际类型不是字符串时,它就会修改ma_lookup,并调用新的函数来进行查找。(这就是说,如 果你对一个纯字符串版本的字典进行非字符串检索时,比如d.get(1),整体检索速度会比通用的情况要慢,哪怕这是一次失败查询。接下来字典的检索函数 就会切换到通用版本。这时候就算查的还是字符串类型的关键字,查询速度已经不能跟纯字符串版本同日而语了。)PyStringObject类型的子类应该 被认为时非字符串类型,因为子类里可能会定义相等比较操作。
冲突处理
拉链法:由于建立链表需要为每个链表元素申请内存空间,而内存分配比较慢。同时,遍历链表有可能会降低缓存局部性。此法被Python弃用。
开放地址法:如果第一次在存储单元i中没有找到待查关键字,那就按照某种固定的模式尝试其他存储单元。
/* 起始存储单元 */
slot = hash;
/* 初始干扰值 */
perturb = hash;
while (<存储单元非空> && <单元内的值不等于关键字值>){
slot = (5*slot) + 1 + perturb;
perturb >>= 5;
}
在c语言的代码里,5*slot可以用位运算和加法来替代:(slot<<2) + slot 。一开始,干扰因子perturb直接取成哈希值;接下来,他的二进制值在每次循环的时候右移5位。移位操作可以让哈希值的每个比特都能够迅速地参与到探 索尝试的计算中。经过若干次移位操作,干扰因子最终会变为0,此时探索模式就退化为slot=(5*slot)+1,它生成的数值可以使0和 ma_mask之间的任何一个数。因此,这种尝试模式可以确保:要么是找到关键字(如果是在检索字典),要么是找到一个空的存储单元(如果是准备向字典里 插入数据)。
每次偏移量5比特是根据试验得出来的。 跟4和6相比,5可以把冲突率再降低一点(Objects/dictobject.c里有大段的注释详细讨论了这种优化的前世今生。)
调整大小
关于关键字数量,现在的做法是保证哈希表最多有三分之二是满的。这个比例是折中的结果:太满就会导致更多的冲突,太稀疏就会浪费太多内存,同时也不利于缓存处理。
确定新尺寸
不超过5万个关键字的中小规模哈希表,新的尺寸应该是ma_used*4。大多数使用大字典的Python程序都会在初始阶段构建字 典,接下来做的就只是检索关键字或者是遍历字典。这时4倍会让字典显得有点稀疏(1/4的存储单元有数据),好处是在初始阶段构建字典时减少了字典调整的 次数。对于超过5w个关键字的大型字典,规定新尺寸是ma_used*2,可以避免浪费过多的内存空间。
一个合算的内存折中方案:空闲列表
Python在用参数关键字作函数调用时,会创建字典实例,然后在函数返回时销毁它们。这种操作发生的很频繁,字典实例的生命周期也很短。这种情况的一个有效优化措施是回收那些不再使用的数据结构。
Python用一个叫做free_dicts的数组来保存不再使用的字典。在Python 2.5中,这个数组长度是80.当需要创建PyDictObject时,Python会设法从free_dicts里找到一个指针,重用它指向的数据结 构。当字典被删除时, 它们就会被加到free_dicts数组中。如果此时数组已经满了,那就直接释放这个字典对象。
迭代和动态变化
在遍历迭代的过程中,Python不允许增加或者删除字典的存储单元。 当iter*()系列方法第一次被调用时,迭代器会记住字典中所有元素的个数。如果在遍历迭代过程中,字典大小发生变化,迭代器就会抛出 RuntimeError异常,告诉用户:“迭代过程中字典的大小发生了变化”。