第四课 如何开发一个万年历----上篇
万年历程序是个有效的体验项目,它包含了程序开发所需要的技术和方法,甚至体现了很多软件工程的内容,项目要求输入某年、某月的整型数字,输出那年那月的月历,例如输入 2015 年 5月,项目应该打印正确的包含星期的月历。
万年历项目包括了开发程序的所有基本语法内容(除了自定义数据),这对掌握基本语法很有益,另外重要的是万年历项目的开发方法,见过许多人学和用这个例程,但是用明白的不多,嘿嘿,招式可以学,吐纳呼吸的方法没人讲那是不容易学得会的呀。还有一个项目和万年历一样,但稍微高阶一些,本节课的后面会稍微讲解一下关键,然后让大家自己实践,那就是一个扑克游戏“二十一点”。是否掌握了在一个操作系统上编程的方法或者是否掌握了一门语言,那么尝试写一个万年历和一个二十一点游戏吧。
本项目涉及的知识有:
1、变量定义
2、输入与格式化输出
3、函数
4、关系运算逻辑运算
5、集合:列表
6、程序流程:分支与循环
7、多文件机制
编程只有目标没有思路,这句话是我说的,但是漫漫开发路上总要给自己设立几个路标,就如同有个著名马拉松运动员说的那样,把漫漫长路分成几个可以达到的目标很有助于树立信心。
编程如雕塑,不象书法,“一蹴而就”基本不可能,开发就是设立一个大体目标,分解成阶段目标,然后修修补补,咋看起来,如何开发万年历程序是个“不摸门”的事,但是还是有几个可以达到的目标的:万年历程序开发的过程大约设定为5步,
第一阶段:形式月历,目标是输出一个“死的”月历
第二阶段:输入年月,确定每月应该输出多少天?
第三阶段:确定月历中的一号是星期几,然后输出月历
第四阶段:打印当年的任意月历(年历)
第五阶段:由你完成万年历
第一阶段是项目完成的大致形式,但是没有任何逻辑,全部用打印完成,所以比较容易。以后每阶段都在以前的基础上前进了一小步,但每一阶段都是完整的,每一阶段都有一个另开发者感兴趣的结果。这就是软件开发中的“迭代增量”。开发这个项目时千万不要找个完整的万年历程序试图“读懂”,根本没有意义,一定要尝试并体会项目“生长”的过程。
下面就来开发第一阶段,你会发现,即使是一个阶段目标,也会有两三次迭代
第一阶段:形式月历,目标是输出一个“死的”月历,如下图
我想先打印这样一个“样子”不难吧:关键在于需要将所有输出内容组成一个“字符串”
于是就先写了个这个例程1-1:
out_str= " SUN MON TUE WEN THU FRI SAT "
i=1
for i in range(1,31):
out_str=out_str+`i`
print out_str
结果打印成这样:
所有30天的数字挤在一起了,想一想,应该插入一些空格,然后逢7插入一个“回车”,这里的难点是利用好第一个被整除的数字:“0”
于是程序变成了这样,例程1-2:
out_str= " SUN MON TUE WEN THU FRI SAT "
i=0
for i in range(32):
if i%7==0:
out_str=out_str+'\n '+`i+1`
else:
out_str=out_str +" "+`i+1`
print out_str
运行发现,输出变成了这样:
很接近目标了,只是输出无法对齐,
依靠程序中字符串加入的空格是很难“对齐”的,这时候就用上“字符串的格式化输出了”,代码如例程1-3:
out_str= " SUN MON TUE WEN THU FRI SAT "
i=0
for i in range(31):
if i%7==0:
out_str=out_str+'\n{:^7d}'.format(i+1)
else:
out_str=out_str +'{:^7d}'.format(i+1)
print out_str
运行结果如下图
字符串的格式化输出需要了解的要点
1)格式:字符串.format( 表达式 )
2)字符串中加如形如{:^nd}的格式控制符,用以控制format中表达式的输出形式
3)常用格式控制符有:{:^m.nd}用来输出整形,n代表输出所占字符列数;{:^m.nf}用来输出浮点型,m代表输出所占字符列数,n代表小数点部分的位数。
4)若指定输出的字符列数大于实际输出,则输出空格占位;若实际输出字符数大于指定,则按实际输出。
至此,第一阶段完成了!
第二阶段:输入年月,确定每月应该输出多少天?
每月输出多少天是由年和月共同决定的,比如闰年的2月就该输出29天
所以第二阶段的第一步比较简单,把原来循环里那个固定值“31”变成一个变量,所以程序变成了例程2-1的样子:
例程2-1源文件和运行的结果
在例程2-1的第5行,增加了变量days_i_m,在第7行它代替31控制了“打印天数”,那么,只要控制days_i_m就能控制“打印天数”了,在实际用应用中这个”打印天数“应该和月关联。于是,代码变成了例程2-2所示:
注意例程2-2的 12到17行 利用一系列的if elif和else 确定了输入的month和打印天数的关系,但是这个结构不够巧妙,资深程序员的经常考虑的问题就是减少程序中的if,特别是象“排比”一样的if使人眼晕。
有一句名言:程序=算法+数据结构,这句话固然说明了程序由算法和数据结构组成,另外更关键的含义是算法和数据结构之间是“和的关系”,这意味着:数据结构恰当,算法就可以简单一些。所以我们可以把每个月的天数组织起来放入一个元组(数据不可变的列表)中:
days_month=(0,31,28,31,30,31,30,31,31,30,31,30,31),由于元组的下标从0开始,而月份从1开始,所以在元组的0元素位置使用0占个位置,这样打印天数就简单地变为:
days_month[month]了,省略了一大堆if,其实只要计算得当,内存充裕,任何一个程序的任何一个if都是可以被优化的(为什么和if有那么大“仇恨”?那是因为if占用资源较多)。
例程2-2还有一个不妥之处,那就是对“闰年2月”没做处理,另外如果能把一些判断和求值都“工具画”就好了,于是想写两个函数,一个判断是否是闰年is_leap_year,另一个求输入的月份有多少天 days_in_month,为了使源文件结构更清晰,应该把工具性的函数都放到另一个新的源文件中去,这样还可以利于今后的复用,于是例程2-3的两个源文件如下:
例程2-3
例程2-3包含两个函数的工具包文件: can_2_3_tool.py,这个文件包含了两个函数,其中is_leap_year用来计算输入的年份是否是闰年,第7行定义了常规每月包含天数的序列,第10行是闰年的判断条件,从第11行和13行得知若参数y是闰年函数将返回True否则返回False;函数days_in_month的用途是根据参数y(代表year),m(代表month)来计算指定的月份应该有(打印)多少天,之所以计算每月的天数时需要参数y,那是因为需要考虑一个特例:闰年的2月有29天。在days_in_month函数中调用了is_leap_year来确定输入的年份是否是闰年。
由于使用了函数,calendar2_3的主要源程序文件就变得比较简洁易懂了,第2行,利用from和import引入can_2_3_tool.py中的函数,第7,8行输入了年和月,第17行直接利用函数days_in_month计算出了每月需要打印多少天,并赋值给print_days,应该注意到了,程序的第15行定义了一个变量wd,这个变量的将用来控制每月1日打印位置(或者说每月1日对应周几?),现在我们把它赋初值为0,并且不改变它,那么程序像以前一样打印并没有变化,后面就是顺理成章了。
程序运行的结果如下:
结果表明第二阶段也成功了。