#!/usr/bin/env python # coding: utf-8 # #开发一个万年历 # 前面讲解的知识和工具足够应对日常的程序开发,但是对学生来说,总是觉得这些知识与真实开发之间还有些距离,其实,只是学生缺乏信心和经验。所以在这里设置一个体验软件开发过程的项目,利用所学过的工具和知识开发一个“一眼看去,摸不到头脑的程序”。体会一下点滴功能如何组合成一个完善的项目。 # 这个项目就是开发“万年历程序”,程序的用户输入年和月的数字,程序则输出用户指定的月历,例如输入2015年5月,程序将打印正确的包含星期的月历。 # 大多数学生在初学编程时都会认为自己“不足以开发”如此“摸不到头脑”的程序,其实知识和工具的储备足够了,细想一下,开发的困难在于找不到落脚点,而前面说到的“迭代增量”的方法就是先写出一个简单的框架版本,然后在此基础上不断完善,就像在马拉松的路上给自己设置一个个“可达到”的目标。你不用考虑如何完成全程,只需考虑如何到达比较近的目标。 # ##最初版本:只会打印 # 万年历程序开发的第一个目标就是:输出一个形式月历,即输出一个月历的形式,不需要任何“功能”。 # 第一阶段可以全部用打印完成,所以比较容易。以后每阶段都在以前的基础上前进了一小步,但每阶段都是完整的,每阶段都有一个令学生感兴趣的结果。这就是“迭代增量”的含义。开发这个项目时,千万不要让学生找个完整的万年历程序试图“读懂”,根本没有意义,一定要尝试并体会项目“生长”的过程。 # 先看看第一阶段的目标,忽略代码,只看结果: # In[10]: out_str = " SUN MON TUE WED 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 # 利用print打印这个“样子”并不难,关键在于需要将所有输出内容组成一个“字符串”,于是就先写以下代码: # In[11]: out_str = " SUN MON TUE WED THU FRI SAT " i = 1 for i in range(1,31): out_str = out_str + `i` print out_str # 现在的问题是:所有30天的数字挤在一起了,想一想,应该插入一些空格,然后逢7插入一个“回车”,这里的难点是利用好第一个被整除的数字:“0”。于是程序的代码变成了这样: # In[12]: out_str = " SUN MON TUE WED THU FRI SAT " i = 0 for i in range(31): if i%7 == 0: out_str = out_str + '\n ' + `i + 1` else: out_str = out_str + ' ' + `i + 1` print out_str # 很接近目标了,只是输出无法对齐,依靠程序中字符串加入的空格是很难“对齐”的,这时候就用上“字符串的格式化输出了”,代码如下: # In[13]: out_str = " SUN MON TUE WED 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 # ##第二阶段:确定打印天数 # 第一阶段完成后,需要确立第二阶段的目标,第二阶段的目标来自第一阶段的不足,我们把“以输入年、月来确定每月打印的天数”作为第二阶段的目标。每月输出多少天是由年和月共同决定的,比如闰年的2月就该输出29天。所以第二阶段的第一步比较简单,把原来循环里那个固定值“31”变成一个变量: # In[14]: out_str = " SUN MON TUE WED THU FRI SAT " i = 0 days_i_m = 31 for i in range(days_i_m): 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 # 我们增加了一个变量days_i_m,用它代替31控制了“打印天数”,那么,只要控制days_i_m就能控制“打印天数”了。在实际应用中这个”打印天数“应该和月关联,比如1月应该输出31天,而6月应该是30天。于是: # In[7]: year = 0 month = 0 year = input("Please input year:") month = input("Please input month:") out_str = " SUN MON TUE WED THU FRI SAT " i = 0 if month == 1 or month == 3 or month == 5 or month == 7 or month == 8 or month ==10 or month == 12: days_i_m = 31 elif month == 4 or month == 6 or month == 9 or month == 11: days_i_m = 30 else: days_i_m = 28 for i in range(days_i_m): 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 # 我们利用一系列的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都是可以被优化的。 # 代码中还有不妥之处:一是对“闰年2月”没做处理,另外如果能把一些判断和求值都“工具化”就好了,我们可以写两个函数,一个判断是否是闰年is_leap_year,另一个求输入的月份有多少天 days_in_month,为了使源文件结构更清晰,应该把工具性的函数都放到另一个新的源文件中去,这样还可以利于今后的复用,于是分化的两个源文件: # In[ ]: #calendar_tools.py tu_days_in_month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) def is_leap_year(y): if y % 4 == 0 and y % 100 != 0 or y % 400 == 0: return True else: return False def days_in_month(y,m): if is_leap_year(y) and m == 2: return 29 else: return tu_days_in_month[m] # calendar_tools.py是包含两个函数的工具包文件,函数is_leap_year用来计算输入的年份是否是闰年,tu_days_in_month常规每月包含天数的序列,若参数y是闰年函数将返回True否则返回False;函数days_in_month的用途是根据参数y(代表year),m(代表month)来计算指定的月份应该有多少天,之所以计算每月的天数时需要参数y,那是因为需要考虑一个特例:闰年的2月有29天。在days_in_month函数中调用了is_leap_year来确定输入的年份是否是闰年。 # In[ ]: from calendar_tools import * i_year = 0 i_month = 0 i_year = input("Please input year:") i_month = input("Please input month:") out_str = " SUN MON TUE WED THU FRI SAT " i = 0 wd = 0 #控制每月1日打印位置(或者说每月1日对应周几?) print_days = days_in_month(i_year, i_month) for i in range(print_days): pd = i + 1 - wd if i%7 == 0: bl_str = '\n{:^7d}'.format(pd) else: bl_str = '{:^7d}'.format(pd) out_str = out_str + bl_str print out_str # 由于使用了函数,程序就变得比较简洁易懂了,利用from和import引入calendar_tools.py中的函数,输入了年和月后直接利用函数days_in_month计算出了每月需要打印多少天,并赋值给print_days。注意我们定义了一个变量wd,这个变量将用来控制每月1日打印位置(或者说每月1日对应周几?),现在我们把它赋初值为0,并且不改变它,那么程序像以前一样打印并没有变化,后面就是顺理成章了。 # ##第三阶段:确定星期关系 # 如果在第二阶段的基础上确定某月一日与星期的对应关系后就能够输出正确的月历,所以我们可以把这个确定把第三阶段的目标,面对一个月历分析一下,每月从最左端开始(日期的1号对应星期日)可以方便地控制以7个日期加入一个回车的形式输出该月的天数,但是实际的月历并不都是从最左边开始的,怎么办呢?观察得知,若开始打印位置不是周日,那么打印结束的位置就会延展相应的天数。举个例子,若某月的1日是周3,那么开始和结束打印的位置将都向后3个打印位置。那么将每月1日对应的星期设为wd,其中周日对应wd=0,周六对应wd=6。那么把打印的范围修改成: # range(print_days+wd) # In[17]: tu_days_in_month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) def is_leap_year(y): if y % 4 == 0 and y % 100 != 0 or y % 400 == 0: return True else: return False def days_in_month(y,m): if is_leap_year(y) and m == 2: return 29 else: return tu_days_in_month[m] i_year = 0 i_month = 0 i_year = input("Please input year:") i_month = input("Please input month:") out_str = " SUN MON TUE WED THU FRI SAT " i = 0 wd = 3 #控制每月1日打印位置(或者说每月1日对应周几?) print_days = days_in_month(i_year, i_month) for i in range(print_days + wd): pd = i + 1 - wd if i%7 == 0: bl_str = '\n{:^7d}'.format(pd) else: bl_str = '{:^7d}'.format(pd) out_str = out_str + bl_str print out_str # 上面代码确定了wd=3,拓展了打印范围,从结果看有些奇怪,输入的是2015年4月,打印开始的位置正确了,但是日期从-2打印到30了,对此程序需要修补一下,控制打印0以上的数字照原样打印,其他数字打印占位符,这样就大功告成了。 # In[6]: tu_days_in_month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) def is_leap_year(y): if y % 4 == 0 and y % 100 != 0 or y % 400 == 0: return True else: return False def days_in_month(y,m): if is_leap_year(y) and m == 2: return 29 else: return tu_days_in_month[m] i_year = 0 i_month = 0 i_year = input("Please input year:") i_month = input("Please input month:") out_str = " SUN MON TUE WED THU FRI SAT " i = 0 wd = 3 #控制每月1日打印位置(或者说每月1日对应周几?) print_days = days_in_month(i_year, i_month) for i in range(print_days + wd): pd = i + 1 - wd if i%7 == 0: if pd > 0: bl_str = '\n{:^7d}'.format(pd) else: bl_str = '\n ' else: if pd > 0: bl_str = '{:^7d}'.format(pd) else: bl_str = ' ' out_str = out_str + bl_str print out_str # 我们在原有的if结构中嵌入了判断数字是否大于0的结构,至此第三阶段也完成了。程序已经能打印一个正常的月历,离最终的目标已经相当接近,其中“小技巧”固然重要,但是需要着重体会的是“迭代”的方法。下面只要让程序能够自动计算wd的值就可以了,这是第四阶段要解决的问题。 # ##第四阶段:完成“年历” # 如何计算wd呢,观察月历可以发现日期对应于星期的规律是7天一个循环,那么只要知道年份的1月1日对应的星期,然后用累计天数对7求余的方法就可以知道任意一天对应的星期了。比如2015年1月1日是周4,那么59天后的3月1日就是 (59+4)%7=0,这样可以计算出3月1日是周三。 # 我们需要再编写两个函数,一个是days_before_month函数用来计算要打印的月份的1日到1月1日之前一共有多少天,另一个week_day函数将利用days_before_month来计算指定月份的1日是周几: # In[ ]: def days_before_month(y,m): sum = 0 i = 0 while i < m: sum += days_in_month(y,i) i += 1 return sum def week_day(y,m): sum = days_before_month(y,m) return (sum+4)%7 # 至此,我们已经可以做到已知某年1月1日是星期几的情况下准确输出该年某月的月历了: # In[7]: tu_days_in_month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) def is_leap_year(y): if y % 4 == 0 and y % 100 != 0 or y % 400 == 0: return True else: return False def days_in_month(y,m): if is_leap_year(y) and m == 2: return 29 else: return tu_days_in_month[m] def days_before_month(y,m): sum = 0 i = 0 while i < m: sum += days_in_month(y,i) i += 1 return sum def week_day(y,m): sum = days_before_month(y,m) return (sum+4)%7 i_year = 0 i_month = 0 i_year = input("Please input year:") i_month = input("Please input month:") out_str = " SUN MON TUE WED THU FRI SAT " i = 0 wd = week_day(i_year,i_month) #控制每月1日打印位置(或者说每月1日对应周几?) print_days = days_in_month(i_year, i_month) for i in range(print_days + wd): pd = i + 1 - wd if i%7 == 0: if pd > 0: bl_str = '\n{:^7d}'.format(pd) else: bl_str = '\n ' else: if pd > 0: bl_str = '{:^7d}'.format(pd) else: bl_str = ' ' out_str = out_str + bl_str print out_str # ##第五阶段:完成万年历 # 现在我们的程序已经能够利用计算一年中某月之前的天数来确定某月的1日是周几,然后打印该月的月历,那么同样的道理,若知道从公元1年1月1日(星期一)起到某年某月之前一共有多少天,就可以知道该月1日应该打印位置了。可以将求总天数分成两部分,一部分是已经完成的:求当年天数,另一部分就是求该年份之前的所有年份的总天数,这个需求可以利用一个循环完成,其中闰年累加366,平年累加365。 # In[ ]: def days_before_year(y): sum = 0 i = 0 while i < y-1: if is_leap_year(y): sum += 366 else: sum += 365 i += 1 return sum # In[9]: tu_days_in_month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) def is_leap_year(y): if y % 4 == 0 and y % 100 != 0 or y % 400 == 0: return True else: return False def days_in_month(y,m): if is_leap_year(y) and m == 2: return 29 else: return tu_days_in_month[m] def days_before_year(y): sum = 0 i = 0 while i < y-1: if is_leap_year(y): sum += 366 else: sum += 365 i += 1 return sum def days_before_month(y,m): sum = 0 i = 0 while i < m: sum += days_in_month(y,i) i += 1 return sum def week_day(y,m): sum = days_before_year(y) + days_before_month(y,m) return sum%7 i_year = 0 i_month = 0 i_year = input("Please input year:") i_month = input("Please input month:") out_str = " SUN MON TUE WED THU FRI SAT " i = 0 wd = week_day(i_year,i_month) #控制每月1日打印位置(或者说每月1日对应周几?) print_days = days_in_month(i_year, i_month) for i in range(print_days + wd): pd = i + 1 - wd if i%7 == 0: if pd > 0: bl_str = '\n{:^7d}'.format(pd) else: bl_str = '\n ' else: if pd > 0: bl_str = '{:^7d}'.format(pd) else: bl_str = ' ' out_str = out_str + bl_str print out_str # ##项目总结 # 我们把万年历项目分了五个阶段,演示了迭代增量的开发方法,可以体会在开发的各个阶段间的迭代关系,而每个阶段的中间也利用迭代方式开发,开发就是这么个“猜想,计划”、“尝试,实现”、“测试,反思”、“修正,提高”,这就是开发领域的“PDCA”循环,迭代是个由简到繁“生长”的过程。 # 在软件开发领域出现过许多开发方法,例如传统的瀑布模型,现代的敏捷开发、极限编程等等,迭代增量是其中一种易于掌握的开发方法,其关键优势有: # 1.每次迭代完成后,都要交付一个可运行的项目,容易评估项目完成水平 # 2.各次迭代目标的焦点(阶段性的中心)明显、易理解、易达到 # 3.降低开发风险、可以持续部署和测试、代码复用率高 # ##项目完善 # 万年历项目还有一些可以继续完善的地方,比如: # 1.若输入的年份、月份不合规范则让用户重新输入 # 2.中国人的习惯是星期一放在月历的最前面而不是星期日 # ……